
Android-Kernel-CVE-Analyse und PoC für eine Type-Confusion im MediaTek-ION-Allocator, einschließlich Root-Cause-Diffing, unprivilegierter Auslösung und Bewertung der Ausnutzbarkeit.
Zusammenfassung. CVE-2023-20768 ist eine Typverwechslung (CWE-843) im ION-Allokator von MediaTek. Ich habe geprüft, ob sie auf einem Samsung SM-M325F (Galaxy M32, Helio G80) mit der Firmware vom Juli 2022 tatsächlich ausnutzbar ist. Der verwundbare Code ist vorhanden, und ich habe nachgewiesen, dass er auf diesem Gerät ausgeführt wird, wenn er von einem unprivilegierten Prozess ausgelöst wird. Ich konnte ihn nicht ausnutzen. Der ioctl-Pfad ist durch Eingabevalidierung begrenzt, und der Pfad, der den eigentlichen Out-of-Bounds-Read enthält, verarbeitet nur echte ION-Puffer, weil eine zweite Prüfung des dma_buf_ops-Zeigers alles ablehnt, was ich fälschen könnte. Dieser Bericht beschreibt, wie ich zu diesem Schluss gekommen bin, einschließlich eines Zwischenergebnisses, das sich als falsch herausstellte.
Die CVE ist öffentlich und wurde gepatcht. Alle Tests wurden auf meinem eigenen, mit Magisk gerooteten Gerät durchgeführt.
Der Fehler liegt im ION-Code von MediaTek, nicht im Upstream-Linux oder in etwas, das Samsung geschrieben hat. MediaTek liefert seinen eigenen Fork des Android-ION-Allokators im BSP aus, das an jeden Hersteller geht, der ihre Chips verbaut. Das M32 verwendet einen Helio G80 und bekommt daher diesen Code. Dasselbe Telefon mit einem Exynos-SoC wäre überhaupt nicht betroffen.
Ich habe die vmlinux-Images von 2022 (verwundbar) und 2023 (gepatcht) geholt und sie in IDA einem Diff unterzogen. Zwei ION-Funktionen haben sich geändert:
| Funktion | 2022 | 2023 |
|---|---|---|
ion_drv_file_to_buffer | strstr(name, "dmabuf") | is_dma_buf_file() |
_ion_ioctl | strcmp(name, "ion") | is_dma_buf_file() |
is_dma_buf_file existiert im 2022er-Image nicht. Es taucht im 2023er auf. Beide Funktionen entschieden also anhand eines Namens, ob eine struct file ein dma_buf war, und der Fix ersetzte das durch eine echte Typprüfung. Wenn hier Objekte verwechselt werden, liest der Kernel ein Nicht-dma_buf, als wäre es eines.
Von den beiden ist _ion_ioctl diejenige, die ein unprivilegierter Prozess erreichen kann:
open("/dev/ion")
-> ion_ioctl (.unlocked_ioctl)
-> ION_IOC_CUSTOM (0xC0104906)
-> ion_custom_ioctl
-> _ion_ioctl
-> case 0: ION_SYS_CACHE_SYNC
-> find_vma(user_VA) (call site at _ion_ioctl+0x9f0)
-> strcmp(vma->vm_file...name, "ion")
Die Anfrage ist eine ion_custom_data { u32 cmd = 0; u64 arg; }, die auf eine 120 Byte große ion_sys_data zeigt:
spoof.c baut das auf.
Ich konnte es nicht einfach tracen. Das Gerät blockiert kprobe_events, set_ftrace_filter und function_graph über Samsungs SELinux-Richtlinie und Kernel-Härtung, und die printks von ION sind nur bei Debug aktiviert, sodass dmesg ruhig bleibt.
Also nutzte ich stattdessen Rückgabecodes als Orakel. Vier Anfragen, und das Muster der Antworten verrät, wohin die Ausführung gelangte:
Dass C Erfolg zurückgibt, bedeutet, dass die Switch-Anweisung tatsächlich anhand von sys_cmd verzweigt. Dass A und B sich unterscheiden, bedeutet, dass die VA verarbeitet wird, wodurch die Ausführung in find_vma landet. Das ist der verwundbare Pfad, der ohne Root erreicht wird.
Die Prüfung zu erreichen ist nicht dasselbe, sie zu überwinden. Ich habe getestet, was der strcmp tatsächlich akzeptiert:
ION_IOC_SHARE und dann mmap gemappt wird, besteht die Prüfung und gibt 0 zurück.memfd:ion schlägt fehl, -EFAULT.ion heißt, schlägt fehl, -EFAULT.Das Feld, das bei vm_file+0x60 verglichen wird, ist also nicht der Dateiname. Es ist ein dma_buf-internes Feld, fast sicher dma_buf->exp_name, das ION auf "ion" setzt. Die Prüfung ist von Natur aus unsolide, aber nichts, was ich aus dem Userspace erzeugen kann, kann dieses Feld setzen.
Ich habe den Pfad dann mit Fuzzing getestet: sync_type 0 bis 7, Größen {0, 1, 0x1000, 0x100000, 0xffffffff}, VAs {echter ION-Puffer, memfd, 0}, 120 Fälle, dazu eine Sonde mit freigegebenem Handle für einen Use-after-Free. Kein Absturz, das Gerät blieb an. Überdimensionierte Größen brechen vor find_vma ab und geben 0 zurück. Die sync-Typen 3 bis 5 treffen den m4u-Pfad und geben -EPERM zurück. Alles über 5 gibt -EINVAL zurück. Ein freigegebenes Handle gibt -EINVAL zurück, ION validiert es also, und es gibt dort kein UAF.
Der eigentliche Out-of-Bounds-Read befindet sich in ion_drv_file_to_buffer. Er führt ldr [private_data+0x28] aus und liest das private_data eines Nicht-dma_buf, als wäre es ein dma_buf. Es gibt einen Vergleich ops == &ion_dma_buf_ops (die Tabelle liegt bei 0xFFFFFF800A097F18), aber dieser findet nach diesem Read statt und verhindert ihn daher nicht. Nachgelagert liest __do_dump_share_fd Felder bei +0x28, +0x48, +0x50, +0xb8, +0xe4 aus dem zurückgegebenen Puffer und gibt sie aus, und ldr x8, [buf+0x28]; ldr [x8+0x30] ist eine wilde Zeigerdereferenz für ein verwechseltes Objekt.
Der Auslöser ist ion_dump_all_share_fds, das mit iterate_fd die Dateideskriptoren jedes ION-Client-Prozesses durchläuft. Meine erste Schlussfolgerung war, dass dies nur läuft, wenn man einen ION-debugfs-Knoten liest, und dieser Kernel hat CONFIG_DEBUG_FS nicht gesetzt. Das habe ich auf drei Wegen verifiziert: /proc/config.gz, debugfs fehlt in /proc/filesystems, und mount -t debugfs gibt ENODEV zurück. Ich habe den Pfad als strukturell unerreichbar abgetan.
Das war falsch. dump_header, der Speicherauszug des OOM-Killers, enthält einen direkten bl ion_mm_heap_memory_detail bei 0xffffff8008204b9c, und dump_header wird von out_of_memory und oom_kill_process aufgerufen. Kein debugfs nötig.
memcg_oom.c bestätigt das. Es erzeugt eine Cgroup unter /dev/memcg, begrenzt sowohl memory.limit_in_bytes als auch memory.memsw.limit_in_bytes auf 8 MB (wenn nur die erste begrenzt wird, kann das Kind in den zram-Swap entkommen), und forkt ein Kind, das allokiert, bis es stirbt. dmesg zeigt dann:
dump_header <- oom_kill_process <- out_of_memory <- mem_cgroup_oom_synchronize
gefolgt von der vollständigen Ausgabe von ion_mm_heap_memory_detail und __do_dump_share_fd, die echte gralloc-dma_bufs auflöst. Die verwundbare Funktion wird also ausgeführt – während eines Vorgangs, den jeder unprivilegierte Prozess auslösen kann. Es gibt auch einen dritten Auslöser, ShowStatus von hang_detect_dump_thread im MediaTek-Watchdog.
Ich hielt 32 memfds mit dem Namen memfd:dmabuf offen und löste denselben memcg-OOM aus. Wenn einer von meinen an ion_drv_file_to_buffer verfüttert worden wäre, würde der strstr bestehen, private_data wäre NULL, und der Kernel würde [ION]ion_drv_file_to_buffer warnning, dmabuf is NULL auf KERN_ERR ausgeben. Diese Zeile erschien nie. Nur Graphics- und gralloc-Clients wurden ausgegeben. Entweder ist die fd-Tabelle eines einfachen /dev/ion-Clients nicht das, was iterate_fd hier durchläuft, oder memfd schlägt still vor der Ausgabe fehl.
Wie auch immer: Der OOM-Dump verarbeitet nur die legitimen dma_bufs des Systems, die die Prüfung sauber passieren. memfd ist außerdem der einzige fd-Typ, dessen Name ich stark genug kontrollieren kann, um "dmabuf" zu enthalten, und es kann keinen Fehler auslösen.
Die Schwachstelle ist vorhanden. Der verwundbare Code ist erreichbar und wird auf diesem Build tatsächlich ausgeführt, ohne Root auslösbar. Eine Ausnutzung vom Userspace aus ist hier nicht möglich. Der ioctl-Pfad ist durch Handle-Validierung, eine Größenprüfung und access_ok begrenzt. Der Dump-Pfad sieht nur echte ION-Puffer, und das Fälschen wird durch ops == &ion_dma_buf_ops blockiert. Um weiter zu kommen, bräuchte man ein kontrollierbares Nicht-ION-Objekt, dessen exp_name "ion" ist, oder eine andere Primitive wie ein ION-Puffer-UAF oder eine TOCTOU-Race.
spoof.c — Cache-Sync-ioctl-PoC und das differentielle Orakelmemcg_oom.c — memcg-OOM-Auslöser für den Dump-Pfadtrigger.c, oom_trigger.c — frühere Trigger-Versucheboot_images/ — extrahierte Kernel-Images (2022 und 2023) und die IDA-Datenbank für den 2022er-Build| Offset | Feld |
|---|
+0x00 | sys_cmd = 0 |
+0x08 | ion-Handle (zuerst eines allokieren, heap_id_mask = 0x1 funktioniert) |
+0x10 | virtuelle Benutzeradresse |
+0x18 | niederwertige Hälfte = Größe, hochwertige Hälfte = sync_type aus {0,1,2} |
| Fall | Anfrage | Ergebnis |
|---|
| A | sys_cmd=0, gefälschte VA | -EFAULT |
| B | sys_cmd=0, VA = 0 | 0 |
| C | sys_cmd=4 | 0 |
| D | sys_cmd=99 | -EFAULT |