
empty_list - Exploit für p0 issue 1564 (CVE-2018-4243) iOS 11.0 - 11.3.1 kernel r/w
empty_list - Exploit für p0 Issue 1564 (CVE-2018-4243) iOS 11.0 - 11.3.1 Kernel r/w @i41nbeer
BUG: getvolattrlist nimmt über den Syscall fgetattrlist ein benutzerkontrolliertes bufferSize-Argument entgegen.
Beim Allokieren eines Kernel-Puffers, um die Attributliste hinein zu serialisieren, gibt es den folgenden Kommentar:
/*
Das Problem ist, dass der Code den Fall nicht korrekt behandelt, wenn die vom Benutzer gelieferte Puffergröße kleiner als die angeforderte Headergröße ist. Wenn wir ATTR_CMN_RETURNED_ATTRS übergeben, erreichen wir den folgenden Code:
/* Return attribute set output if requested. / if (return_valid) { ab.actual.commonattr |= ATTR_CMN_RETURNED_ATTRS; if (pack_invalid) { / Only report the attributes that are valid */ ab.actual.commonattr &= ab.valid.commonattr; ab.actual.volattr &= ab.valid.volattr; } bcopy(&ab.actual, ab.base + sizeof(uint32_t), sizeof (ab.actual)); }
Es gibt keine Prüfung, ob der allokierte Puffer groß genug ist, um zumindest das aufzunehmen.
Ausnutzung: Ich hoffe, irgendwann einen ausführlicheren Write-up dazu zu veröffentlichen; dies sind einige grobe Notizen zur Funktionsweise des Exploits:
Der Bug ermöglicht es, 8 Nullbytes über das Ende einer kalloc.16-Allokation hinaus zu schreiben. Auch wenn es so aussieht, als könnte man ein paar Bits in diesen Bytes kontrollieren, bin ich nicht sicher, ob das tatsächlich geht; deshalb habe ich mich darauf konzentriert, den Bug so auszunutzen, als würde ein NULL-Pointer über das Ende geschrieben.
Das ist ein ziemlich eingeschränktes Primitive, daher besteht der erste Schritt darin, die möglichen Optionen aufzulisten:
Letztendlich habe ich die erste Option gewählt. Dafür gibt es zwei weitere Anforderungen:
Ich habe mich für die Struktur ipc_port entschieden, die als zweites DWORD ein Referenzzählungsfeld hat und damit die erste Anforderung erfüllt. Sie wird jedoch nicht in kalloc.16 allokiert; stattdessen lebt sie in einer eigenen Zone (ipc_ports).
Das bedeutet, wir müssen einen kalloc.16-Zonenblock direkt vor einem ipc_ports-Block ausrichten und dann aus der letzten kalloc.16-Allokation im kalloc.16-Block in die erste in ipc_ports überlaufen.
Es gibt zwei Tricks, die dies erleichtern:
Freelist-Umkehrung: Zone-Allokationen erfolgen zuerst aus intermediären (teilweise vollen) Seiten. Das bedeutet, dass Objekte der Größe k.16, wenn wir einfach mitten im Grooming anfangen, sie freizugeben und zu allokieren, erst dann wiederverwendet werden, wenn die aktuelle intermediäre Seite entweder voll oder leer ist.
Das stellt eine Herausforderung dar, weil die Freelists frischer Seiten halbzufällig gefüllt sind, sodass ihre Allokationen von innen nach außen erfolgen:
| 9 8 6 5 2 1 3 4 7 10 | <-- example "randomized" allocation order from a fresh all-free page
Das bedeutet, dass unsere finalen intermediären k.16- und Ports-Seiten in etwa so aussehen:
| - - - 5 2 1 3 4 - - | - - - 4 1 2 3 5 - - | kalloc.16 ipc_ports
Wenn wir den Overflow nutzen, um einen Freelist-Eintrag zu korrumpieren, führt das zu einem Panic, falls er allokiert wird; das müssen wir also vermeiden.
Der Trick besteht darin, dass wir durch Kontrolle der Allokations- und Freigabereihenfolge die Freelists umkehren können, sodass die finalen intermediären Seiten eher so aussehen:
| 1 4 - - - - - 5 3 2 | 2 5 - - - - - 4 3 1 | kalloc.16 ipc_ports
An diesem Punkt ist es viel wahrscheinlicher, dass wir ein kalloc.16 freigeben und für den Overflow neu allokieren können, sodass wir das erste QWORD eines ipc_port treffen.
Sicher überlaufbare Allokationen: Da es wahrscheinlich viele Kandidaten-Allokationen gibt, aus denen wir überlaufen müssen, bevor wir die Ziel-Allokation erreichen (die ganz am Ende, direkt vor dem ipc_port liegt), müssen wir sicherstellen, dass die allokierten Objekte auf der kalloc.16-Seite gefahrlos mit einem NULL-Pointer korrumpiert werden können.
Ich verwende dafür OOL_Port-Deskriptoren von Mach-Nachrichten, da NULL dort ein gültiger Wert ist.
Exploit-Ablauf: Wir führen das Grooming durch, um die kalloc.16-Freelists umzukehren, und beginnen, in ein ipc_port zu überlaufen.
Wir kennen den ungefähren Bereich von Mach-Portnamen, die den zu korrumpierenden Port enthalten; nach jedem Overflow-Versuch prüfen wir jeden dieser Ports, um festzustellen, ob der Port korrumpiert wurde. Ein Nebeneffekt einer erfolgreichen Korrumpierung ist, dass das io_active-Flag des Ports auf null gesetzt wird. Das können wir ohne Nebenwirkungen mit der MIG-Methode mach_port_kobject erkennen.
Sobald wir den korrumpierten Port gefunden haben, müssen wir dafür sorgen, dass eine Referenz darauf genommen und wieder verworfen wird; und wichtiger noch, der Codepfad, der das tut, darf das io_active-Flag nicht prüfen. mach_port_set_attributes erledigt das für uns.
Damit haben wir unser NULL-Pointer-Schreiben über das Ende einer kalloc.16-Allokation in einen verwaisten Mach-Port verwandelt :)
Wir lösen einen Zone-GC aus, um zu erreichen, dass der Speicher des Ports als kalloc.4096-Seite wiederverwendet wird. Zuerst lassen wir ihn als ool_ports-Deskriptor wiederverwenden, wobei das Feld ip_context mit einem Send-Recht überlappt, das wir uns selbst an einen Canary-Port schicken. Dadurch erfahren wir die ungefähre Adresse unserer Objekte im Kernel. Danach ersetzen wir den ool_desc durch einen Pipe-Puffer und können mit etwas Herumprobieren herausfinden, wo sich der verwaiste Mach-Port im Speicher befindet.
Wir bauen dort einen gefälschten Kernel-Task-Port und räumen dann auf.
Zuverlässigkeit: Der Exploit funktioniert tatsächlich, das war mein Ziel :) Die Zuverlässigkeit liegt bei vielleicht etwa 30 %; es hängt alles davon ab, wie schnell man den anfänglichen Overflow und die Testschleife durchführen kann. Wenn etwas anderes dazwischenkommt und in kalloc.16 allokiert oder freigibt, erhöht sich die Wahrscheinlichkeit, dass man einen Freelist-Eintrag oder etwas anderes korrumpiert und ein Panic auftritt.
Ich bin sicher, dass der Exploit zuverlässiger gemacht werden kann; ich habe ihn nur so weit gebracht, dass ich demonstriert habe, dass dieser Bug ausnutzbar ist. Wenn du das als Ausgangspunkt nehmen und zeigen möchtest, wie man die Zuverlässigkeit verbessert, würde ich mich über einen Blogpost freuen! Ich stelle mir vor, dass das die Überwachung der kalloc.16-Allokationen sowie das Verständnis der Fehlerfälle und ihrer Vermeidung beinhalten würde.
Die Erfolgsraten scheinen am höchsten zu sein, wenn das Gerät neu gestartet und eine Weile im Leerlauf gelassen wurde.
Bereinigung: Wenn der Exploit funktioniert, sollte er sich selbst aufräumen und keinen Panic auf dem Gerät auslösen. Der gefälschte Kernel-Task-Port bleibt bestehen.
Verwende die Funktionen in kmem.h, um Kernel-Speicher zu lesen und zu schreiben. Wenn du nach dem Beenden dieses Prozesses weiterhin Zugriff auf den Kernel-Speicher behalten möchtest, persistiere dort ein Send-Recht auf tfp0.
Getestet auf: iPod Touch 6G, iPhone 6S, iPhone SE, iPhone 7, iPhone 8 Es sollte auf iOS 11 bis iOS 11.3.1 funktionieren.