Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
Pixel_GPU_Exploit — Android 14 Kernel-Exploit für Pixel7/8 Pro | Kitploit
Tools/GitHubGitHub/0x36/pixel_gpu_exploit
Android-SicherheitPrivilege EscalationSpeicherforensikSchwachstellenanalyseExploitationLernen & BildungBinary-Exploitation
GitHub0x36/pixel_gpu_exploit

Pixel_GPU_Exploit

Android 14 Kernel-Exploit für Pixel7/8 Pro

Repository anzeigen
55788vor 2 JahrenVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Mali GPU Kernel LPE

Dieser Artikel bietet eine detaillierte Analyse von zwei Kernel-Schwachstellen im Mali GPU, die aus der standardmäßigen Anwendungs-Sandbox erreichbar sind, welche ich unabhängig identifiziert und Google gemeldet habe. Er enthält einen Kernel-Exploit, der beliebige Kernel-Lese-/Schreibfunktionen erreicht. Dadurch wird SELinux deaktiviert und root-Berechtigungen auf Google Pixel 7 und 8 Pro Modellen mit den folgenden Android 14 Versionen erlangt:

  • Pixel 8 Pro: google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keys
  • Pixel 7 Pro: google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keys
  • Pixel 7: google/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (von m4b4 (Marcel))

Schwachstellen

Dieser Exploit nutzt zwei Schwachstellen aus: einen Integer-Überlauf, der aus einem unvollständigen Patch im gpu_pixel_handle_buffer_liveness_update_ioctl ioctl-Befehl resultiert, und einen Informationsleck innerhalb der Timeline-Stream-Message-Puffer.

Buffer Underflow in gpu_pixel_handle_buffer_liveness_update_ioctl() aufgrund eines fehlerhaften Integer-Überlauf-Fixes

Google behob einen Integer-Überlauf im gpu_pixel_handle_buffer_liveness_update_ioctl ioctl-Befehl in diesem Commit. Anfangs, als ich dieses Problem meldete, nahm ich an, dass der Fehler durch eine Problem im oben beschriebenen Patch verursacht wurde. Nach Durchsicht des Berichts erkannte ich, dass meine Analyse der Schwachstelle ungenau war. Trotz meiner ersten Annahme, dass der Patch unvollständig sei, beseitigt und verhindert er effektiv einen Underflow in der Berechnung. Dies ließ mich vermuten, dass die Änderung nicht in den Produktionsversionen angewendet wurde. Obwohl ich jedoch einen Underflow in der Berechnung verursachen kann, ist es nicht möglich, einen Überlauf zu verursachen. Dies deutet darauf hin, dass der ioctl-Befehl teilweise gefixt wurde, jedoch nicht mit dem oben gezeigten Patch. Ein Blick in IDA ergab, dass ein weiterer unvollständiger Patch in den Produktionsversionen ausgeliefert wurde, und dieser Patch ist in keinem Git-Zweig des Mali GPU-Kernelmoduls vorhanden.

Diese Schwachstelle wurde erstmals in der neuesten Android-Version entdeckt und am 19. November 2023 gemeldet. Google teilte mir später mit, dass sie sie bereits intern identifiziert und ihr CVE-2023-48409 im Android-Sicherheitsbulletin vom Dezember zugewiesen hatten, und stufte sie als Duplikat ein. Obwohl ich bestätigen konnte, dass der Fehler Monate vor meiner Meldung intern identifiziert worden war (basierend auf dem Commit-Datum um den 30. August), bleibt Verwirrung. Insbesondere ist es seltsam, dass die Security Patch Levels (SPL) für Oktober und November der neuesten Geräte noch von dieser Schwachstelle betroffen waren – ich habe keine Versionen vor diesen untersucht. Daher kann ich nicht abschließend feststellen, ob es sich tatsächlich um ein Duplikat handelte und ob der entsprechende Patch vor meiner Einreichung bereits für Dezember eingeplant war oder ob es ein Versehen bei der Behebung dieser Schwachstelle gab.

