В первой части мы изучили некоторые вопросы безопасности хранения и передачи данных. Теперь переходим к защите исполняемого кода. Мы будем модифицировать функционал iOS-приложения во время выполнения и проделаем реверс-инжиниринг. И снова, помните! Наша цель — не стать гадким взломщиком, а защитить ваше приложение и пользователей от злонамеренных действий. Для этого нужно понять, что может сделать взломщик.
Чтобы успешно пройти этот урок, вы должны представлять, что такое ассемблер. Автор статьи советует пройти туториал по ARM (на английском).
Вообще-то, чтобы понять смысл урока, уровень необходимых знаний — это пару минут погуглить по ходу дела. Ну вы ближе к концу статьи сами решите, нужно изучать ассемблер или нет. :) — Прим. пер.
Начнём
Нам понадобится:
- Тестовый проект Meme Collector из предыдущей части.
- Утилита class-dump-z.
- Опенсорсный HEX-редактор для OS X: Hex Fiend.
- Демо-версия IDA — это известный мультипроцессорный дизассемблер и отладчик. У демо-версии есть ограничения, несущественные для данного урока.
Надеюсь, вы узнаете об этих инструментах много нового!
Манипуляции со средой выполнения (runtime)
В предыдущей серии мы модифицировали файлы .plist для изменения остатка на вашем счёте. Сейчас посмотрим, как манипулировать переменными и методами прямо во время выполнения приложения (то, что зовётся runtime). Для этого используем дебаггер LLDB.
Откройте в терминале папку главного бандла (Meme Collector.app
), установленного на симулятор iOS. Если вы затрудняетесь это сделать, загляните в первую часть.
Занимаем исходную позицию: симулятор запущен, приложение установлено, но не запущено.
В терминале наберите:
lldb
Дебаггер запущен, отлично. На следующей строчке мы видим приглашение от него: (lldb)
Набираем команду для дебаггера:
я не буду писать символы (lldb)
в начале строки, чтобы вы ничего не перепутали при копировании
attach --name "Meme Collector" --waitfor
Команда attach
служит для подключения к определённому процессу. Здесь мы просим LLDB, чтобы он подождал запуска нового процесса с названием "Meme Collector
" и подключился к нему.
Итак, дебаггер ждёт. Перейдём к Симулятору iOS и выполним традиционное (по прошлой части урока) удаление приложения из многозадачности, и далее повторный запуск (запускать именно из симулятора, не из IDE) — далее будем называть это «перезапуск».
Если всё сделано правильно, LLDB начнёт вместе весело шагать с процессом в симуляторе. Отладчик подключится к процессу, приостановит его выполнение и скажет:
Process 1427 stopped
Executable module set to "/Users/dmitriy/Library/Application Support/iPhone Simulator/7.0.3/Applications/9A72F266-8851-4A25-84E4-9CF8EFF95CD4/Meme Collector.app/Meme Collector".
Architecture set to: i486-apple-macosx.
И приглашение для ввода новой команды: (lldb)
Давайте добавим точку останова (breakpoint) перед отображением каждого ViewController’а. Это как раз то место, где обычно происходит много интересных вещей. Зачастую там определяется значительная часть логики работы приложения. Например, давайте добавим точку останова на каждый вызов метода viewDidLoad
, так как в iOS подклассы UIViewController
’а почти всегда переопределяют viewDidLoad
.
Выполните в терминале:
b viewDidLoad
Названия методов чувствительны к регистру, поэтому вариант viewdidload
не пройдёт.
Этим мы устанавливаем точки останова на все методы, которые называются viewDidLoad
(включая методы C++ и Objective C). При желании, для конкретных селекторов ObjC вы можете ввести их имена, например, -[UIViewController viewDidLoad]
, но обратите внимание, что этот вариант не сработает для наследников класса UIViewController
.
Итак, LLDB сообщает нам, что он нашёл 15 подходящих мест для точек останова:
Breakpoint 1: 15 locations.
Отлично. Посмотрим, куда он их поставил. Вводим команду:
br l
(Это сокращение от breakpoint list
— если хотите, можете писать полную версию команды.)
Ну и вот они:
Current breakpoints:
1: name = 'viewDidLoad', locations = 15, resolved = 15
1.1: where = Meme Collector`-[ViewController viewDidLoad] + 18 at ViewController.m:27, address = 0x0001f482, resolved, hit count = 0
1.2: where = UIKit`-[UIViewController viewDidLoad], address = 0x005d3db5, resolved, hit count = 0
1.3: where = UIKit`-[_UIModalItemsPresentingViewController viewDidLoad], address = 0x0065ab4b, resolved, hit count = 0
1.4: where = UIKit`-[UIKeyboardCandidateGridCollectionViewController viewDidLoad], address = 0x00680729, resolved, hit count = 0
1.5: where = UIKit`-[UIActivityGroupViewController viewDidLoad], address = 0x008d2b6b, resolved, hit count = 0
1.6: where = UIKit`-[UIPrintPanelTableViewController viewDidLoad], address = 0x009be80f, resolved, hit count = 0
1.7: where = UIKit`-[UIPrintStatusViewController viewDidLoad], address = 0x009c8828, resolved, hit count = 0
1.8: where = UIKit`-[UIPrintRangeViewController viewDidLoad], address = 0x009d29ae, resolved, hit count = 0
1.9: where = UIKit`-[_UILongDefinitionViewController viewDidLoad], address = 0x00a10cf4, resolved, hit count = 0
1.10: where = UIKit`-[_UINoDefinitionViewController viewDidLoad], address = 0x00a1249d, resolved, hit count = 0
1.11: where = UIKit`-[UIReferenceLibraryViewController viewDidLoad], address = 0x00a13bd4, resolved, hit count = 0
1.12: where = UIKit`-[_UIFallbackPresentationViewController viewDidLoad], address = 0x00a77877, resolved, hit count = 0
1.13: where = UIKit`-[_UIViewServiceViewControllerOperator viewDidLoad], address = 0x00aba23b, resolved, hit count = 0
1.14: where = UIKit`-[UIActivityViewController viewDidLoad], address = 0x00b4f296, resolved, hit count = 0
1.15: where = UIKit`-[_UITextEditingController viewDidLoad], address = 0x00b9a6ec, resolved, hit count = 0
На самом деле, тут видно, что нам нужно оставить только один breakpoint: -[ViewController viewDidLoad]
, так как остальные принадлежат Apple Private API. Но нам интересно, поэтому оставим их включенными.
Вернёмся к запуску нашего приложения! Введите команду:
c
что в полном варианте выглядит как continue
. Приложение продолжит выполнение кода вплоть до первого вызова viewDidLoad
:
Process 1427 resuming
Process 1427 stopped
* thread #1: tid = 0x83c4, 0x0001f482 Meme Collector`-[ViewController viewDidLoad](self=0x08f7c620, _cmd=0x00c50587) + 18 at ViewController.m:27, queue = 'com.apple.main-thread, stop reason = breakpoint 1.1
frame #0: 0x0001f482 Meme Collector`-[ViewController viewDidLoad](self=0x08f7c620, _cmd=0x00c50587) + 18 at ViewController.m:27
24
25 - (void)viewDidLoad
26 {
-> 27 [super viewDidLoad];
28 self.memeDescriptionTextView.clipsToBounds = YES;
29 self.memeDescriptionTextView.layer.cornerRadius = 20.0f;
30 [self.moneyLabel sizeToFit];
«А тепе-е-ерь… пора нам позабавиться: а то я не играю!»
Мы остановили процесс на кадре (frame) класса ViewController
(файл ViewController.m). Значит, у нас есть доступ к его переменным экземпляра (instance variables) и к методам. Круто? И ещё! Секция кода уже загружена в память. Следовательно, у нас есть доступ ко всем другим классам, включая — внимание! — синглтоны.
Да, синглтоны. Если вы внимательно изучили этот момент в первой части, вы могли заметить «интересный» класс под названием MoneyManager
. У него есть метод purchaseCurrency
, который так и хочется потестировать, а? :)
Наберите в нашем (lldb)
-терминале:
call [[MoneyManager sharedManager] purchaseCurrency]
Мы вызвали метод! Отладчик выведет результат выполнения:
(BOOL) $0 = YES
Если мы увидели ответ YES
— значит, мы успешно «приобрели» виртуальную валюту. (Тут автор проболтался, это инсайдерская информация. Мы — взломщики — не должны её знать. — Прим. пер.)
А ещё LLDB повторяет предыдущую команду по нажатию Enter. Поэтому нажмите несколько раз Enter, чтобы ещё немного ограбить Михалкова:
(lldb) call [[MoneyManager sharedManager] purchaseCurrency]
(BOOL) $0 = YES
(lldb)
(BOOL) $1 = YES
(lldb)
(BOOL) $2 = YES
(lldb)
(BOOL) $3 = YES
(lldb)
(BOOL) $4 = YES
(lldb)
(BOOL) $5 = YES
(lldb)
(BOOL) $6 = YES
(lldb)
(BOOL) $7 = YES
(lldb)
Приобретение и так бесплатного контента никогда не было таким лёгким! Теперь пару раз введите команду
c
…чтобы закончились все breakpoint'ы, которые мы понаставили, и оцените результат в симуляторе:
Неплохо, да? Ну-ка посмотрим, что мы можем с этим сделать.
Чтобы приостановить выполнение приложения и снова вернуться в командную строку, переключитесь на терминал и там нажмите Ctrl+C. Отладчик LLDB снова готов выполнять наши команды.
Давайте пока закончим сеанс отладки: введите команду q
и затем для подтверждения y
:
(lldb) q
Quitting LLDB will detach from one or more processes. Do you really want to proceed: [Y/n] y
Возвращаемся на сторону разработчика. Можно ли перехитрить желающих манипулировать вашим приложением через отладчик?
Защита от runtime-манипуляций
К счастью, есть способ узнать, подключен ли отладчик к нашему коду! Но есть одна проблема. Эта проверка определяет, подключен ли отладчик в данное конкретное время. Хакер (крэкер, читер, ...) может подключиться к приложению после этой проверки, когда приложение уже не подозревает об опасности. Эту проблему можно решить по меньшей мере двумя способами:
- Включить проверку в цикл выполнения (run loop), так что проверка будет выполняться постоянно.
- Поставить проверку в наиболее критичных участках кода, где нас больше всего волнует безопасность.
Первый вариант обычно нежелателен. Его цена — трата драгоценного процессорного времени на нагрев девайса. Давайте пойдём по второму пути.
Одно элегантное решение — сделать проверку на активность отладчика в синглтоне MoneyManager
. Например: если мы определили, что происходит отладка, то вернуть nil
вместо статического экземпляра класса.
Ну наконец-то, поработаем с кодом! Откройте наш проект (в вашей любимой IDE или в Xcode) и перейдите к файлу MoneyManager.m. Вот что мы сделаем: добавим макрос препроцессора, который проверит, в какой конфигурации собрано наше приложение, и если это Release, то проверит, запущен ли отладчик. Если запущен, вернёт nil
. Иначе всё выполняется, как обычно.
Добавьте 3 строчки в начало метода sharedManager
класса MoneyManager
:
#ifndef DEBUG
SEC_IS_BEING_DEBUGGED_RETURN_NIL();
#endif
Теперь метод должен выглядеть так:
SEC_IS_BEING_DEBUGGED_RETURN_NIL()
— это вызов стандартного макроса препроцессора, который возвращает nil
, если к приложению подключен отладчик.
Обратите внимание: этот макрос доступен только в конфигурации Release. Если вы следили за нами в части первой, вы уже должны были переключиться на релиз.
AppCode: Run > Edit configurations… > Configuration: Release.
А теперь запустите наше приложение из IDE (не забыли выбрать конфигурацию Release?)
Xcode: Run (⌘R)
AppCode: Debug (Ctrl+D)
Xcode автоматически подключает отладчик LLDB при выборе команды Run. Результат: остаток на счёте не отображается! Действительно, где-то там nil
:
А в AppCode существуют две разные команды: команда Run не подключает отладчик, а команда Debug — подключает. Удобно.
Чтобы окончательно убедиться в том, что наша защита работает, проверьте: сможете ли вы сейчас купить что-то? MoneyManager
недоступен — значит, не сможете.
Остановите приложение, нажав в IDE кнопку Stop (с квадратом). Также остановите дебаггер LLDB. Переключитесь на симулятор и запустите приложение оттуда. Приложение отобразит валюту, т.к. отладчик не подключен.
Как мы уже говорили, подключить отладчик к процессу можно не только при запуске, но вообще в произвольный момент времени. Выполните в терминале:
ps aux | grep "Meme Collector"
Вывод данной команды будет содержать список всех процессов, в имени которых встречается фраза «Meme Collector»:
dmitriy 2008 0,0 0,0 2432784 636 s001 S+ 1:05 0:00.00 grep Meme Collector
dmitriy 2001 0,0 0,4 857416 32240 ?? S 1:04 0:00.65 /Users/dmitriy/Library/Application Support/iPhone Simulator/7.0.3/Applications/9A72F266-8851-4A25-84E4-9CF8EFF95CD4/Meme Collector.app/Meme Collector
Можно видеть, что вторая строчка соответствует папке приложения в симуляторе. Обратите внимание на номер этого процесса (второй столбец). В моём случае это число 2001.
Из терминала запустим LLDB с ключом -p
, чтобы он подключился к процессу по номеру:
lldb -p {ваш номер процесса}
Например, мне нужно набрать «lldb -p 2001».
LLDB запустится и сообщит об успешном подключении к процессу:
Attaching to process with:
process attach -p 2001
Process 2001 stopped
Executable module set to "/Users/dmitriy/Library/Application Support/iPhone Simulator/7.0.3/Applications/9A72F266-8851-4A25-84E4-9CF8EFF95CD4/Meme Collector.app/Meme Collector".
Architecture set to: i486-apple-macosx.
Когда LLDB запущен, попробуйте обратиться к синглтону MoneyManager
:
call [[MoneyManager sharedManager] purchaseCurrency]
Попытка «купить» валюту теперь возвращает NO
, то есть, не проходит.
А попробуем напечатать описание объекта sharedManager
. Наберите команду:
po [MoneyManager sharedManager]
И что же в этом описании?
nil
Чего и требовалось добиться! Наш синглтон не возвращает хоть сколько-нибудь вразумительный результат, а также не показывает сообщение об ошибке в процессе покупки. Простой и непонятный взломщику nil
.
Продолжим выполнение приложения командой:
c
Попробуйте легально пополнить свой счёт по кнопке «Purchase Currency». Ничего не выйдет! Ведь LLDB по-прежнему подключен к процессу.
Отключите отладчик от процесса: нажать Ctrl+C и затем ввести команду q. Кнопка «Purchase Currency» снова работает.
В дополнение к проверке на наличие отладчика, можно применить более жёсткий подход. Функция ptrace
помогает по возможности сопротивляться подключению GDB / LLDB к вашему приложению.
Для этого вернитесь в IDE и откройте main.m. Добавьте один заголовочный файл:
#include <sys/ptrace.h>
И три строчки в начало функции main
:
#ifndef DEBUG
ptrace(PT_DENY_ATTACH, 0, 0, 0);
#endif
Функция ptrace
обычно используется в отладчиках, чтобы подключиться к процессу, как это делают — как мы видели — GDB и LLDB. Мы добавили вызов ptrace
, который со специальным параметром PT_DENY_ATTACH
просит операционную систему запретить другие процессам (то есть, отладчикам) подключаться к нашему приложению.
Теперь запустим приложение из IDE.
Xcode: приложение как будто не запускается. Что происходит? Мы видим на мгновение чёрный экран, который тут же исчезает — это приложение загружается в память и начинает выполняться. При этом Xcode хочет подключить к нему LLDB, но iOS не разрешает и завершает процесс отладчика. «Раз отладчик завершён, — думает Xcode, — приложение завершилось, поэтому остановлю-ка его выполнение». Последняя фраза звучит дико, но всё работает именно так. — Прим. пер.
AppCode: по команде Run (⌘R) приложение нормально запускается, а по команде Debug (Ctrl+D) «падает» подобно Xcode.
И из симулятора запускается корректно. Попробуйте теперь подключить к нему отладчик, как обсуждалось выше:
lldb -p {номер процесса Meme Collector}
Результат — предсказуемый:
Attaching to process with:
process attach -p 3435
error: attach failed: process did not stop (no such process or permission problem?)
Это хорошее средство, чтобы остановить маленьких детей, начитавшихся хабра, от игр с вашим приложением. Но это не остановит бородатых хакеров. Они остановят ваш процесс при вызове функции ptrace
и модифицируют её перед продолжением.
В общем, не чувствуйте себя слишком комфортно. Хакеры любят использовать Cycript — язык скриптов (напоминающий JavaScript) — именно для манипуляций над ObjC-приложениями во время выполнения. Защита от дебаггера, которую мы сделали, не защитит вас от Cycript. Помните, с чего мы начали разговор в предыдущей статье:
Ни одно приложение не является безопасным!
Препарируем бинарник
Прежде чем приступить к модификации бинарных файлов, давайте выясним, как его разобрать на части, и что к чему.
Я буду периодически ссылаться на конкретные адреса в бинарнике для иллюстрации определённых понятий. Если у вас не такая версия компилятора, как у меня (например, поставляемая с более новой Xcode), либо вы компилируете конфигурацию Debug вместо Release, либо сами вносили изменения в проект — адреса могут быть другими. Пусть это вас не смущает — просто следите за изложением, чтобы понять идею.
Формат исполняемых файлов в OS X и iOS называется Mach-O. Как правило, бинарник начинается с заголовка (header), содержащего всю информацию о том, где и какие данные есть в бинарнике. За этой информацией следуют команды загрузки (load commands), которые расскажут вам о разметке файла по сегментам. Кроме того, эти команды определяют специальные флаги: например, зашифрованы ли двоичные данные в файле.
В каждом сегменте (segment) есть одна или несколько секций (sections). Два вида секций стоит отметить:
- Текстовая секция. В основном для данных, предназначенных только для чтения (read-only). Например, исходный код, Си-строки, константы и т.д. Особенность read-only данных в том, что если в системе заканчивается оперативная память, она может легко освободить данные из этих разделов и в дальнейшем (если понадобится) снова загрузить из файла.
- Секция данных. В основном для тех данных, которые могут быть модифицированы из кода. Они включают в себя BSS-секции для статических переменных, common-секцию для глобальных переменных и т.д.
А ещё у Apple есть превосходный справочник по формату Mach-O на английском. — Прим. пер.
Теперь мы будем исследовать бинарный файл Meme Collector
, чтобы увидеть всё это в действии. Начнём с заголовка. В терминале, по-прежнему находясь в папке главного бандла «Meme Collector.app», введите:
otool -h "Meme Collector"
Эта команда напечатает заголовок исполняемого двоичного файла «Meme Collector». Приблизительно так:
Meme Collector:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedface 7 3 0x00 2 25 3372 0x01000085
Заметим: файл имеет 25 команд загрузки (cmds
), а занимают они 3372 байт (sizeofcmds
). Посмотрим на эти команды:
otool -l "Meme Collector"
(Перед этим можно очистить окно терминала, нажав ⌘K. Так будет удобнее листать. — Прим. пер.)
Вы получите много-много строк. Из этих строк (даже без предварительной подготовки) можно увидеть много интересного о порядке загрузки сегментов и секций в память. Но это исследование выходит за рамки данного туториала, оставим его наиболее любопытным читателям на самостоятельное изучение.
А мы продолжаем наш урок. Найдите (⌘F) секцию под названием __objc_classname
, обратите внимание на offset
— это «позиция» или «сдвиг» данной секции относительно начала виртуальной памяти, занимаемой приложением.
offset
. Наиболее любопытные, может быть, уже поняли разницу между addr
и offset
, почему эта разница везде равна 0x1000 = 4096 байт? Если ещё нет, то почитайте подробнее про __PAGEZERO
, очень интересная страница.
Здесь сдвиг секции __objc_classname
равен 159942 байт (в десятичном представлении). На изображении ниже, в левой части — подчёркнуто красным.
Перейдите в терминал. Откройте новое окно терминала (⌘N) и из той же папки «Meme Collector.app» выполните:
strings -o "Meme Collector"
Команда strings
ищет строки в бинарном файле, а флаг -o
напишет у каждой строки её позицию относительно начала файла.
Ну-ка, что у нас находится по адресу 159942? Имена классов! (Выделены красным.) Логично, мы же искали секцию __objc_classname
:
Непосредственно над этой секцией мы видим секцию __objc_methname
, она начинается начиная с 140887 — здесь у нас имена методов (выделены синим), начиная с метода init
.
init
идёт первым?
Там, где имена методов заканчиваются, сразу начинаются имена классов. Секция __objc_classname
идёт сразу после секции __objc_methname
. В командах загрузки они шли друг за другом — и загружаются в память последовательно.
Итак, мы видим, как команды загрузки (load commands) позволяют упорядочить тот хаос, который представляет из себя бинарный файл Mach-O. С этим знанием мы приступаем… тадааам! к модификации секции кода.
Тяжёлая артиллерия: дизассемблер и реверс-инжиниринг
Вы готовы пустить в ход серьёзные пушки? Наконец мы узнаем, как модифицировать бинарный файл приложения!
Вероятно, вы часто слышите в жизни фразу: приложение «взломано». Это значит, кто-то модифицировал приложение так, что оно работает… ммм… иначе, чем задумывал разработчик. Например, не просит зарегистрироваться. Поэтому мы (автор и переводчик) искренне надеемся, что наш труд послужит вам во благо. Только для защиты ваших приложений.
Скачайте IDA Demo и какой-нибудь HEX-редактор, например, Hex Fiend. IDA — это инструмент, к которому хакеры обращаются чаще всего при изучении бинарников. Это невероятно мощный дизассемблер, отладчик и декомпилятор. И полная версия стоит не так уж дорого.
Но если вы ещё не готовы купить программу, о которой услышали 15 секунд назад, IDA предлагает демонстрационную версию с ограниченным функционалом. В демо-версии ограничены типы ассемблерных файлов, которые можно в нём изучать. А также отключена возможность модификации кода.
Но в ней есть наш тип ассемблера x86. А все модификации мы будем делать вручную в другой программе — Hex Fiend.
Установите и запустите IDA. Нас приветствует Ада Лавлейс (Ada Lovelace) — первый в мире программист:
Нажмите кнопку Go. В терминале мы (да-да) всё ещё находимся в папке бандла (Meme Collector.app
). Введите следующую команду, чтобы показать данную папку в Finder'е:
open -R .
Не забудьте про точку в конце. Символ точки здесь означает «текущую папку».
Далее в открывшемся окне Finder: правой кнопкой мыши > Показать содержимое пакета:
(OS Mavericks в русском переводе называет бандл (bundle) «пакетом», но мне это название кажется неинформативным. — Прим. пер.)
Внутри бандла-пакета вы найдёте исполняемый файл Meme Collector
, перетащим его в окно IDA и получим диалоговое окно:
Действительно, IDA определил, что этот бинарный файл является исполняемым файлом архитектуры i386.
Ваши настройки должны соответствовать тем, что изображены выше (думаю, вам не придётся ничего менять) — и нажимайте «Поехали!» OK. Дизассемблер разберёт файл по мелким кусочкам и составит его схему (mapping) — то, чем мы занимались выше, но… как сказать… более профессионально. :)
Если спросит «Objective-C 2.0 structures detected. Do you want to parse them and rename methods?» — отвечайте Yes. Если спросит что-то про «proximity view», отвечайте No.
Когда IDA закончит обработку бинарного файла, вы конечно же, придёте в шок увидите главный экран. Если окно IDA не очень похоже на то, что ниже, то в левой панели найдите имя функции start
, щёлкните на нём и затем нажимайте пробел до тех пор, пока не увидите вот такую красивую блок-схему:
(В моём случае оказалось недостаточно одного пробела, пришлось ещё Enter нажать раз-другой. Ну вы быстро разберётесь, что к чему. — Прим. пер.)
И откройте проект в Xcode или AppCode. В целях сокращения изложения, мы чуть-чуть подсмотрим в код.
Откройте MoneyManager.m и взгляните на метод buyObject:
- (BOOL)buyObject:(id<PurchasableItemProtocol>)object
{
NSUInteger totalMoney = self.money.unsignedIntegerValue;
NSUInteger cost = [object cost].unsignedIntegerValue;
if (totalMoney < cost) {
return NO;
}
_money = @(totalMoney - cost);
return [self saveState];
}
Изучите алгоритм, он довольно простой. Если переменная эклемпляра _money
не имеет достаточной суммы, чтобы расплатиться, то функция вернёт NO
, и транзакция не будет выполнена. Этот условный оператор, разрешающий/запрещающий покупку, полагается на одно булево значение: достаточно ли у пользователя денег? (Напоминает поведение некоторых людей в магазине? — Прим. пер.)
Если бы эту проверку обойти («перепрыгнуть», в терминах ассемблера) — можно было бы купить всё, что угодно, тогда значение _money
уже бы не рассматривалось как фактор при покупке.
Теперь найдём тот же код в дизассемблере. Вернитесь к IDA, щёлкните по любой функции на панели Functions (просто чтобы активировать эту панель) и затем нажмите Сtrl+F (или из меню: Edit > Quick Filter). Появится поле ввода для поиска нашей функции. Нам нужно найти buyObject:
Ага, нашли, дважды щёлкните на названии метода. IDA покажет окошко дизассемблера, которое отлично демонстрирует условный оператор и ветвление кода:
Даже если вы ничего не знаете из школьного курса ассемблера, из сравнения с исходным кодом buyObject:
можно предположить, что зелёная стрелка «вправо» — именно то место, куда мы хотим хакерски пойти, там выполняется много действий. А краткий код под красной стрелкой «влево» больше похож на лаконичное "return nil
".
Немного нагружу вас ассемблером. Посмотрим на условный оператор («jump»), самый нижний в верхнем блоке, из которого идут две стрелки — красная и зелёная. Это jnb
, что означает «jump if not below» (перейти, если «не меньше»). Судя по всему, мы должны заменить эту инструкцию на «jump always» — инструкцию jmp
.
Чтобы инструкцию заменить, нужно её найти. Дважды щёлкните на операнде jnb
. Он подсветится жёлтым. Теперь нажмите пробел, чтобы перейти в текстовый режим.
Здесь та же информация, но в линейном виде. Найдите номер строки с командой jnb
(эта строка выделена):
В моём случае адрес оказался 0x00018D88. Напомню, у вас может быть любой другой адрес.
Код операнда "jnb short
" — это 0x73??
, где знаки вопроса означают относительное смещение в байтах, куда мы хотим перейти. Нам нужно поменять код операнда на 0xEB??
— код безусловного перехода "jmp short
" (на то же количество байт). Откуда я взял коды операндов? Например, из Intel Software Developer’s Manual (кстати, захватывающее чтение!)
Скачайте (если ещё не скачали) Hex Fiend. Установите его, например, скопировав в папку /Applications
. Из терминала (предполагая, что мы по-прежнему находимся в папке бандла «Meme Collector.app») наберите команду:
open -a "/Applications/Hex Fiend.app/" "Meme Collector"
Откроется окно с нашим бинарником. Красиво, правда? Вот они — наши знакомые: __objc_classname
и другие секции. Перед нами явно заголовок исполняемого файла.
Теперь наберите в терминале:
otool -l "Meme Collector" | grep -a10 "sectname __text"
Как мы видели ранее, otool -l
отображает команды загрузки двоичного файла в память. Нас интересует секция кода («текстовая» секция), поэтому мы сужаем область поиска командой grep
. Получили примерно такой ответ:
segname __TEXT
vmaddr 0x00001000
vmsize 0x0002e000
fileoff 0
filesize 188416
maxprot 0x00000007
initprot 0x00000005
nsects 11
flags 0x0
Section
sectname __text
segname __TEXT
addr 0x00002970
size 0x0001dec3
offset 6512
align 2^4 (16)
reloff 0
nreloc 0
flags 0x80000400
reserved1 0
reserved2 0
Здесь мы видим стартовый адрес секции (addr
) 0x00002970, а сдвиг (offset
) — 6512 (десятичное число). Вы можете взять IDA и наглядно убедиться, что стартовый адрес, с которого начинается код — именно 0x2970, для этого надо проскроллить (в «линейном» виде) на самый-самый верх. (Напоминаю, ваши конкретные значения могут отличаться, но смысл тот же).
Отлично! Время заняться арифметикой: нужно пересчитать смещение инструкции jnb
(найденное для «текстовой» секции) в абсолютное значение внутри бинарного файла. Если вы попробуете изменить байты по адресу, найденному в IDA, вы наверняка где-то словите крэш, т.к. они не совпадают.
Чтобы нам не сильно отвлекаться, я приготовил для вас формулу:
{абсолютная позиция команды в бинарном файле} =
{адрес команды} – {стартовый адрес текстовой секции} + {сдвиг текстовой секции}
В моём случае:
Адрес команды jnb
= 0x18D88 (из IDA)
Стартовый адрес текстовой секции = 0x2970 (из otool
)
Сдвиг текстовой секции = десятичное 6512 (из otool
)
Берём калькулятор, переключаем из меню: Вид > Для программиста… (не забудьте переключать на нужную систему счисления при вводе десятичных и 16-ричных чисел).
У меня получилось:
0x18D88 – 0x2970 + 6512 = 0x17D88
Если ваши расчёты оказались верны, это и будет адрес той инструкции jnb
, которую мы видели в IDA. Теперь в Hex Fiend нажмите ⌘L (либо из меню Edit > Jump To Offset), чтобы открыть поле ввода адреса. Введите своё значение адреса (если вводите в шестнадцатиричном формате, не забудьте 0x
в начале).
Хм, номера строк почему-то в десятичном виде. Ну ладно, пересчитаем: 0x17D88 = 97672, т.е. от позиции 97664 нужно отсчитать ещё 8 байт вправо. 8 байт = 16 шестнадцатиричных цифр = два 4-байтных слова. Видите, Hex Fiend группирует бинарный «текст» по словам:
Пропускаем первые два слова, а в начале третьего слова — вот он — наш код операции 0x7304
. 0x73
— код инструкции, а 0x04
— смещение, на сколько байт процессор должен «прыгнуть» вперёд.
Исправьте 0x73
на 0xEB
(аккуратно: одно нажатие Backspace удаляет сразу 1 байт = два шестнадцатиричных символа). Сохраните (⌘S) и закройте файл. Откройте симулятор, удалите приложение из памяти и снова запустите (именно из симулятора, а не из IDE, чтобы не скомпилировалось заново). «Покупайте» мемы, пока у вас не закончатся деньги. Что произошло, когда вы попытались купить товар, который стоит дороже, чем у вас осталось «денег»?
Да, мы действительно выкинули проверку условия «есть ли у пользователя деньги?» Даже без денег, транзакция выполняется. И небольшой бонус: unsigned-значение _money
«зацикливается», в силу особенностей представления чисел в памяти, вместо отрицательного становится чуть меньше, чем 1032 (около 4 миллиардов).
Защита от реверс-инжиниринга
Как же нам защититься? Помните, я говорил: «ничто не безопасно». Это утверждение работает и здесь. Реверс-инжиниринг можно сильно затруднить, но вы не можете остановить взломщика, если он настроен серьёзно. Ваша единственная надежда — запутать злоумышленников настолько, что они бросят это дело и пойдут ломать другие приложения.
Один из способов — изменить имена важных классов и методов через препроцессор. Откройте проект в IDE и найдите в нём файл «Meme Collector-Prefix.pch». Добавьте в него строчку:
#define MoneyManager DS_UIColor_Theme
Этот код заменит все вхождения "MoneyManager
" на название, которое покажется взломщикам менее интересным: "DS_UIColor_Theme
".
Данный подход должен применяться с большой осторожностью, чтобы ничего не сломать. Нужно убедиться на 100%, что выбранное новое имя больше нигде не встречается в вашем приложении. Иначе запутаете сами себя, с приложением начнут происходить необъяснимые вещи.
Обычно в исполняемом файле есть таблица символов, в которой хранится отображение адресов в читабельные имена функций и методов. И вот, ещё один способ запутать код — удалить таблицу символов после сборки проекта. Это больше подходит для того, чтобы спрятать функции Cи и C++, потому что сообщения Objective C обрабатываются одной функцией objc_msgSend()
.
Снова откройте MoneyManager.m и добавьте следующую Си-функцию в начало:
BOOL aSecretFunction(void) {
return YES;
}
Затем снова скомпилируйте приложение. Проверим существование данной функции в таблице символов. Из терминала:
nm "Meme Collector" | grep aSecretFunction
Команда nm
выводит таблицу символов, а grep
фильтрует по имени функции. Вот, она тут есть:
00018b8f t _aSecretFunction
Лёгкий способ удалить таблицу символов из iOS-приложения — в настройках проекта найти две опции: Deployment Postprocessing и Strip Linked Product, и выставить их в Yes:
Затем необходимо «очистить» проект (Xcode: Product > Clean или в AppCode: Run > Clean) и заново скомплировать. После чего перейдите в терминал и выполните ту же команду:
nm "Meme Collector" | grep aSecretFunction
Превосходно! Мы успешно удалили символ, который ссылался на aSecretFunction()
. Теперь взломщику придётся потратить больше времени, чтобы найти критичные моменты в коде.
Что дальше?
Мы убедились, что злоумышленник может:
- легко увидеть названия селекторов Objective C;
- манипулировать файлами, к которым обращается ваше приложение;
- перехватывать и модифицировать сетевое взаимодействие;
- управлять средой выполнения;
- менять исполняемый файл вашего приложения.
При создании приложения важно помнить эти вещи. Подумайте, сколько усилий вы готовы приложить, чтобы сделать приложение более безопасным. Что такое безопасность? Это всегда баланс между вашими ресурсами (временем), уровнем проблем для ваших пользователей, и — вероятностью взлома.
Безопасность iOS-приложения — это серьёзная тема. Можно ещё многому научиться. Пока мы только чуть-чуть поскребли по поверхности. Весь спектр возможностей отладчика и других инструментов для анализа кроется гораздо глубже. Если вам интересна эта тема, советую подумать о джейлбрейке тестового девайса. Файловая система даст богатую пищу для исследований.
Если у вас нет проблем с английским, обязательно ознакомьтесь с книгой Hacking and Securing iOS Applications (автор Jonathan Zdziarski). Хотя она слегка устарела (придётся погуглить на предмет изменений в механизме шифрования приложений Apple), но у автора статьи это одна из любимых книг по iOS и по безопасности.
Ещё пара книг:
Hacking: The Art of Exploitation, 2nd Edition by Jon Erickson
Mac OS X and iOS Internals: To the Apple's Core by Jonathan Levin
Форумы:
http://www.woodmann.com
http://www.reddit.com/r/ReverseEngineering
Статья по инъекции кода:
http://blog.timac.org/?p=761
Автору можно писать в комменты, а по переводу пишите на почту dev@
x128.ru.
Автор: x256