Безопасность IOS-приложений (часть 21) – Основы ARM и GDB

Безопасность IOS-приложений (часть 21) – Основы ARM и GDB

На данный момент все устройства с операционной системой IOS базируются на архитектуре ARM. Изначально (перед преобразованием в машинный код) программа, написанная на Objective-C, конвертируется в ассемблерный код совместимый с архитектурой ARM. Тот, кто хорошо знаком с «ARM-ассемблером» и владеет отладчиком GDB, вполне может расшифровать код программы во время ее выполнения и даже модифицировать его.

Автор: Пратик Джианчандани (Prateek Gianchandani)

На данный момент все устройства с операционной системой IOS базируются на архитектуре ARM. Изначально (перед преобразованием в машинный код) программа, написанная на Objective-C, конвертируется в ассемблерный код совместимый с архитектурой ARM. Тот, кто хорошо знаком с «ARM-ассемблером» и владеет отладчиком GDB, вполне может расшифровать код программы во время ее выполнения и даже модифицировать его.

В этой статье мы будет учиться отлаживать простейшее приложение GDB-Demo, которое можно скачать с моего аккаунта на github. Установите и запустите это приложение на вашем IOS-устройстве. Если у вас нет учетной записи разработчика, инструкции по запуску приложений в этом случае приведены в седьмой статье из этой серии.

Подключимся к устройству через SSH:

Рисунок 1: Подключение к устройству

Теперь запускаем GDB и подцепляемся к запущенному приложению при помощи команды attachwaitfor Appname. При желании вы также можете запустить приложение на вашем устройстве и подцепиться к запущенному процессу, используя команду attach, как показано ниже:

Рисунок 2: Подключение к запущенному процессу

После подключения к приложению, вы заметите, что оно находится на паузе. Вы можете продолжить выполнение программы, используя команду c, но прежде давайте проведем некоторые исследования. Как и в любой другой архитектуре в ARM память делится на регистры размером 32 бита (в IOS 7 – эти регистры размером 64 бита). Функции этих регистров – хранение и перемещение данных между собой. Информацию о регистрах можно получить при помощи команды info registers.

Рисунок 3: Получение информации о регистрах

Примечание: вышеупомянутая команда выводит информацию не обо всех регистрах. Чтобы вывести информацию обо всех регистрах, используйте команду info all-registers.

Рисунок 4: Информация обо всех регистрах архитектуры ARM

Чтобы увидеть дизассемблированный код, используйте команду disassemble (или disas), которая выводит перечень нескольких следующих инструкций. Мы также можем вывести ассемблерный код конкретной функции, указав ее название в качестве параметра команды disas. Например, для функции main используем команду disas main.

Рисунок 5: Ассемблерный код функции main

Если мы запустим наше тестовое приложение, то увидим, что оно запрашивает имя пользователя и пароль.

Рисунок 6: Тестовое приложение, запрашивающее имя пользователя и пароль

Изучив выгруженную информацию о классах при помощи class-dump-z, мы видим класс ViewController и метод -(void)loginButtonTapped:(id)tapped;

Рисунок 7: Прототип класса ViewController

При помощи GDB мы можем устанавливать точки останова внутри приложения, используя команду b functionName. Вы можете напечатать просто имя метода без указания класса, и GDB спросит имя класса, в котором вы хотите установить точку останова.

Рисунок 8: Перечень методов экземпляров классов, на которые можно установить точку останова

Обратите внимание, что у методов экземпляров классов вначале стоит префикс -, а у методов класса стоит префикс +, как показано ниже. Например, sharedInstance – метод класса, который возвращает общий экземпляр класса «singleton».

Рисунок 9: Перечень методов классов, на которые можно установить точку останова

Увидеть все точки останова можно при помощи команды info breakpoints.


Рисунок 10: Перечень всех точек останова

Удалить точку останова можно при помощи команды delete. В качестве параметра команды необходимо установить идентификатор точки останова (значение из колонки Num).

Рисунок 11: Удаление точки останова

Установим точку останова на метод loginButtonTapped:

Рисунок 12: Точка останова на методе loginButtonTapped

Теперь возвращаемся к выполнению программы, используя команду c.

Рисунок 13: Продолжение выполнение приложения

Нажимаем на кнопку «Login» и программа останавливается на методе loginButtonTapped:

Рисунок 14: Сработала точка останова

После этого мы можем увидеть несколько инструкций, которые следуют после точки останова:

Рисунок 15: Выводим перечень инструкций после точки останова при помощи команды disas

Чтобы установить точку останова на конкретную инструкцию, установите знак * перед адресом этой инструкции:

Рисунок 16: Устанавливаем точку останова на конкретной инструкции

В третьей статье этой серии мы рассматривали функцию objc_msgSend, которая связана с пересылкой сообщений и вызывается каждый раз, когда сообщение отправляется. В дизассемблированном коде функции loginButtonTapped есть множество вызовов objc_msgSend. За каждый вызов objc_msgSend отвечает инструкция blx.

Рисунок 17: Один из вызовов функции objc_msgSend (см. выделенную инструкцию blx)