Jedenfalls macht Folgendes diesen Bug mächtig:

  • Der Puffer info.live_ranges ist vollständig benutzerkontrolliert.
  • Die überlaufenden Werte sind benutzergesteuerte Eingaben, sodass wir die Berechnung überlaufen lassen können, sodass der Zeiger info.live_ranges an einem beliebigen Offset vor dem Start der Kerneladresse buff liegen kann.
  • Die Allokationsgröße ist ebenfalls benutzergesteuerte Eingabe, was die Möglichkeit gibt, eine Speicherzuweisung aus beliebigen General-Purpose-Slab-Allokatoren anzufordern.

Diese Schwachstelle weist Ähnlichkeiten mit der DeCxt::RasterizeScaleBiasData() Buffer Underflow-Schwachstelle auf, die ich im iOS 15 Kernel im Jahr 2022 gefunden und ausgenutzt habe.

Leakage von Kernel-Pointern in Timeline-Stream-Message-Puffern

Die GPU Mali implementiert einen benutzerdefinierten Timeline Stream, der Informationen sammelt, serialisiert und anschließend in einem bestimmten Format in einen Ringpuffer schreibt. Benutzer können den ioctl-Befehl kbase_api_tlstream_acquire aufrufen, um einen Dateideskriptor zu erhalten, der es ihnen ermöglicht, aus diesem Ringpuffer zu lesen. Das Format der Nachrichten ist wie folgt:

  • Ein Paket-Header

  • Eine Nachrichten-ID

  • Ein serialisierter Nachrichtenpuffer, dessen spezifischer Inhalt von der Nachrichten-ID abhängt. Beispielsweise serialisiert die Funktion __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait die Kernel-Pointer kbase_kcpu_command_queue und dma_fence in den Nachrichtenpuffer, was zu einem Leck von Kernel-Pointern an den Userspace-Prozess führt.```c void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait( struct kbase_tlstream *stream, const void *kcpu_queue, const void *fence ) { const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT; const size_t msg_size = sizeof(msg_id) + sizeof(u64) + sizeof(kcpu_queue) + sizeof(fence) ; char *buffer; unsigned long acq_flags; size_t pos = 0;

    buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);

    pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id)); pos = kbasep_serialize_timestamp(buffer, pos); pos = kbasep_serialize_bytes(buffer, pos, &kcpu_queue, sizeof(kcpu_queue)); pos = kbasep_serialize_bytes(buffer, pos, &fence, sizeof(fence));

    kbase_tlstream_msgbuf_release(stream, acq_flags); }

root@kitploit:~
Der Proof-of-Concept-Exploit leak die Adresse des `kbase_kcpu_command_queue`-Objekts, indem er die Nachrichten-ID `KBASE_TL_KBASE_NEW_KCPUQUEUE` überwacht, die von der Funktion `kbasep_kcpu_queue_new` ausgelöst wird, wenn ein neues KCPU-Warteschlangenobjekt zugewiesen wird.

Google informierte mich, dass die Schwachstelle im März 2023 gemeldet und im Sicherheitsbulletin mit [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) versehen wurde. Dennoch konnte ich das Problem auf den neuesten Pixel-Geräten mit den Sicherheitspatch-Stufen (SPL) für Oktober und November reproduzieren, was darauf hindeutet, dass der Fix nicht korrekt oder überhaupt nicht angewendet wurde. Anschließend hat Google das Problem im Dezember-Sicherheitsupdate-Bulletin schnell behoben, ohne Anerkennung zu geben, und mich später informiert, dass das Problem als Duplikat betrachtet wurde. Die Begründung für die Einstufung als Duplikat bleibt jedoch fragwürdig.

## Ausnutzung
---
Also habe ich zwei interessante Schwachstellen. Die erste bietet eine leistungsstarke Fähigkeit, den Inhalt einer beliebigen, 16-Byte-alignierten Kernel-Adresse zu modifizieren, die vor der allokierten ~buff~-Adresse liegt. Die zweite Schwachstelle liefert Hinweise auf die möglichen Positionen von Objekten im Kernel-Speicher.

### Anmerkungen zu den Werten buffer_count und live_ranges_count
Mit vollständiger Kontrolle über die Felder `buffer_count` und `live_ranges_count` habe ich die Flexibilität, das Ziel-Slab und den genauen Offset, auf den ich schreiben möchte, auszuwählen. Die Auswahl von Werten für `buffer_count` und `live_ranges_count` erfordert jedoch aufgrund mehrerer Einschränkungen und Faktoren sorgfältige Überlegung:
- Beide Werte sind miteinander verbunden, und der Überlauf erfolgt nur, wenn alle neu eingeführten Prüfungen umgangen werden.
- Die Anforderung, dass der negative Offset 16-Byte-aligniert sein muss, schränkt die Fähigkeit ein, an eine beliebige Stelle zu schreiben. Dies ist jedoch im Allgemeinen kein wesentliches Hindernis.
- Die Wahl eines größeren Offsets führt dazu, dass eine große Datenmenge in Speicherbereiche geschrieben wird, die möglicherweise nicht die beabsichtigten Ziele sind. Wenn beispielsweise die Allokationsgröße auf `0x3004` überläuft, wird der `live_ranges`-Zeiger auf `-0x4000` Bytes vom allokierten Speicher des `buff`-Objekts gesetzt. Die Funktion `copy_from_user` würde dann `0x7004` Bytes schreiben, basierend auf der Berechnung von `update->live_ranges_count` mal 4. Folglich würde dieser Vorgang dazu führen, dass benutzergesteuerte Daten den Speicherbereich zwischen dem `live_ranges`-Zeiger und der `buff`-Allokation überschreiben. Es ist daher wichtig, sorgfältig sicherzustellen, dass keine kritischen Systemobjekte in diesem Bereich versehentlich überschrieben werden. Da der Vorgang einen `copy_from_user`-Aufruf beinhaltet, könnte man in Betracht ziehen, einen `EFAULT` auszulösen, indem man den unerwünschten Speicherbereich nach dem Benutzerquellpuffer absichtlich abmappt, um zu verhindern, dass Daten an sensible Stellen geschrieben werden. Dieser Ansatz ist jedoch unwirksam, da die Funktion `raw_copy_from_user` bei einem Fehler die verbleibenden Bytes im Ziel-Kernelpuffer auf Null setzt. Dieses Verhalten ist implementiert, um sicherzustellen, dass der Rest des Kernelpuffers im Falle einer partiellen Kopie aufgrund eines Fehlers keine nicht initialisierten Daten enthält.```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
	unsigned long res = n;
	might_fault();
	if (!should_fail_usercopy() && likely(access_ok(from, n))) {
		instrument_copy_from_user(to, from, n);
		res = raw_copy_from_user(to, from, n);
	}
	if (unlikely(res))
		memset(to + (n - res), 0, res);
	return res;
}

