приложением, которое:
По последнему пункту: кто вообще так делает???
Всё, что мы делаем (например, выполняем моддинг TikTok, чтобы он показывал только видео с котиками, или устраняем торможения в чужих приложениях), требует возможности исследования работы приложения.
Но в приложениях для iOS очень часто используются дополнительные защиты от любопытных глаз, например, обнаружение джейлбрейка или обфускация кода.
Похоже, это приложение стало на удивление интересной комбинацией всего перечисленного.
Намного более сложной, чем можно было бы ожидать от обычного старого виджет-приложения.
Давайте рассмотрим по порядку каждую из мер защиты и разберёмся, как их все обойти.
Мы записали видеоверсию поста, в которой процесс показан ещё подробнее:
Давайте начнём с привязки отладчика, потому что это может позволить нам позже разобраться и с другими мерами защиты.
Я запускаю это приложение на телефоне с джейлбрейком; обычно благодаря этому очень легко привязать к приложению отладчик. Можно подключиться к телефону по
А затем подключиться к этому
Далее мы сможем взаимодействовать с приложением, как нам угодно: выводить отладочную информацию, задавать контрольные точки, чтобы отслеживать поток выполнения кода и так далее.
Теоретически, так это и должно работать, но если сделать всё это с нашим приложением-виджетом, то возникнет проблема запуска
Мы получаем segmentation fault, а отладчик не привязывается к приложению.
… после чего телефон целиком уходит в «мягкий» перезапуск/перезапуск рабочего стола (respring) из-за ещё одной меры защиты. Но мы до этого доберёмся!
Мы не можем подключить отладчик из-за функции
Вызов
По сути, приложение для iOS может добавить эту функциональность двумя способами, и способ её обхода зависит от используемой приложением стратегии.
Простейший способ интеграции этого в приложение — вызов функции
Мы указываем для
Однако поскольку это приватный API, полная подготовка для такого вызова будет чуть более замороченной — нужно сначала найти указатель на метод, самостоятельно определить информацию о типе функции и так далее.
Если добавить этот блок кода в приложение для iOS и попытаться запустить его, то приложение на секунду запустится, а потом будет завершено; но только если к нему прикреплён отладчик! Если приказать Xcode не отлаживать запускаемый исполняемый файл (сняв флажок
Итак, мы разобрались, как работает
У этой функции есть одно слабое место, которое легко использовать — она блокирует отладчик только после её вызова.
Так что если мы поместим контрольную точку в любом месте до вызова, допустим, в начале показанной выше функции
Это даёт нам очень много возможностей по обходу последующего вызова
Тогда если продолжить выполнение, то мы увидим, что отладчик прекращает выполнение при вызове функции
Пока мы останавливаемся прямо в начале функции
Мы успешно пропустили вызов
Мы можем попробовать эту стратегию на приложении-виджете, снова запустив
Вместо этого мы укажем интересующий нас процесс на стороне
Если теперь мы снова попробуем запустить приложение, то увидим, что
Но если мы установим контрольную точку на
В конечном итоге контрольная точка ресолвится, но она не задействуется, и мы вылетаем из приложения с кодом ошибки. А спустя пару секунд телефон перезапускается!
Очевидно, мы столкнулись здесь с чем-то более серьёзным.
Выше я говорил, что в iOS есть два способа интеграции этой функциональности
Во-первых, очень легко найти и пропустить этот вызов
Во-вторых, нам нужно выполнять подозрительно выглядящий поиск в среде выполнения при помощи
Здесь мы можем убить двух зайцев одним выстрелом, полностью пропустив этот вызов функции
Как мы видим, сама реализация на удивление короткая. В начале и конце есть обработка ошибок, но самая важная часть функции — это команда
Это системный вызов, то есть функция делегирует всю сложную работу ядру; это логично для столь низкоуровневой задачи, как интеграция отладки.
Некоторые разработчики вместо вызова этой функции
Не волнуйтесь, если слабо разбираетесь в ассемблере, тут всё намного проще, чем кажется.
Во-первых, у нас есть те же четыре параметра, которые мы передавали вызову функции
Далее нам нужно сообщить ядру, какой системный вызов мы хотим выполнить. iOS ожидает, что эта информация будет передана в регистре
А затем мы выполняем сам системный вызов.
Вот и весь ассемблерный код. Если мы заново запустим наш пример приложения, добавив эту функцию, то увидим то же поведение, что и раньше — приложение выбросит нас, как только будет подключён отладчик, без вызова приватной функции
Такой способ Apple распознать немного сложнее, а нам немного сложнее обойти.
Здесь нет стандартной функции наподобие
Для этого нам нужно открыть дешифрованную копию приложения в дизассемблере и найти способ поиска этих команд.
К счастью, изучив эти команды, мы найдём хорошего кандидата для поиска:
Это конкретное значение, записываемое в конкретный регистр. Вряд ли в обычном коде такое будет встречаться очень часто, поэтому поиск такой команды — хороший способ сужения пространства поиска.
В большинстве дизассемблеров есть опция поиска по сырому дизассемблированному тексту вывода, но это может быть очень медленно, плюс вы можете споткнуться об небольшие различия в форматировании, например, в отображении чисел в шестнадцатеричном или десятичном виде.
Такой опцией можно воспользоваться при необходимости, но есть вариант и получше — искать байты, соответствующие этой команде.
Я люблю пользоваться для этого armconverter.com. Он позволяет вставить ассемблерную команду, например
и получить соответствующие байты; в данном случае:
Затем мы можем поискать эти байты в двоичном файле, чтобы найти потенциальные места выполнения системного вызова
Поискав байты, соответствующие
С этим вполне можно работать. Нажав на каждый из результатов, мы не увидели в первых двух ничего особо подозрительного, то есть соседние команды не очень походили на ассемблерный код из примера выше.
Но взглянув на третий результат, мы нашли именно тот ассемблерный код, который и искали!
Отлично, именно эта часть приложения мешает нам привязать отладчик. И оказалось, что четвёртое вхождение — это всего лишь другая ветвь той же функции ниже по коду. Вполне очевидно, что именно эту функцию мы хотим пропатчить.
Снова прикрепим к приложению отладчик точно так же, как делали это раньше, воспользовавшись командой
Мы используем
Теперь если мы продолжим выполнение, то попадём в одну из контрольных точек!
Как и ожидалось, мы остановились на команде
Есть пара вариантов того, как предотвратить действие этого вызова, но легче всего просто полностью обойти команду. Это можно сделать, попросив
Если мы выполним
И если теперь продолжим выполнение… то окажется, что отладчик привязан!
Ровно до того момента, пока мы не дойдём до второй меры защиты, вызывающей мягкий перезапуск всего телефона.
У нас возникло очень важное отличие от других случаев, когда мы сталкивались с мягкой перезагрузкой/respring: в этот раз
Устранив проблему с отладчиком, мы могли глубже разобраться с мягким перезапуском в целом. Мы могли посмотреть, какой процесс отправлял сигнал на перезагрузку, и запускали
Он выполняет функцию, захватывающую содержимое экрана, что довольно интересно. Кроме того, мы нашли последний адрес, по которому мы проходим по коду самого приложения — во
Первым делом мы бы хотели посмотреть на эту функцию в дизассемблере, но проблема заключалась в том, что этот адрес относителен процессу, загруженному в память, о котором наш дизассемблер ничего не знает.
Но мы можем попросить у
Во второй строке этого вывода содержится адрес относительно самого двоичного файла —
В декомпилированном виде эта функция выглядит так:
В бесконечном цикле мы вызываем
Но любопытна окружающая его реализация. Этот метод очень активно использует память, но мы ничего не делаем с результатом, просто создаём в бесконечном цикле образы!
Иногда ошибки возникают при декомпиляции, но если взглянуть на граф этой функции, то увидим то же самое — выполняется бесконечный цикл, и в этом цикле происходит только вызов
Нужно подчеркнуть, насколько это безумно: разумеется, при использовании слишком большого объёма памяти приложение будет завершено, но, похоже, кто-то нашёл метод, который при слишком быстром вызове приводит к мягкому перезапуску всего телефона (на самом деле, позже я выяснил, что эту стратегию использует как минимум одна джейлбрейк-утилита, чтобы выполнять respring телефона по запросу пользователя). Затем они решили воспользоваться этой информацией и написали функцию для вызова метода в бесконечном цикле, чтобы перезапускать телефон каждого, кто запустит этот код.
Потрясающе!
В видео мы чуть глубже оттрассировали источник этого вызова и выяснили, что он запускается, когда срабатывает уведомление
Внутреннее устройство этой системы любопытно, но чтобы обойти эту защиту, нам не нужно слишком вдаваться в подробности — если мы снова запустим
И теперь мы можем полностью войти в приложение с прикреплённым отладчиком!
В начале статьи мы написали ещё об одной проблеме — при попытках инъецирования нового кода в приложение оно тоже вылетает.
Я часто инъецирую код, когда подключён отладчик, и мне нужны более сложные утилиты, позволяющие глубже исследовать приложение, например, быстро создать лог информации о доступности всех кнопок на экране. Это можно сделать и при помощи одного лишь
Но инъецировать код требуется и тогда, когда у вас вообще нет телефона с джейлбрейком — инъецирование инструментов наподобие Frida и Flex позволяет приступить к исследованию приложения без необходимости джейлбрейка. Обычно их инъецирование выполняется при помощи инструмента, который переподписывает приложение (мне нравится Sideloadly), но если сделать это в нашем случае, но при запуске приложения оно сразу же вылетит.
То есть это ещё одна мера защиты, проверяющая, загружены ли в среде выполнения фреймворки, и выполняющая вылет в случае нахождения чего-то неожиданного?
Некоторые приложения ведут себя так, но к счастью, не в данном случае. Существует гораздо более простое объяснение. Можно исследовать вылет, например, или при помощи
В данном случае вылет отображается на вершине трассировке стека как
Декомпиляция этого метода чуть длиннее, но просканировав её, мы найдём строку, выделяющуюся, как вероятная причина вылета:
Проблема в том, что группы приложений определяются в рамках процесса подписывания кода, а мы как раз избавились от старой подписи приложения, инъецировав код и переподписав приложение. При этом мы потеряли все группы.
То есть этот метод, который должен возвращать URL, указывающий на локальную файловую систему, вероятно, возвращает
На самом деле, это достаточно распространённая проблема, и хотя здесь она действует как мера защиты, это вряд ли было сделано намеренно. Это приложение-виджет, поэтому логично, что ему потребуется находиться в группе приложений с его расширением приложения-виджета, управляющим отображением виджетов на основном экране.
Самым простым способом обойти эту проблему будет… отказ от переподписывания приложения. Телефон с джейлбрейком сильно упрощает это — можно инъецировать фреймворки в приложение при помощи утилиты джейлбрейка без необходимости переподписывать его. Или даже можно запускать код с недопустимыми подписями; можно добавлять любые приложения в любые группы. Джейлбрейк позволяет избавиться от множества проблем с подписыванием кода.
Однако при необходимости в случае инъецирования фреймворка отладки, когда нет устройства с джейлбрейком, можно иногда обойти подобные проблемы, даже переподписав приложение.
Приложение вылетает, потому что ожидает возврата URL, но не получает его. Мы можем создать маленький фреймворк, который преобразует этот метод, чтобы URL всегда возвращался:
В данном случае мы делаем так, чтобы при каждом вызове метода
Стоит отметить, что это не эквивалентная замена тому, что пытается сделать приложение — этот метод вызывается, потому что приложению нужна общая папка, к которой имеют доступ она и её расширения, но мы обманываем её, говоря «Вот твоя общая папка», хотя это точно не она.
Но в некоторых случаях это может быть вполне приемлемо. Я выяснил, что подобные небольшие патчи часто ломают сами расширения приложений, но меня они обычно не волнуют — мне важно, чтобы нормально работало основное приложение, особенно если моя цель заключается в исследовании того, как приложение выполняет определённую задачу, и мне достаточно работы базовой функциональности.
Возможно, в этом случае можно сделать что-то более продвинутое, например, создать собственную группу приложений, переподписать основное приложение и все использующие его расширения, а затем подменить этот метод (а также остальные, с которыми вы столкнётесь), чтобы он использовал новый идентификатор группы. Но для этого понадобится гораздо больше усилий, и труд вряд ли того стоит — зачастую проще вместо этого найти устройство с джейлбрейком.
Как бы то ни было, если мы соберём этот фреймворк и инъецируем его в приложение со всем остальным, что нам нужно (например, с
Недавно я столкнулся с - Блокирует прикрепление к нему отладчиков.
- Выполняет преждевременный выход при попытках инъецирования кода.
- Приводит к вылету телефона целиком, если запустить её со включённым джейлбрейком (!).
По последнему пункту: кто вообще так делает???
Всё, что мы делаем (например, выполняем моддинг TikTok, чтобы он показывал только видео с котиками, или устраняем торможения в чужих приложениях), требует возможности исследования работы приложения.
Но в приложениях для iOS очень часто используются дополнительные защиты от любопытных глаз, например, обнаружение джейлбрейка или обфускация кода.
Похоже, это приложение стало на удивление интересной комбинацией всего перечисленного.
Намного более сложной, чем можно было бы ожидать от обычного старого виджет-приложения.
Оказалось, функции приложения намного интереснее, чем у обычного старого приложения Widget, но это уже тема для отдельного поста!
Давайте рассмотрим по порядку каждую из мер защиты и разберёмся, как их все обойти.
Видеоверсия статьи
Мы записали видеоверсию поста, в которой процесс показан ещё подробнее:
PT_DENY_ATTACH
Давайте начнём с привязки отладчика, потому что это может позволить нам позже разобраться и с другими мерами защиты.
Я запускаю это приложение на телефоне с джейлбрейком; обычно благодаря этому очень легко привязать к приложению отладчик. Можно подключиться к телефону по
ssh
, а затем запустить debugserver
, прикреплённый к нужному приложению:$ /var/jb/usr/bin/debugserver 0.0.0.0:4445 -a AppStore
А затем подключиться к этому
debugserver
с другого компьютера при помощи lldb
:$ lldb
(lldb) platform select remote-ios
(lldb) process connect connect://localhost:4445
Process 303 stopped
Target 0: (AppStore) stopped.
Далее мы сможем взаимодействовать с приложением, как нам угодно: выводить отладочную информацию, задавать контрольные точки, чтобы отслеживать поток выполнения кода и так далее.
Теоретически, так это и должно работать, но если сделать всё это с нашим приложением-виджетом, то возникнет проблема запуска
debugserver
:$ /var/jb/usr/bin/debugserver 0.0.0.0:4445 -a TopWidget
debugserver-@(#)PROGRAM:LLDB PROJECT:lldb-1403.2.3.13
for arm64.
Attaching to process TopWidget...
Segmentation fault
Мы получаем segmentation fault, а отладчик не привязывается к приложению.
… после чего телефон целиком уходит в «мягкий» перезапуск/перезапуск рабочего стола (respring) из-за ещё одной меры защиты. Но мы до этого доберёмся!
Мы не можем подключить отладчик из-за функции
ptrace
. ptrace
— это приватный API в iOS, но в macOS этот API публичный, то есть мы можем легко найти его документацию.ptrace
очень крута, благодаря ей работает основная функциональность отладки. Но здесь особенно любопытна одна операция — PT_DENY_ATTACH
. Цитата из документации:PT_DENY_ATTACH
Этот запрос проверяет, используется ли другая операция трассируемым процессом; он позволяет процессу, который пока не трассируется, запрещать будущие трассировки от своего родителя. Все остальные аргументы игнорируются. Если процесс в текущий момент трассируется, то он выполняет выход со статусом ENOTSUP; в противном случае он устанавливает флаг, запрещающий будущие трассировки. Попытка трассировки родителем процесса, задавшего этот флаг, приведёт к segmentation violation у родителя.
Вызов
ptrace
с типом запроса PT_DENY_ATTACH
предотвращает все будущие запросы отладки, а если приложение уже отлаживается, то произойдёт полный выход из приложения. Это очень полезно, если вы не хотите, чтобы кто-то копался в вашем приложении!По сути, приложение для iOS может добавить эту функциональность двумя способами, и способ её обхода зависит от используемой приложением стратегии.
Простейший способ интеграции этого в приложение — вызов функции
ptrace
, как и можно предположить. Сам вызов метода достаточно прост:ptrace(PT_DENY_ATTACH, 0, 0, 0);
Мы указываем для
ptrace
тип запроса PT_DENY_ATTACH
, чтобы сообщить, что хотим помешать подключению отладчиков; далее идут ещё три обязательных параметра, но, судя по документации, для этого типа запроса они не используются, поэтому мы присваиваем им всем значение 0
.Однако поскольку это приватный API, полная подготовка для такого вызова будет чуть более замороченной — нужно сначала найти указатель на метод, самостоятельно определить информацию о типе функции и так далее.
#import <dlfcn.h>
#import <sys/types.h>
// Тип, представляющий функцию `ptrace`
typedef int (*ptrace_ptr_t)(
int request,
pid_t pid,
caddr_t addr,
int data
);
// Значение запроса для `PT_DENY_ATTACH`
#define PT_DENY_ATTACH 31
void __attribute__((constructor)) prevent_debugging(void) {
// Получам дескриптор `libsystem_kernel.dylib`
void *libsystem_kernel_handle = dlopen(
"/usr/lib/system/libsystem_kernel.dylib",
RTLD_GLOBAL | RTLD_NOW
);
// Находим символ `ptrace` в этом дескрипторе
ptrace_ptr_t ptrace = dlsym(
libsystem_kernel_handle,
"ptrace"
);
// Вызываем ptrace с PT_DENY_ATTACH
// Все остальные аргументы игнорируются
ptrace(PT_DENY_ATTACH, 0, 0, 0);
// Закрываем дескриптор
dlclose(libsystem_kernel_handle);
}
Если добавить этот блок кода в приложение для iOS и попытаться запустить его, то приложение на секунду запустится, а потом будет завершено; но только если к нему прикреплён отладчик! Если приказать Xcode не отлаживать запускаемый исполняемый файл (сняв флажок
Scheme > Edit Scheme > Run > Info > Debug executable
), то приложение запустится без проблем!▍ Обход PT_DENY_ATTACH (низкий уровень сложности)
Итак, мы разобрались, как работает
PT_DENY_ATTACH
; но как нам обойти его?У этой функции есть одно слабое место, которое легко использовать — она блокирует отладчик только после её вызова.
Так что если мы поместим контрольную точку в любом месте до вызова, допустим, в начале показанной выше функции
prevent_debugging
, а затем запустим приложение заново, то отладчик подключится без проблем.Это даёт нам очень много возможностей по обходу последующего вызова
ptrace
. Можно сделать это множеством разных способов, но, вероятно, проще всего установить контрольную точку на самой ptrace
:b ptrace
Тогда если продолжить выполнение, то мы увидим, что отладчик прекращает выполнение при вызове функции
ptrace
:(lldb) bt
* thread #1, queue = 'com.apple.main-thread',
stop reason = breakpoint 2.1
* frame #0: 0x000000010109d7d0
libsystem_kernel.dylib`__ptrace
Пока мы останавливаемся прямо в начале функции
ptrace
, то есть можем использовать thread return
, чтобы вернуться туда, где мы были до вызова, не вызывая при этом функцию, после чего снова продолжить выполнение.thread return
con
Мы успешно пропустили вызов
ptrace
— приложение запустилось, а отладчик по-прежнему подключён!Мы можем попробовать эту стратегию на приложении-виджете, снова запустив
debugserver
, но на этот раз не привязывая его ни к одному запущенному процессу:$ /var/jb/usr/bin/debugserver 0.0.0.0:4445
Вместо этого мы укажем интересующий нас процесс на стороне
lldb
, на этот раз воспользовавшись флагом --waitfor
, чтобы указать, что процесс пока не был запущен, но мы хотим подключиться к нему, как только он будет запущен:$ lldb
(lldb) platform select remote-ios
(lldb) process connect connect://localhost:4445
(lldb) process attach --name TopWidget --waitfor
Если теперь мы снова попробуем запустить приложение, то увидим, что
lldb
успешно подключился к приложению до выполнения его кода, в том числе и кода, пытающегося избавиться от отладчика!Process 707 stopped
* thread #1, stop reason = signal SIGSTOP
frame #0: 0x0000000108ff85b0 dyld`stat64 + 8
Target 0: (TopWidget) stopped.
(lldb)
Но если мы установим контрольную точку на
ptrace
, а затем продолжим выполнение, как это сделано в примере выше, то что-то пойдёт не так:(lldb) b ptrace
Breakpoint 1: no locations (pending).
WARNING: Unable to resolve breakpoint
to any actual locations.
(lldb) con
Process 707 resuming
1 location added to breakpoint 1
Process 707 exited with status = 45 (0x0000002d)
В конечном итоге контрольная точка ресолвится, но она не задействуется, и мы вылетаем из приложения с кодом ошибки. А спустя пару секунд телефон перезапускается!
Очевидно, мы столкнулись здесь с чем-то более серьёзным.
▍ Обход PT_DENY_ATTACH (высокий уровень сложности)
Выше я говорил, что в iOS есть два способа интеграции этой функциональности
ptrace
, и описанный выше способ обладает некоторыми недостатками.Во-первых, очень легко найти и пропустить этот вызов
ptrace
, как мы видели это в примере выше — простая установка контрольной точки на ptrace
позволяет быстро её выявить и обойти.Во-вторых, нам нужно выполнять подозрительно выглядящий поиск в среде выполнения при помощи
dlopen
и dlsym
, благодаря чему Apple гораздо проще понять, что мы вызываем приватный API.Здесь мы можем убить двух зайцев одним выстрелом, полностью пропустив этот вызов функции
ptrace
. Если мы запустим показанный выше пример приложения, и снова установим контрольную точку на функции ptrace
, то lldb
покажет нам дизассемблированный код этой функции:libsystem_kernel.dylib`__ptrace:
-> 0x1039517d0 <+0>: adrp x9, 59
0x1039517d4 <+4>: add x9, x9, #0x48 ; errno
0x1039517d8 <+8>: str wzr, [x9]
0x1039517dc <+12>: mov x16, #0x1a ; =26
0x1039517e0 <+16>: svc #0x80
0x1039517e4 <+20>: b.lo 0x103951800 ; <+48>
0x1039517e8 <+24>: stp x29, x30, [sp, #-0x10]!
0x1039517ec <+28>: mov x29, sp
0x1039517f0 <+32>: bl 0x10394acf4 ; cerror
0x1039517f4 <+36>: mov sp, x29
0x1039517f8 <+40>: ldp x29, x30, [sp], #0x10
0x1039517fc <+44>: ret
0x103951800 <+48>: ret
Как мы видим, сама реализация на удивление короткая. В начале и конце есть обработка ошибок, но самая важная часть функции — это команда
scv
посередине.Это системный вызов, то есть функция делегирует всю сложную работу ядру; это логично для столь низкоуровневой задачи, как интеграция отладки.
Некоторые разработчики вместо вызова этой функции
ptrace
просто выполняют сами тот же системный вызов, по сути, используя тот же самый ассемблерный код:void __attribute__((constructor)) prevent_debugging(void) {
asm volatile(
// передаём те же аргументы,
// которые передавали ptrace.
// PT_DENY_ATTACH = 31;
// другие аргументы не используются
"mov x0, #31 \n"
"mov x1, #0 \n"
"mov x2, #0 \n"
"mov x3, #0 \n"
// x16 содержит тип системного вызова,
// который мы хотим выполнить; в данном случае
// ptrace = 26
"mov x16, #26 \n"
// Выполняем сам системный вызов
"svc #0x80 \n"
);
}
Примечание: такой стиль кода со встроенным ассемблером имеет некоторые проблемы — если вы будете создавать что-то подобное в своём приложении, то лучше воспользоваться extended asm. Но такой стиль упрощает нам объяснения.
Не волнуйтесь, если слабо разбираетесь в ассемблере, тут всё намного проще, чем кажется.
Во-первых, у нас есть те же четыре параметра, которые мы передавали вызову функции
ptrace
. Это PT_DENY_ATTACH
(на самом деле это целочисленное значение 31), за которым следуют три нуля. Мы передаём всё это в регистрах с x0
по x3
.Далее нам нужно сообщить ядру, какой системный вызов мы хотим выполнить. iOS ожидает, что эта информация будет передана в регистре
x16
, в который мы записали значение 26
. Это значение соответствует ptrace
, что можно увидеть в дизассемблированной версии настоящей функции ptrace
— она записывает в регистр x16
значение 0x1a
(26
в десятичном виде).А затем мы выполняем сам системный вызов.
0x80
здесь тоже не используется, мы просто обязаны передать значение, а iOS применяет это значение по соглашению.Вот и весь ассемблерный код. Если мы заново запустим наш пример приложения, добавив эту функцию, то увидим то же поведение, что и раньше — приложение выбросит нас, как только будет подключён отладчик, без вызова приватной функции
ptrace
.Такой способ Apple распознать немного сложнее, а нам немного сложнее обойти.
Здесь нет стандартной функции наподобие
ptrace
, на которую мы можем установить контрольную точку. Нам придётся разбираться, где в двоичном файле происходит системный вызов, и исходя из этого решать, как мы хотим его обойти.Для этого нам нужно открыть дешифрованную копию приложения в дизассемблере и найти способ поиска этих команд.
К счастью, изучив эти команды, мы найдём хорошего кандидата для поиска:
mov x16, #26
Это конкретное значение, записываемое в конкретный регистр. Вряд ли в обычном коде такое будет встречаться очень часто, поэтому поиск такой команды — хороший способ сужения пространства поиска.
В большинстве дизассемблеров есть опция поиска по сырому дизассемблированному тексту вывода, но это может быть очень медленно, плюс вы можете споткнуться об небольшие различия в форматировании, например, в отображении чисел в шестнадцатеричном или десятичном виде.
Такой опцией можно воспользоваться при необходимости, но есть вариант и получше — искать байты, соответствующие этой команде.
Я люблю пользоваться для этого armconverter.com. Он позволяет вставить ассемблерную команду, например
mov x16, #26
и получить соответствующие байты; в данном случае:
50 03 80 D2
Затем мы можем поискать эти байты в двоичном файле, чтобы найти потенциальные места выполнения системного вызова
ptrace
. Стоит учесть, что эту команду можно записать и иначе, воспользовавшись w16
вместо x16
. На самом деле, это тот же регистр: w16
— это 32-битное представление, а x16
— 64-битное. При записи этого ассемблерного кода допустимы оба способа, поэтому мы должны искать в двоичном файле оба.Поискав байты, соответствующие
mov x16, #26
, мы ничего не нашли, но поискав байты mov w16, #26
, получили четыре результата:Адрес Функция Команда
__text:00000001013BB18C sub_1013BA900 MOV W16, #0x1A
__text:0000000102A21D80 sub_102A21868 MOV W16, #0x1A
__text:0000000102A2BB10 sub_102A2B984 MOV W16, #0x1A
__text:0000000102A2BB64 sub_102A2B984 MOV W16, #0x1A
С этим вполне можно работать. Нажав на каждый из результатов, мы не увидели в первых двух ничего особо подозрительного, то есть соседние команды не очень походили на ассемблерный код из примера выше.
; sub_1013BA900
MOV W15, #8
STURB W15, [X29,#var_F8+0xC]
MOV W16, #0x1A
STURB W16, [X29,#var_F8+0xD]
STURB W12, [X29,#var_F8+0xE]
Но взглянув на третий результат, мы нашли именно тот ассемблерный код, который и искали!
; sub_102A2B984
MOV X0, #0x1F
MOV X1, #0
MOV X2, #0
MOV X3, #0
MOV W16, #0x1A
SVC 0x80 ; addr=0x102A2BB14
Отлично, именно эта часть приложения мешает нам привязать отладчик. И оказалось, что четвёртое вхождение — это всего лишь другая ветвь той же функции ниже по коду. Вполне очевидно, что именно эту функцию мы хотим пропатчить.
Снова прикрепим к приложению отладчик точно так же, как делали это раньше, воспользовавшись командой
--waitfor
, чтобы попросить lldb
дождаться загрузки процесса. Но на этот раз мы установим контрольную точку на адресах, в которых выполняются эти системные вызовы. Согласно дизассемблеру, это адреса 0x102A2BB14
и 0x102A2BB68
.(lldb) br s -a 0x102A2BB14 -s TopWidget
(lldb) br s -a 0x102A2BB68 -s TopWidget
Мы используем
br s
(сокращение от breakpoint set
), указав при помощи -a
адрес, а при помощи -s
указываем, что адрес относителен имени двоичного файла; так как дизассемблер не знает, куда загружен двоичный файл в памяти, нам нужно, чтобы lldb
выполнил преобразование.Теперь если мы продолжим выполнение, то попадём в одну из контрольных точек!
(lldb) con
Process 865 resuming
Process 865 stopped
* thread #1, queue = 'com.apple.main-thread',
stop reason = breakpoint 1.1
frame #0: 0x000000010327bb14
TopWidget`___lldb_unnamed_symbol254690 + 400
TopWidget`___lldb_unnamed_symbol254690:
-> 0x10327bb14 <+400>: svc #0x80
0x10327bb18 <+404>: and w10, w9, #0x1
0x10327bb1c <+408>: mov w9, #0x7149
0x10327bb20 <+412>: movk w9, #0xd7ad, lsl #16
Target 0: (TopWidget) stopped.
Как и ожидалось, мы остановились на команде
svc
.Есть пара вариантов того, как предотвратить действие этого вызова, но легче всего просто полностью обойти команду. Это можно сделать, попросив
lldb
перейти к адресу следующей команды (в данном случае 0x10327bb18
):(lldb) jump *0x10327bb18
Если мы выполним
di
(сокращение от disassemble
), то увидим, что переместились к команде непосредственно после команды svc
: 0x10327bb14 <+400>: svc #0x80
-> 0x10327bb18 <+404>: and w10, w9, #0x1
0x10327bb1c <+408>: mov w9, #0x7149
0x10327bb20 <+412>: movk w9, #0xd7ad, lsl #16
И если теперь продолжим выполнение… то окажется, что отладчик привязан!
Ровно до того момента, пока мы не дойдём до второй меры защиты, вызывающей мягкий перезапуск всего телефона.
Перезапуск телефона
У нас возникло очень важное отличие от других случаев, когда мы сталкивались с мягкой перезагрузкой/respring: в этот раз
lldb
по-прежнему прикреплён:Process 865 stopped
* thread #1, queue = 'com.apple.main-thread',
stop reason = signal SIGKILL
frame #0: 0x0000000211445030
libsystem_kernel.dylib`mach_msg2_trap + 8
libsystem_kernel.dylib`mach_msg2_trap:
-> 0x211445030 <+8>: ret
libsystem_kernel.dylib`macx_swapon:
0x211445034 <+0>: mov x16, #-0x30
0x211445038 <+4>: svc #0x80
0x21144503c <+8>: ret
Target 0: (TopWidget) stopped.
(lldb)
Устранив проблему с отладчиком, мы могли глубже разобраться с мягким перезапуском в целом. Мы могли посмотреть, какой процесс отправлял сигнал на перезагрузку, и запускали
bt
, чтобы увидеть текущую трассировку стека. Мы могли видеть, что происходит, когда телефон перезапускается:(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGKILL
* frame #0: 0x0000000211445030 libsystem_kernel.dylib`mach_msg2_trap + 8
frame #4: 0x00000001d72b1690 QuartzCore`CARenderServerCaptureDisplayWithTransform_ + 628
frame #5: 0x00000001d70e5cdc QuartzCore`CARenderServerSnapshot_(unsigned int, NSDictionary*) + 1844
frame #6: 0x00000001d719d6f0 QuartzCore`CARenderServerSnapshot + 12
frame #7: 0x00000001d8881628 UIKitCore`___UISnapshotScreenWindowsRectBlock_block_invoke + 368
frame #8: 0x00000001d8030e40 UIKitCore`_performAfterCommitUnderCoverAllowDefer + 328
frame #9: 0x00000001d887fe30 UIKitCore`_UISnapshotScreenWindowsRectAfterCommit + 388
frame #10: 0x00000001d888009c UIKitCore`_UISnapshotScreenCompatibilityRectAfterCommit + 564
frame #11: 0x0000000100891898 TopWidget`___lldb_unnamed_symbol7950 + 76
frame #12: 0x00000001ddb9f490 Combine`Combine.Subscribers.Sink.receive(τ_0_0) -> Combine.Subscribers.Demand + 88
// ...
frame #28: 0x00000001054a0344 dyld`start + 1860
Он выполняет функцию, захватывающую содержимое экрана, что довольно интересно. Кроме того, мы нашли последний адрес, по которому мы проходим по коду самого приложения — во
frame #11
.Первым делом мы бы хотели посмотреть на эту функцию в дизассемблере, но проблема заключалась в том, что этот адрес относителен процессу, загруженному в память, о котором наш дизассемблер ничего не знает.
Но мы можем попросить у
lldb
предоставить больше информации об этом адресе:(lldb) image lookup -a 0x0000000100891898
Address: TopWidget[0x0000000100041898] (TopWidget.__TEXT.__text + 235672)
Summary: TopWidget`___lldb_unnamed_symbol7950 + 76
Во второй строке этого вывода содержится адрес относительно самого двоичного файла —
0x100041898
. Мы можем взять этот адрес и выполнить переход в дизассемблере, чтобы найти функцию, которую выполняли, когда произошёл вылет.В декомпилированном виде эта функция выглядит так:
void __noreturn sub_10004184C() {
void *v0; // x19
id v1; // x20
v0 = (void *)objc_opt_self(&OBJC_CLASS___UIScreen);
while ( 1 )
{
v1 = objc_retainAutoreleasedReturnValue(
objc_msgSend(v0, "mainScreen")
);
objc_release(objc_retainAutoreleasedReturnValue(
objc_msgSend(v1, "snapshotViewAfterScreenUpdates:", 1LL)
));
objc_release(v1);
}
}
В бесконечном цикле мы вызываем
+[UIScreen mainScreen]
, а по результату вызываем snapshotViewAfterScreenUpdates:
. Это публичный API, к тому же достаточно распространённый; он просто создаёт образ экрана.Но любопытна окружающая его реализация. Этот метод очень активно использует память, но мы ничего не делаем с результатом, просто создаём в бесконечном цикле образы!
Иногда ошибки возникают при декомпиляции, но если взглянуть на граф этой функции, то увидим то же самое — выполняется бесконечный цикл, и в этом цикле происходит только вызов
snapshotViewAfterScreenUpdates:
. Результат нигде не используется. Не вызываются больше никакие другие методы. Мы никак не можем выйти из цикла.Нужно подчеркнуть, насколько это безумно: разумеется, при использовании слишком большого объёма памяти приложение будет завершено, но, похоже, кто-то нашёл метод, который при слишком быстром вызове приводит к мягкому перезапуску всего телефона (на самом деле, позже я выяснил, что эту стратегию использует как минимум одна джейлбрейк-утилита, чтобы выполнять respring телефона по запросу пользователя). Затем они решили воспользоваться этой информацией и написали функцию для вызова метода в бесконечном цикле, чтобы перезапускать телефон каждого, кто запустит этот код.
Потрясающе!
В видео мы чуть глубже оттрассировали источник этого вызова и выяснили, что он запускается, когда срабатывает уведомление
com.apple.tw.twrr
, которое, похоже, связано с тем, что приложение выполняет проверку рисков на телефоне. Если мы не проходим эту проверку, выполняется respring телефона.Внутреннее устройство этой системы любопытно, но чтобы обойти эту защиту, нам не нужно слишком вдаваться в подробности — если мы снова запустим
lldb
, обойдём вызов защиты от отладки, но на этот раз установим дополнительную контрольную точку в начале функции, намеренно перезапускающей телефон, то сможем просто воспользоваться thread return
, чтобы пропустить её выполнение:(lldb) br s -a 0x102A2BB68 -s TopWidget
Breakpoint 2:
where = TopWidget`___lldb_unnamed_symbol256584 + 756,
address = 0x0000000107003b68
// Later, on breakpoint hit:
* thread #1, queue = 'com.apple.main-thread',
stop reason = breakpoint 2.1
(lldb) thread return
(lldb) con
И теперь мы можем полностью войти в приложение с прикреплённым отладчиком!

Инъецирование кода
В начале статьи мы написали ещё об одной проблеме — при попытках инъецирования нового кода в приложение оно тоже вылетает.
Я часто инъецирую код, когда подключён отладчик, и мне нужны более сложные утилиты, позволяющие глубже исследовать приложение, например, быстро создать лог информации о доступности всех кнопок на экране. Это можно сделать и при помощи одного лишь
lldb
, но процесс будет довольно мучительным: проще написать это во фреймворке, инъецировать в приложение, а затем вызывать из отладчика вспомогательную функцию.Но инъецировать код требуется и тогда, когда у вас вообще нет телефона с джейлбрейком — инъецирование инструментов наподобие Frida и Flex позволяет приступить к исследованию приложения без необходимости джейлбрейка. Обычно их инъецирование выполняется при помощи инструмента, который переподписывает приложение (мне нравится Sideloadly), но если сделать это в нашем случае, но при запуске приложения оно сразу же вылетит.
То есть это ещё одна мера защиты, проверяющая, загружены ли в среде выполнения фреймворки, и выполняющая вылет в случае нахождения чего-то неожиданного?
Некоторые приложения ведут себя так, но к счастью, не в данном случае. Существует гораздо более простое объяснение. Можно исследовать вылет, например, или при помощи
lldb
, или, в случае, если мы делаем это, потому что у нас нет джейлбрейка, получив логи вылета прямо с устройства.В данном случае вылет отображается на вершине трассировке стека как
0x1002027D4
. Перейдя по этому адресу в дизассемблере, мы увидим функцию BRK
, то есть именно то, что ожидаем встретить в ситуации намеренного вылета приложения, например, при помощи принудительной распаковки (force unwrapping) значения nil.Декомпиляция этого метода чуть длиннее, но просканировав её, мы найдём строку, выделяющуюся, как вероятная причина вылета:
v13 = objc_retainAutoreleasedReturnValue(objc_msgSend(
v8, "containerURLForSecurityApplicationGroupIdentifier:", v12)
);
containerURLForSecurityApplicationGroupIdentifier:
— это публичный, совершенно обычный метод, возвращающий папку, доступ к которой совместно могут получать различные приложения или расширения. Обычно приложения полностью отделены песочницами друг от друга, но если поместить их в общую группу, то они смогут иметь доступ к общим ресурсам.Проблема в том, что группы приложений определяются в рамках процесса подписывания кода, а мы как раз избавились от старой подписи приложения, инъецировав код и переподписав приложение. При этом мы потеряли все группы.
То есть этот метод, который должен возвращать URL, указывающий на локальную файловую систему, вероятно, возвращает
nil
, а затем приложение, скорее всего, принудительно распаковывает его, в результате вылетая.На самом деле, это достаточно распространённая проблема, и хотя здесь она действует как мера защиты, это вряд ли было сделано намеренно. Это приложение-виджет, поэтому логично, что ему потребуется находиться в группе приложений с его расширением приложения-виджета, управляющим отображением виджетов на основном экране.
Самым простым способом обойти эту проблему будет… отказ от переподписывания приложения. Телефон с джейлбрейком сильно упрощает это — можно инъецировать фреймворки в приложение при помощи утилиты джейлбрейка без необходимости переподписывать его. Или даже можно запускать код с недопустимыми подписями; можно добавлять любые приложения в любые группы. Джейлбрейк позволяет избавиться от множества проблем с подписыванием кода.
Однако при необходимости в случае инъецирования фреймворка отладки, когда нет устройства с джейлбрейком, можно иногда обойти подобные проблемы, даже переподписав приложение.
Приложение вылетает, потому что ожидает возврата URL, но не получает его. Мы можем создать маленький фреймворк, который преобразует этот метод, чтобы URL всегда возвращался:
@implementation NSFileManager (Swizzle)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector
= @selector(containerURLForSecurityApplicationGroupIdentifier:);
SEL swizzledSelector
= @selector(swizzled_containerURLForSecurityApplicationGroupIdentifier:);
Method originalMethod
= class_getInstanceMethod(class, originalSelector);
Method swizzledMethod
= class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(
originalMethod,
swizzledMethod
);
});
}
- (NSURL *)swizzled_containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier {
return [self temporaryDirectory];
}
@end
В данном случае мы делаем так, чтобы при каждом вызове метода
containerURLForSecurityApplicationGroupIdentifier:
мы вызывали наш подменный метод, просто возвращающий какую-то временную папку.Стоит отметить, что это не эквивалентная замена тому, что пытается сделать приложение — этот метод вызывается, потому что приложению нужна общая папка, к которой имеют доступ она и её расширения, но мы обманываем её, говоря «Вот твоя общая папка», хотя это точно не она.
Но в некоторых случаях это может быть вполне приемлемо. Я выяснил, что подобные небольшие патчи часто ломают сами расширения приложений, но меня они обычно не волнуют — мне важно, чтобы нормально работало основное приложение, особенно если моя цель заключается в исследовании того, как приложение выполняет определённую задачу, и мне достаточно работы базовой функциональности.
Возможно, в этом случае можно сделать что-то более продвинутое, например, создать собственную группу приложений, переподписать основное приложение и все использующие его расширения, а затем подменить этот метод (а также остальные, с которыми вы столкнётесь), чтобы он использовал новый идентификатор группы. Но для этого понадобится гораздо больше усилий, и труд вряд ли того стоит — зачастую проще вместо этого найти устройство с джейлбрейком.
Как бы то ни было, если мы соберём этот фреймворк и инъецируем его в приложение со всем остальным, что нам нужно (например, с
Flex
), то приложение начнёт замечательно запускаться на обычном устройстве! На устройстве с джейлбрейком нам всё равно понадобится прикрепить отладчик и обходить описанные выше защиты, но разобравшись с ними, мы вернёмся к приложению, в которое уже будет успешно инъецирован Flex
, что позволит нам использовать кучу других полезных инструментов отладки:
Telegram-канал со скидками, розыгрышами призов и новостями IT ?

senchik
Спасибо, было очень интересно!