При вызове любого нового метода или считывании информации из свойства вызывается функция objc_msgSend. Следовательно, если мы установим точку останова на вызов функции objc_msgSend, то сможем узнать вызываемый метод и объект, которому принадлежит этот метод, а это, в свою очередь, поможет нам лучше разобраться в логике работы приложения. В девятой статье этой серии мы уже рассматривали утилиту Snoop-it, которая помогла нам узнать информацию обо всех совершенных вызовах. Чтобы узнать информацию обо всех вызываемых методах, для начала нам необходимо ознакомиться с соглашением о вызовах для архитектуры ARM. Ниже показан скриншот этого соглашения, взятый из Википедии.

Рисунок 18: Соглашение о вызовах для архитектуры ARM

Из этого соглашения важен следующий тезис:

Рисунок 19: Выдержка из соглашения – регистры r0 - r3 используются для хранения аргументов, передаваемых в подпрограмму, и результатов работы этой подпрограммы

Таким образом, мы можем установить точку останова на каждый вызов objc_msgSend и при ее срабатывании, считывать параметры, передаваемые в функцию, из регистров r0-r3. Ниже показан скриншот из документации Apple для функции objc_msgSend:

Рисунок 20: Описание работы функции objc_msgSend

Первые два аргумента – self и op. Аргумент self – указатель на экземпляр класса, который получает сообщение. Аргумент op – селектор метода, который обрабатывает сообщение. Селектор – не что иное, как сигнатура для сообщения. Например, если у метода прототип -(void)addOjectsToArray:(NSArray *)array, сигнатурой в этом случае будет addOjectsToArray:. Как мы уже знаем, регистры r0 - r3 используются для хранения аргументов, передаваемых в функцию, следовательно, можно сделать вывод, что регистр r0 – содержит аргумент self, а регистр r1 – аргумент op.

Давайте рассмотрим эту концепцию на конкретном примере. Установим точку останова на функции objc_msgSend и продолжим выполнение программы до тех пор, пока эта точка останова не сработает.

Рисунок 21: Сработала точка останова на функции objc_msgSend

Как мы уже выяснили, регистр r0 должен содержать указатель на экземпляр класса, который получает сообщение, а регистр r1 – селектор. Все последующие регистры, начиная с r2, содержат аргументы, которые передаются в метод. Вначале давайте рассмотрим команду x, которая помогает в исследовании памяти. Мы также можем задать формат отображения содержимого памяти. Все опции этой команды можно получить, используя команду help x.

Рисунок 22: Перечень опций команды x

Давайте рассмотрим содержимое регистра r0, содержащего указатель на экземпляр класса, который получит сообщение. Формат отображения – адресный, и мы будем использовать команду x/a. Так как мы хотим исследовать память, перед r0 поставим символ $.

Рисунок 23: Содержимое регистра r0

Мы видим, что получатель сообщения – экземпляр класса UIRoundedRectButton. Теперь посмотрим содержимое регистра r1, который, как мы уже знаем, содержит сигнатуру метода. Для этого используем команду x/s (информация будет отображаться в формате строки).

Рисунок 24: Содержимое регистра r1

Теперь разберемся с аргументами, которые передаются в метод. Это может оказаться не так просто, поскольку мы не знает формат хранения данных в регистре r2. Однако взглянув на селектор, respondsToSelector, и используя немного здравого смысла, мы можем предположить, что аргумент – это селектор, и, следовательно, мы вновь используем команду x/s.

Рисунок 25: Содержимое регистра r2

Из селектора метода мы точно знаем, что в метод передается один аргумент, следовательно, исследовать остальные регистры нет необходимости. Таким образом, вызов метода выглядит примерно так:

-[UIRoundedRectButton respondsToSelector:@selector(debugDescription)];

Кроме того, во время работы программы происходит множество вызовов функции objc_msgSend, и анализировать каждый из этих вызовов займет слишком много времени. Попробуем автоматизировать этот процесс. В третьей статье этой серии мы узнали о том, как задавать вызов команд в gdb всякий раз, когда срабатывает точка останова:

Рисунок 26: Перечень команд, вызываемых при срабатывании точки останова

Теперь продолжим выполнение программы, используя команду c, и увидим все вызываемые методы. Это позволит нам лучше понять логику работы приложения.

Рисунок 27: Перечень всех вызываемых методов

Теперь давайте выведем эту информацию в более удобочитаемой форме, используя синтаксис Objective-C. В этом нам поможет функция class_getName, которая упоминается в документации Apple. Как видно из скриншота ниже, в функцию передается один аргумент, объект класса. Следовательно, мы будем передавать в функцию содержимое регистра r0.

Рисунок 28: Выдержка из документации для функции class_getName

Теперь немного перепишем формат отображения данных при срабатывании точки останова:

Рисунок 29: Новый формат отображения информации при срабатывании точки останова

Когда мы вернемся к выполнению программы, то увидим, что информация имеет более удобочитаемый вид.

Рисунок 30: Информация отображается в новом формате

Теперь мы лучше понимаем, что происходит внутри приложения. В следующей статье мы будем использовать знания, полученные в этой статье, и научимся манипулировать приложением во время его выполнения при помощи GDB.

Домашний Wi-Fi – ваша крепость или картонный домик?

Узнайте, как построить неприступную стену