In Anbetracht dessen müssen wir das zu überschreibende Objekt und die zu schreibenden Daten sorgfältig auswählen.

Das richtige Objekt zum Überschreiben auswählen

Da ich mit dieser unglücklichen Prüfung feststecke, besteht meine Strategie darin, ein Objekt zu identifizieren, das, wenn es auf null gesetzt wird, kein unerwünschtes Ergebnis hervorruft. Aber bevor ich dazu komme, gibt es ein weiteres Problem, das gelöst werden muss. Erinnern Sie sich, als ich im letzten Teil sagte, dass ich jede beliebige Zuweisungsgröße und damit jeden Allzweck-Slab-Cache-Allokator wählen kann, um meinen Zuweisungspuffer zu bedienen? Das ist nicht korrekt, denn es liegt wieder an copy_from_user! Das liegt an der CONFIG_HARDENED_USERCOPY Mitigation. Sie verbietet die Angabe einer Größe, die nicht der entsprechenden Slab-Cache-Größe entspricht, bei der der Kernel-Zielpuffer (in diesem Fall) einem Heap-Objekt entspricht. Sie bestimmt, ob die Seite des Puffers eine Slab-Seite ist, und wenn ja, ruft sie die passende kmem_cache->size ab und prüft, ob die vom Benutzer angegebene Größe diese nicht überschreitet; andernfalls stürzt der Kernel aufgrund des Größenkonflikts ab. Mit anderen Worten: Ich kann Objekte, die zum Allzweck-Allokator gehören, nicht angreifen, aber ich kann immer noch Objekte angreifen, die große Größen haben (d. h. solche, die direkt vom Seitenallokator bedient werden).

