Недавно я столкнулся с приложением, которое:

  1. Блокирует прикрепление к нему отладчиков.
  2. Выполняет преждевременный выход при попытках инъецирования кода.
  3. Приводит к вылету телефона целиком, если запустить её со включённым джейлбрейком (!).

По последнему пункту: кто вообще так делает???

Всё, что мы делаем (например, выполняем моддинг 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 ?

Комментарии (1)


  1. senchik
    20.02.2025 08:08

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