
фреймворк для перехвата функций ядра iOS на устройствах, поддерживающих checkra1n

Вывод из журнала ядра после компиляции и запуска example/open1_hook.c
xnuspy — это модуль pongoOS, который устанавливает новый системный вызов xnuspy_ctl, позволяющий перехватывать функции ядра из пользовательского пространства. Поддерживаются iOS 13.x, iOS 14.x и iOS 15.x на checkra1n 0.12.2 и выше. Устройства с 4K не поддерживаются.
Этот модуль полностью нейтрализует KTRR/KPP и позволяет создавать RWX-память внутри EL1. Не используйте это на вашем основном устройстве.
Требуется libusb: brew install libusb
Выполните make в корневом каталоге. Будут собраны загрузчик и модуль.
Добавьте их перед make.
XNUSPY_DEBUG=1
kprintf).XNUSPY_SERIAL=1
IOLog.XNUSPY_LEAKED_PAGE_LIMIT=n
64. Дополнительную информацию можно найти в разделе Отладка паник ядра.XNUSPY_TRAMP_PAGES=n
XNUSPY_DEBUG и XNUSPY_SERIAL не зависят друг от друга.
После сборки всего, заставьте checkra1n загрузить ваше устройство в оболочку pongo: /Applications/checkra1n.app/Contents/MacOS/checkra1n -p
В том же каталоге, где вы собрали загрузчик и модуль, выполните loader/loader module/xnuspy. После этого xnuspy выполнит свою работу, и через несколько секунд ваше устройство загрузится. loader подождет еще несколько секунд после выдачи xnuspy-getkernelv на случай, если необходимо эксплуатировать SEPROM.
Иногда несколько моих телефонов зависали на этапе "Загрузка" после выполнения KPF от checkra1n. Я еще не выяснил причину, но если это произойдет, попробуйте снова. Также, если устройство зависает после bootx, попробуйте снова. Наконец, пометка скомпилированного кода xnuspy_ctl как исполняемого на моем iPhone X под iOS 13.3.1 работает с перебоями, но на других телефонах удается в 100% случаев. Если при выполнении вашей программы перехвата возникает паника с ошибкой выборки инструкции ядра, попробуйте снова.
xnuspy изменяет системный вызов enosys, чтобы он указывал на xnuspy_ctl_tramp. Это небольшой трамплин, который помечает скомпилированный код xnuspy_ctl как исполняемый и переходит к нему. Реализацию xnuspy_ctl можно найти в module/el1/xnuspy_ctl/xnuspy_ctl.c, а примеры — в каталоге example.
Внутри include/xnuspy/ находится xnuspy_ctl.h — заголовочный файл, определяющий константы для xnuspy_ctl. Он предназначен для включения во все программы, которые перехватывают функции ядра.
Вы можете использовать sysctlbyname, чтобы узнать, какой системный вызов был изменен:```
size_t oldlen = sizeof(long);
long SYS_xnuspy_ctl = 0;
sysctlbyname("kern.xnuspy_ctl_callnum", &SYS_xnuspy_ctl, &oldlen, NULL, 0);
Этот системный вызов принимает четыре аргумента: `flavor`, `arg1`, `arg2` и `arg3`.
Тип операции может быть `XNUSPY_CHECK_IF_PATCHED`, `XNUSPY_INSTALL_HOOK`,
`XNUSPY_REGISTER_DEATH_CALLBACK`, `XNUSPY_CALL_HOOKME`, `XNUSPY_CACHE_READ`,
`XNUSPY_KREAD`, `XNUSPY_KWRITE` или `XNUSPY_GET_CURRENT_THREAD`.
Значение следующих трёх аргументов зависит от типа операции.
## `XNUSPY_CHECK_IF_PATCHED`
Этот тип существует, чтобы вы могли проверить, присутствует ли `xnuspy_ctl`. Вызов с этим
типом заставит его вернуть `999`. Значения остальных аргументов
игнорируются.
## `XNUSPY_INSTALL_HOOK`
Я спроектировал этот тип, чтобы он соответствовал API [`MSHookFunction`](http://www.cydiasubstrate.com/api/c/MSHookFunction/).
`arg1` — это *НЕСМЕЩЁННЫЙ* адрес функции ядра, которую вы хотите перехватить. Если вы
укажете смещённый адрес, скорее всего произойдёт паника. `arg2` — это указатель на вашу
ABI-совместимую функцию-замену. `arg3` — это указатель для `xnuspy_ctl`,
чтобы `copyout` (скопировать наружу) адрес трамплина, представляющего исходную функцию
ядра. Он может быть `NULL`, если вы не собираетесь вызывать исходную.
## `XNUSPY_REGISTER_DEATH_CALLBACK`
Этот тип позволяет зарегистрировать опциональный «callback смерти» — функцию, которую xnuspy
вызовет при завершении вашей программы-перехватчика. Он даёт вам возможность очистить всё,
что вы создали в своих перехватчиках ядра. Если вы создавали какие-либо потоки ядра,
вы должны сообщить им о завершении в этой функции.
Ваш callback не вызывается асинхронно, поэтому если вы заблокируете выполнение, вы помешаете
потоку сборки мусора xnuspy выполниться.
`arg1` — это указатель на вашу callback-функцию. Значения остальных аргументов
игнорируются.
## `XNUSPY_CALL_HOOKME`
`hookme` — это небольшая заглушка на ассемблере, которую xnuspy экспортирует через кеш xnuspy
для вашего перехвата. Вызов `xnuspy_ctl` с этим типом приведёт к вызову `hookme`,
предоставляя вам простой способ получить выполнение кода ядра без необходимости
перехватывать реальную функцию ядра.
`arg1` — это аргумент, который будет передан `hookme` при его вызове.
Он может быть `NULL`.
## `XNUSPY_CACHE_READ`
Этот тип даёт вам возможность читать из кеша xnuspy. Он содержит много полезных
вещей, таких как `kprintf`, `current_proc`, `kernel_thread_start`, некоторые функции libc,
и сдвиг ядра, чтобы вам не пришлось искать их самостоятельно. Полный список
идентификаторов кеша смотрите в `example/xnuspy_ctl.h`.
`arg1` — это один из идентификаторов кеша, определённых в `xnuspy_ctl.h`, а `arg2` — это
указатель для `xnuspy_ctl`, чтобы `copyout` (скопировать наружу) адрес или значение того, что вы запросили.
Значения остальных аргументов игнорируются.
## `XNUSPY_KREAD`
Этот тип даёт вам простой способ читать память ядра из пользовательского пространства без
tfp0.
`arg1` — это виртуальный адрес ядра, `arg2` — адрес буфера пользовательского пространства,
а `arg3` — размер этого буфера пользовательского пространства. `arg3` байтов будет записано
из `arg1` в `arg2`.
## `XNUSPY_KWRITE`
Этот тип даёт вам простой способ записывать в память ядра из пользовательского пространства без
tfp0.
`arg1` — это виртуальный адрес ядра, `arg2` — адрес буфера пользовательского пространства,
а `arg3` — размер этого буфера пользовательского пространства. `arg3` байтов будет записано
из `arg2` в `arg1`.
## `XNUSPY_GET_CURRENT_THREAD`
Этот тип предоставляет пользовательскому пространству адрес ядра вызывающего потока.
`arg1` — это указатель для `xnuspy_ctl`, чтобы `copyout` (скопировать наружу) возвращаемое значение
`current_thread`. Значения остальных аргументов игнорируются.
### Ошибки
Для всех типов, кроме `XNUSPY_CHECK_IF_PATCHED`, в случае успеха возвращается `0`.
При ошибке возвращается `-1` и устанавливается `errno`. `XNUSPY_CHECK_IF_PATCHED`
не возвращает никаких ошибок. Для преобразования `kern_return_t` в соответствующий `errno`
используется `mach_to_bsd_errno` из XNU.
#### Ошибки, относящиеся к `XNUSPY_INSTALL_HOOK`
`errno` устанавливается в...
- `EEXIST`, если:
- Перехват уже существует для несмещённой функции ядра, обозначенной `arg1`.
- `ENOMEM`, если:
- `unified_kalloc` вернул `NULL`.
- `ENOSPC`, если:
- Нет свободных структур `xnuspy_tramp` — внутренней структуры данных
xnuspy. Этого не должно произойти, если только вы не перехватываете сотни функций ядра
*одновременно*. Если вам нужно больше перехватов функций, смотрите [Лимиты](#limits).
- `ENOTSUP`, если:
- Вызывающий не является исполняемым файлом Mach-O или динамической библиотекой.
- `ENOENT`, если:
- `mh_for_addr` не смог определить заголовок Mach-O, соответствующий
`arg2` в адресном пространстве вызывающего.
- `EFAULT`, если:
- Определённый заголовок Mach-O на самом деле не является заголовком Mach-O. Это, вероятно,
никогда не произойдёт.
- `EIO`, если:
- `mach_make_memory_entry_64` не вернул запись памяти для всего
определённого заголовка Mach-O сегментов `__TEXT` и `__DATA`.
`errno` также зависит от возвращаемого значения `vm_map_wire_external`,
`mach_vm_map_external`, `mach_make_memory_entry_64`, `copyin`, `copyout` и,
если применимо, функции однократной инициализации.
Если этот тип возвращает ошибку, целевая функция ядра не была перехвачена.
Если вы передали ненулевой указатель для `arg3`, он мог быть или не быть
инициализирован. Использовать его небезопасно, если он был инициализирован.
#### Ошибки, относящиеся к `XNUSPY_REGISTER_DEATH_CALLBACK`
`errno` устанавливается в...
- `ENOENT`, если:
- Вызывающий процесс не перехватил ни одной функции ядра.
Если этот тип возвращает ошибку, ваш callback смерти не был зарегистрирован.
#### Ошибки, относящиеся к `XNUSPY_CALL_HOOKME`
`errno` устанавливается в...
- `ENOTSUP`, если:
- `hookme` находится слишком далеко от памяти, содержащей структуры `xnuspy_tramp`.
Это определяется внутри pongoOS и может произойти только в том случае, если
xnuspy пришлось вернуться к неиспользуемому коду, уже находящемуся внутри кеша ядра.
В этом случае вызов `hookme` почти наверняка вызовет панику ядра,
и вам придётся придумать другую функцию ядра для перехвата.
Если этот тип возвращает ошибку, `hookme` не был вызван.
#### Ошибки, относящиеся к `XNUSPY_CACHE_READ`
`errno` устанавливается в...
- `EINVAL`, если:
- Константа, обозначенная `arg1`, не представляет ничего в кеше.
- `arg1` был `IO_LOCK`, но ядро iOS 14.4.2 или ниже либо iOS 15.x.
- `arg1` был `IPC_OBJECT_LOCK`, но ядро iOS 15.x.
- `arg1` был `IPC_PORT_RELEASE_SEND`, но ядро iOS 14.5 или выше.
- `arg1` был `IPC_PORT_RELEASE_SEND_AND_UNLOCK`, но ядро iOS 14.4.2 или ниже.
- `arg1` был `KALLOC_CANBLOCK`, но ядро iOS 14.x или выше.
- `arg1` был `KALLOC_EXTERNAL`, но ядро iOS 13.x.
- `arg1` был `KFREE_ADDR`, но ядро iOS 14.x или выше.
- `arg1` был `KFREE_EXT`, но ядро iOS 13.x.
- `arg1` был `PROC_REF`, но ядро iOS 14.8 или ниже.
- `arg1` был `PROC_REF_LOCKED`, но ядро iOS 15.x.
- `arg1` был `PROC_RELE`, но ядро iOS 14.8 или ниже.
- `arg1` был `PROC_RELE_LOCKED`, но ядро iOS 15.x.
- `arg1` был `VM_MAP_UNWIRE`, но ядро iOS 15.x.
- `arg1` был `VM_MAP_UNWIRE_NESTED`, но ядро iOS 14.8 или ниже.
`errno` также зависит от возвращаемого значения `copyout` и, если применимо,
возвращаемого значения функции однократной инициализации.
Если этот тип возвращает ошибку, указатель, который вы передали для `arg2`, не был
инициализирован.
#### Ошибки, относящиеся к `XNUSPY_KREAD` и `XNUSPY_KWRITE`
`errno` устанавливается в...
- `EFAULT`, если:
- Не удалось преобразовать адрес для `arg1` или `arg2`. Если вы скомпилировали с
`XNUSPY_DEBUG=1`, сообщение об этом выводится в журнал ядра.
Если этот тип возвращает ошибку, память ядра не была прочитана/записана.
#### Ошибки, относящиеся к `XNUSPY_GET_CURRENT_THREAD`
Если `copyout` завершается ошибкой, `errno` устанавливается в его возвращаемое значение.
# Важная информация
### Частые ошибки
При написании функций-замен легко забыть, что я пишу код ядра.
Вот несколько вещей, которые следует иметь в виду при написании перехватчиков:
- *Вы не можете выполнять любой код пользовательского пространства, находящийся за пределами
`__TEXT` сегмента вашей программы*. Вы получите панику, если, например, случайно вызовете `printf`
вместо `kprintf`. Вам нужно перереализовать любую функцию libc, которую вы хотите вызвать,
если эта функция ещё не доступна через `XNUSPY_CACHE_READ`.
Однако вы можете создавать указатели на функции для других функций ядра и вызывать их.
- *Многие макросы, обычно используемые в коде пользовательского пространства, небезопасны для ядра.*
Например, `PAGE_SIZE` раскрывается в `vm_page_size`, а не в константу. Вам нужно
отключить PAN (на A10+, что я тоже не рекомендую делать) перед чтением этой
переменной, иначе вы получите панику.
- *Убедитесь, что компилируете свой код с флагами `-fno-stack-protector` и `-D_FORTIFY_SOURCE=0`* В некоторых случаях
устройству придётся прочитать `___stack_chk_guard` путём разыменования другого указателя пользовательского
пространства, что вызовет панику на A10+.
- *Для безопасности не компилируйте свои программы-перехватчики с оптимизациями компилятора.*
Также рекомендуется бегло просмотреть https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/style/style.html .
### Отладка паник ядра
Ошибки неизбежны при написании кода, поэтому рано или поздно вы вызовете панику ядра.
Паника не обязательно означает ошибку в xnuspy, поэтому
прежде чем открывать issue, пожалуйста, убедитесь, что паника всё ещё происходит, когда вы
делаете не более чем вызов исходной функции и возвращаете её значение (если необходимо). Если
паника всё ещё происходит, то это, вероятно, ошибка xnuspy (и, пожалуйста, откройте issue),
но если нет, то что-то не так с вашей заменой.
Поскольку xnuspy на самом деле не перенаправляет выполнение на страницы EL0, отладка
паники не столь прямолинейна. Откройте `module/el1/xnuspy_ctl/xnuspy_ctl.c`,
и прямо перед единственным вызовом `kwrite_instr` в `xnuspy_install_hook`
добавьте вызов `IOSleep` на несколько секунд. Это делается, чтобы обеспечить
достаточно времени до паники устройства для распространения журналов. Перекомпилируйте xnuspy с
`XNUSPY_DEBUG=1 make -B` и загрузите модуль снова. После загрузки модуля,
если вы ещё этого не сделали, скомпилируйте `klog` из `klog/`. Загрузите его на устройство
и выполните `stdbuf -o0 ./klog | grep shared_mapping_kva`. Запустите свою программу-перехватчик снова
и следите за строкой из `klog`, которая выглядит так:
`shared_mapping_kva: dist 0x7af4 uaddr 0x104797af4 umh 0x104790000 kmh 0xfffffff00c90c000`
Если вы устанавливаете более одного перехватчика, будет больше одного вхождения.
В этом случае `dist` и `uaddr` будут различаться, но `umh` и `kmh` — нет. `kmh`
указывает на начало отображения ядром `__TEXT` сегмента вашей программы.
Загрузите свою программу-перехватчик в любимый дизассемблер и перебазируйте её так, чтобы её заголовок Mach-O
находился по адресу `kmh`. Для IDA Pro это `Edit -> Segments -> Rebase
program...` с выбранным `Image base`. После того как устройство снова запаникует и перезагрузится,
если в журнале паники будут адреса, соответствующие отображению ядром вашей замены,
они совпадут с дизассемблированным кодом. Если их нет, то, вероятно,
у вас какое-то тонкое повреждение памяти внутри вашей замены.
xnuspy также не может знать, выполняется ли (или будет выполняться) поток ядра
на отображении ядром `__TEXT` сегмента вашей программы после того, как ваши
перехватчики будут удалены. Одна из вещей, которую делает xnuspy для решения этой проблемы, — это не
освобождать это отображение сразу после завершения вашей программы-перехватчика. Вместо этого оно
добавляется в конец очереди. Как только поток сборки мусора xnuspy заметит,
что заданный лимит превышен относительно количества страниц отображений, хранящихся
в этой очереди, он начнёт освобождать с начала очереди и будет
продолжать, пока лимит не перестанет быть превышен. По умолчанию этот лимит составляет 1 МБ,
или 64 страницы.
Хотя это очень помогает, чем больше становятся сегменты `__TEXT` и `__DATA`
вашей программы-перехватчика, тем меньше вероятность, что xnuspy выиграет эту гонку. Если вы
регулярно получаете панику и ваша программа-перехватчик довольно большая, попробуйте увеличить
этот лимит, добавив `XNUSPY_LEAKED_PAGE_LIMIT=n` перед `make`. Это установит
лимит в `n` страниц вместо 64.
### Лимиты
xnuspy резервирует одну страницу статической памяти ядра перед загрузкой XNU для своих структур `xnuspy_tramp`,
что позволяет одновременно перехватывать около 225 функций ядра. Если вам нужно
больше, вы можете добавить `XNUSPY_TRAMP_PAGES=n` перед `make`. Это укажет xnuspy
зарезервировать `n` страниц статической памяти для структур `xnuspy_tramp`. Однако, если
xnuspy приходится возвращаться к неиспользуемому коду, уже находящемуся внутри кеша ядра, это игнорируется.
Когда это происходит, подробно описано в [Как это работает](#how-it-works).
### Логирование
По какой-то причине логи из `os_log_with_args` не отображаются в потоке,
выводимом утилитой командной строки `oslog`. Логи из `kprintf` также туда
не попадают, но их *можно* увидеть с помощью `dmesg`. Однако `dmesg`
не является потоком в реальном времени, поэтому я написал `klog` — инструмент, который показывает логи `kprintf`
в реальном времени. Найдите его в `klog/`. Я настоятельно рекомендую использовать его вместо
постоянного вызова `dmesg` для ваших сообщений `kprintf`.
Если после запуска `klog` вы получаете `open: Resource busy`, выполните эту команду
`launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist`
и попробуйте снова.
К сожалению, вы не сможете увидеть никакие `NSLog`, если
в bootargs XNU установлен `atm_diagnostic_config=0x20000000`. `klog` зависит
от наличия этого boot-аргумента. Если вы хотите вернуть `NSLog`, удалите этот
boot-аргумент из `pongo_send_command` внутри `loader.c`.
### Удаление перехватчиков
xnuspy управляет этим за вас. Как только процесс завершается, все перехватчики ядра,
установленные этим процессом, удаляются в течение примерно секунды.
### Перехватываемые функции ядра
Большинство фреймворков перехвата функций имеют некоторую минимальную длину, при которой данная
функция может быть перехвачена. У xnuspy есть этот лимит *только* если вы планируете вызывать исходную
функцию *и* первая инструкция перехватываемой функции не является `B`. В этом
случае минимальная длина составляет восемь байт. В противном случае минимальной длины нет.
xnuspy использует `X16` и `X17` для своих трамплинов, поэтому функции ядра, которые
ожидают, что эти регистры сохраняются при вызовах функций, не могут быть перехвачены (таких
немного). Если функция, которую вы хотите перехватить, начинается с `BL`,
и вы намереваетесь вызывать исходную, вы можете сделать это только в том случае, если выполнение
исходной функции не изменяет `X17`.
### Потокобезопасность
`xnuspy_ctl` выполнит однократную инициализацию при первом вызове
после свежей загрузки. Это единственная часть xnuspy, которая может быть подвержена гонкам, поскольку
я не могу статически инициализировать блокировку чтения/записи, которую использую. После того как первый вызов
возвращается, любые последующие вызовы гарантированно потокобезопасны.
# Как это работает
Это упрощённое описание, но оно хорошо передаёт основную идею. Перехват функции в xnuspy
представляет собой структуру, расположенную в доступной для записи и исполняемой памяти ядра. В большинстве случаев
это память, возвращаемая `alloc_static` внутри pongoOS. Всё можно свести
к следующему:```
struct {
uint64_t replacement;
uint32_t tramp[2];
uint32_t orig[10];
};
Где replacement — это адрес виртуальной памяти ядра (подробнее о нём позже) функции замены, tramp — небольшой трамплин, перенаправляющий выполнение на replacement, а orig — более крупный и сложный трамплин, представляющий исходную функцию.
Одним из первых действий xnuspy является определение местоположения замены EL0 в адресном пространстве вызывающего процесса. Это делается для того, чтобы можно было перехватывать функции ядра из динамических библиотек. Заголовок Mach-O, соответствующий адресу этой замены, сохраняется.
После этого создаётся общее отображение (shared mapping) сегментов __TEXT и __DATA этого заголовка (а также всех сегментов между ними, если таковые имеются) между пользовательским и ядерным пространствами. __TEXT предоставляется совместно, чтобы можно было вызывать другие функции из ваших хуков. __DATA предоставляется совместно, чтобы изменения глобальных переменных были видны как EL1, так и EL0.
Поскольку это отображение является точной копией __TEXT и __DATA, легко вычислить адрес пользовательской функции замены на нём. Учитывая адрес заголовка Mach-O вызывающего процесса u, адрес начала общего отображения k и адрес пользовательской функции замены r, мы применяем следующую формулу: replacement = k + (r - u)
После этого replacement становится виртуальным адресом ядра пользовательской функции замены на общем отображении и записывается в структуру хука функции. xnuspy не перенаправляет выполнение на адрес EL0 функции замены, потому что это крайне небезопасно: это не только ставит нас в зависимость от планировщика, но и лишает контроля над сценарием, когда процесс с хуком ядра умирает, в то время как поток ядра всё ещё выполняет код замены.
Наконец, общее отображение помечается как исполняемое, и собирается безусловная непосредственная инструкция ветвления (B). Она направляет выполнение на начало tramp и заменяет первую инструкцию теперь уже перехваченной функции ядра. К сожалению, это ограничивает нас возможностью ветвления к структурам хуков, находящимся более чем на 128 МБ от данной функции ядра. xnuspy проверяет этот сценарий перед загрузкой и, если обнаруживает, что это может произойти, вместо этого использует неиспользуемый код, уже присутствующий в кэше ядра, для размещения структур хуков.
Я делаю всё возможное, чтобы патчфайндеры работали, поэтому, если что-то не работает, пожалуйста, откройте issue.