Der erste Gedanke, der mir in den Sinn kam, war die Verwendung der pipe_buffer-Technik, eine sehr elegante Technik, um beliebige Lese-/Schreib-Primitive zu erhalten. Ich werde nicht ins Detail der Technik gehen, aber die Leser werden ermutigt, diesen fantastischen Blog von Interrupt Labs zu lesen. Beim Erstellen eines Pipe-Objekts wird das pipe_buffer-Objekt zunächst in einem Array von 16 Elementen erstellt; die Array-Größe kann jedoch mit fcntl(F_SETPIPE_SZ) angepasst werden. Daher kann die pipe_buffer-Array-Zuweisung so angepasst werden, dass sie vom Seitenallokator bedient wird, was es zu einem perfekten Zielobjekt für den Angriff macht. Nachdem das pipe_buffer-Objekt als Zielkandidat ausgewählt wurde, besteht der nächste Schritt zur Erlangung von Kernel r/w darin, seinen Inhalt mit der Underflow-Schwachstelle zu überschreiben, die es mir ermöglicht, von/zu jedem Speicherort zu lesen/schreiben, dessen Seite das Feld pipe_buffer->page überschreibt. Da die Schwachstelle es mir erlaubt, beliebige Daten zu schreiben, kann ich den gesamten Inhalt von 'pipe_buffer' kontrollieren, einschließlich seines Page-Feldes, und dazu muss ich das pipe_buffer-Array vor dem anfälligen kbuff-Objekt zuweisen, und sie müssen nebeneinander liegen.

Positionierung von pipe_buffer und buff Objekten nebeneinander

Ich habe den Kernel-Speicher mit vielen kbase_kcpu_command_queue-Objekten besprüht (gesprayt), gefolgt von einer Reihe von pipe_buffer-Arrays. Ich kann die pipe_buffer-Arrays nicht allein als primäre Quelle für das Sprays nutzen, aufgrund der durch pipe_max_size auferlegten Einschränkung. Daher entschied ich mich, mit dem kbase_kcpu_command_queue-Objekt zu beginnen. Die Wahl des kbase_kcpu_command_queue-Objekts hatte zwei Gründe: Seine Zuweisungsgröße beträgt 0x38C8, wird also vom Seitenallokator behandelt, und ich kann deterministisch seine Kernel-Adresse unter Verwendung des Informations-Kernel-Leck-Bugs ermitteln, was es zu einem guten Objekt zum Sprayen sowie zu einem guten Zielobjekt macht (wie wir im nächsten Abschnitt sehen werden).

Wie bereits erwähnt, habe ich fcntl(F_SETPIPE_SZ) verwendet, um die Größe der pipe_buffer-Array-Zuweisung zu erhöhen, sodass sie vom Seitenallokator bedient werden kann. Genauer gesagt, habe ich die Zuweisungsgröße auf ==0x4000 Bytes (4 * PAGE_SIZE)== gewählt, um konsistent mit den kbase_kcpu_command_queue-Zuweisungen zu sein.

Ermitteln einer struct page-Adresse

Um das pipe_buffer richtig nutzen zu können, wird eine Seitenadresse benötigt. Die Kernel-Adresse eines kbase_kcpu_command_queue-Objekts, das ich gezielt erstellen und zerstören kann, identifizieren zu können, macht es zu einem guten Kandidaten, und das Finden der zugehörigen struct page kann mit virt_to_page erreicht werden.

Zu schreibende Inhalte im pipe_buffer

So das pipe_buffer-Objekt ist wie folgt:```c struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };

root@kitploit:~
Wie bereits erwähnt, muss das `page`-Feld eine gültige Seitenadresse enthalten. Die Felder `offset` und `len` dürfen `PAGE_SIZE` nicht überschreiten, andernfalls erhöht die Pipe die Head/Tail-Zähler, was zur Verwendung eines neuen `pipe_buffer`-Objekts und zum Kontrollverlust über den gefälschten Pipe-Puffer führt.  
Außerdem müssen die `flags` auf `PIPE_BUF_FLAG_CAN_MERGE` gesetzt sein, damit die folgenden `pipe_write`-Aufrufe nicht blind den Head-Zähler erhöhen und den nächsten Pipe-Puffer verwenden, sondern zuerst prüfen, ob im aktuellen `pipe_buffer` Platz für die Schreibanforderung vorhanden ist. Falls ja, werden die Daten einfach an denselben Pipe-Puffer angehängt, beginnend beim Wert, der im Feld `len` gespeichert ist.  
Um einen Absturz des Geräts in `pipe_buf_confirm` zu vermeiden, das von `pipe_write` und `pipe_read` aufgerufen wird, muss der `ops`-Zeiger ebenfalls eine gültige Kernel-Adresse sein, deren Feld `ops->confirm` auf _NULL_ gesetzt ist. Ich kann einfach einen Offset innerhalb des durchgesickerten `kbase_kcpu_command_queue`-Objekts verwenden, der NULL ist und sich unter keinen Umständen ändert.

### Auswahl des optimalen Offsetwerts für den Unterlauf
Während die Zuweisungsgrößen von `buff`, `kbase_kcpu_command_queue` und `pipe_buffer` bei ~0x4000~ Bytes liegen, habe ich mich entschieden, den Puffer mit **0x8000** Bytes zu unterlaufen. Warum?

Werfen wir einen kurzen Blick darauf, wie `pipe_buffers` während Lese- und Schreibvorgängen aktualisiert werden. Nehmen wir an, wir können den `pipe_buffer` wie folgt gestalten:```c
struct pipe_buffer {
	.page = virt_to_page(addr),
	.offset =  0,
	.len = 0x40,
	.ops = kcpu_addr + 0x50,
	.flags = PIPE_BUF_FLAG_CAN_MERGE,
	unsigned long private = 0
};

Während der Fehler die Möglichkeit bietet, den Inhalt dieses Objekts willkürlich zu kontrollieren, tut er dies nur einmal, da das unterlaufene Objekt unmittelbar nach dem Ende des ioctl-Aufrufs freigegeben wird. Dies stellt tatsächlich ein Problem dar, da ich das pipe_buffer-Objekt manuell aktualisieren muss, um es wieder verwendbar zu machen, da jeder Lese-/Schreibvorgang der Pipe:

  • Das .page-Feld wird nicht aktualisiert; es bleibt gleich, und wenn der Puffer leer ist, wird er freigegeben, was ich nicht möchte, da das .ops-Feld nicht korrekt gesetzt ist.
  • Da das pipe_buffer das .offset-Feld bei einem Lesevorgang aktualisiert, kann ich denselben Speicherbereich nicht erneut lesen.
  • Die in das pipe_buffer geschriebenen Daten werden ab dem Wert von .len an den Puffer angehängt (vorausgesetzt, das PIPE_BUF_FLAG_CAN_MERGE-Flag ist gesetzt), und .len wird entsprechend aktualisiert. Das bedeutet, wir können nicht zweimal Daten in die exakte Adresse schreiben.

Daher kann ich, wenn ich das pipe_buffer nicht nach jedem Lese- oder Schreibvorgang ordnungsgemäß aktualisiere, nicht gleichzeitig von derselben Pipe lesen und in sie schreiben. Deshalb ist ein Unterlauf mit 0x8000 Bytes viel praktischer, denn anstatt ein einzelnes pipe_buffer zu überschreiben, überschreibe ich zwei verschiedene pipe_buffer-Instanzen von zwei verschiedenen Pipe-Objekten: eines wird für Lese- und das andere für Schreibvorgänge betrachtet.```c #define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */

pipe_read = (struct pipe_buffer *)( ptr); pipe_read->page = virt_to_page(ta->kcpu_kaddr); pipe_read->offset = 0; pipe_read->len = 0xfff; pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_read->private = 0;

pipe_write = (struct pipe_buffer )( ptr + 0x4000); pipe_write->page = virt_to_page(ta->kcpu_kaddr); pipe_write->offset = 0; pipe_write->len = 0; / This is the starting position of the pipe_write */ pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_write->private = 0;

root@kitploit:~
Der `pipe_read` ist ein gefälschter Pipe-Puffer, der zum Lesen von Daten von der Zielseite ab `.offset = 0` bis zu `0xfff` Bytes verwendet wird, während `pipe_write` ein gefälschter `pipe_buffer` ist, der zum Schreiben von Daten ab `.len = 0` bis zu `0xfff` Bytes verwendet wird.  
Es ist auch sehr wichtig, noch einmal zu erwähnen, dass das Schreiben von mehr als `PAGE_SIZE` Bytes die Pipe dazu veranlasst, den Head-Zähler zu erhöhen, wodurch ein neu zugewiesener `pipe_buffer` verwendet wird und die Kontrolle über unseren gefälschten `pipe_write` verloren geht. Andererseits bewirkt das Leeren (Lesen von 0xfff Daten aus) des `fake_read`-Puffers, dass der Kernel die tatsächliche Seite freigibt, indem er `ops→release` aufruft, was zum Absturz des Kernels führt, da ich noch keine Kernel-Text-Adresse habe.  
Obwohl ich es geschafft habe, die Lese- und Schreibvorgänge der Pipe so zu trennen, dass ein Schreiben in ein Pipe-Ende den anderen Pipe-Puffer nicht beeinträchtigt und umgekehrt, habe ich das Kernproblem noch nicht gelöst: Wie kann der Pipe-Puffer zuverlässig aktualisiert werden? Die offensichtliche Antwort, die mir in den Sinn kam, war, den Spray-Prozess nach jedem Pipe-Lese- oder Schreibaufruf immer wieder zu wiederholen.  Und das macht keinen Sinn, da es die Zuverlässigkeit des Exploits erheblich beeinträchtigen würde. Im folgenden Abschnitt werde ich das Ziel in zwei Unterziele aufteilen: Zunächst werde ich mich nur auf das `.page`-Feld konzentrieren, danach auf die `.len/.offset`-Felder.  

### Modifying the pipe_buffer→page Field  
Zu meiner Überraschung muss ich das `.page` überhaupt nicht aktualisieren, denn ich kann den `pipe_buffer→page` überschreiben, um auf die Seitenadresse des geleakten `kbase_kcpu_command_queue` zu zeigen. Daher **muss ich nur das `kbase_kcpu_command_queue`-Objekt freigeben und es mit einem neuen `pipe_buffer`-Objekt überlappen. Jupp! Jetzt habe ich einen `pipe_buffer→page`, der auf ein legitimes `pipe_buffer`-Objekt zeigt!  
Das Ersetzen von `kbase_kcpu_command_queue` durch `pipe_buffer` gibt uns die Möglichkeit, einen legitimen Pipe-Puffer zu manipulieren, ohne regelmäßig das `.page`-Feld aktualisieren zu müssen. Allerdings muss ich mich noch mit den `.len`- und `.offset`-Feldern befassen.  

### Modifying the pipe_buffer→len/offset Fields  
Wie ich bereits erwähnt habe, aktualisiert ein Pipe-Lese-/Schreibvorgang die Felder `.len` und `.offset`, wodurch nachfolgende Lese-/Schreibvorgänge auf derselben Seite unbrauchbar werden, selbst wenn sie über die beiden verschiedenen Pipes ausgeführt werden. Hier ist ein weiterer Trick: **Es gibt eine Technik, um Daten zu lesen/schreiben, ohne die Felder `.len/.offset` überhaupt zu berühren!**.  Und dies ist möglich, indem man die `copy_page_from_iter`- und `copy_page_to_iter`-Aufrufe bei `pipe_read/write` fehlschlagen lässt! Ja, genau wie `copy_to/from_user` kopieren `copy_page_to/from_iter` Daten von/nach userspace, die durch die `iov_iter`-Struktur übergeben werden, und sie können fehlschlagen (faulted).  

Um mit dem vorherigen Beispiel fortzufahren: Wenn wir 8 Bytes Daten an eine Adresse schreiben möchten, muss die bereitgestellte Userspace-Puffergröße 8 sein, gefolgt von einem nicht gemappten oder nicht lesbaren Speicherbereich, und dann übergeben wir `9` als Größenargument an den `write`-Systemaufruf, um die Datenmenge anzugeben, die wir schreiben möchten.Dieser Vorgang schreibt 8 Bytes und schlägt beim _neunten_ fehl, da er auf eine nicht gemappte/nicht lesbare Speicherstelle stößt. Folglich wurden die Daten effektiv in den Ziel-Kernel-Puffer geschrieben und das`.len` Feld wurde nicht modifiziert. Die Kernel-Funktion `pipe_write` wird einfach zurückkehren, ohne das `buf->len`-Feld zu aktualisieren.```c
		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) {
			ret = pipe_buf_confirm(pipe, buf);
			if (ret)
				goto out;

			ret = copy_page_from_iter(buf->page, offset, chars, from);
			if (unlikely(ret < chars)) {
				ret = -EFAULT;
				goto out;
			}

			buf->len += ret;
			if (!iov_iter_count(from))
				goto out;
		}

Gleiches gilt für Lesevorgänge; wenn wir 8 Bytes lesen möchten, machen wir das neunte Byte des Puffers unlesbar und geben dann einfach an, 9 Bytes lesen zu wollen. Die Daten werden in den Benutzerpuffer kopiert, ohne dass das .offset-Feld geändert wird.

Folglich können wir unbegrenzte Lese-/Schreiboperationen auf jede Kernel-Speicheradresse durchführen, ohne wiederholt den Spray-Prozess durchlaufen zu müssen.

Root erlangen

Nachdem ich eine starke beliebige Lese-/Schreibprimitive hatte, durchsuchte ich alle struct page im VMEMMAP_START-Array, um die Startadresse des Kernel-Texts zu ermitteln, unter Verwendung der im Blogbeitrag Interrupt Labs beschriebenen Technik. Dann stellte ich fest, dass init_task in den Android November-Sicherheitsupdates ausgenullt wird, also verwendete ich stattdessen kthreadd_task. Mit der Kernel-Adresse von kthreadd_task konnte ich die task->tasks-Liste durchlaufen und meine eigene current-Task-Kerneladresse erhalten. Dann setzte ich die cred-Struktur auf null, um Root-Rechte zu erlangen.

Später erkannte ich, dass das Scannen aller Seitenadressen unnötig war, da ich bereits die Kernel-Textadresse von anon_pipe_buf_ops aus einem pipe_buffer-Objekt hatte. Mit diesen Informationen konnte ich die Basisadresse des Kernel-Texts ableiten und so KASLR effektiv umgehen.

SELinux deaktivieren

Der Exploit deaktiviert auch SELinux. Mit der Basisadresse des Kernel-Texts muss ich nur noch den Speicherort der globalen Struktur selinux_state finden und dann den .enforcing-Wert auf null setzen.

Proof of Concept

Der dem Bericht beigefügte Proof of Concept wurde auf Pixel 7 und 8 Pro Geräten mit Android 14 und den ASBs von Oktober und November getestet und erreichte eine Erfolgsrate von nahezu 100%. Es ist auch wichtig zu erwähnen, dass der Exploit aufgrund einiger fest codierter Offsets nicht sofort auf anderen Geräten funktioniert. Um Unterstützung für ein neues Gerät hinzuzufügen, müssen folgende Werte bereitgestellt werden:

  • kthreadd_task-Offset von der Kernel-Basisadresse.
  • selinux_state-Offset von der Kernel-Basisadresse.
  • task_struct->cred, task_struct->pid und task_struct->tasks Struktur-Offsets.
  • anon_pipe_buf_ops-Offset von der Kernel-Basisadresse.

Kompilierung

Um den Exploit als eigenständige Binärdatei zu kompilieren, verwenden Sie den folgenden Befehl, und führen Sie ihn dann mit adb shell aus:```sh $ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog $ adb push poc /data/local/tmp/ $ adb shell /data/local/tmp/poc

root@kitploit:~
Sie können den Exploit auch über eine Android Studio App ausführen, indem Sie dieses Verzeichnis mit einbetten und stellen Sie sicher, dass die nutzlosen C++-Warnungen deaktiviert werden, indem Sie `-w -Wno-c++11-narrowing` zur cmake-Datei hinzufügen.

### Demo```shell
$ adb logcat  |grep -i EXPLOIT
11-28 16:04:12.500  7989  7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563  7989  7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000  from context (0x0)
11-28 16:04:18.441  7989  7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000  from context (0xff)
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444  7989  7989 E EXPLOIT : 10 00 39 01 89 FF FF FF  10 00 39 01 89 FF FF FF  | ..9.......9.....
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.445  7989  7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446  7989  7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462  7989  7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463  7989  7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF  00 00 00 00 30 00 00 00  | @..&........0...
11-28 16:04:18.463  7989  7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF  10 00 00 00 00 00 00 00  | p7..............
11-28 16:04:18.463  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00                           | ........
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102  7989  7989 E EXPLOIT : [+] Cleanup  ... OK
Tool herunterladen