
Lokaler Privilege-Escalation-Exploit, der CVE-2026-43499 für das Xiaomi 17T Pro (warhol) unter Android 16 mit MediaTek-MT6993-SoC ausnutzt.
CVE-2026-43499 (IonStack) lokale Privilegienerweiterung, portiert auf das Xiaomi 17T Pro
(warhol) — MediaTek MT6993, Android 16.
Nur GKI 6.12 / android16. Das Waiter-Overlay, das
pselect-Stack-Layout und jeder Struct-Offset sind an diesen Branch gebunden, daher lässt sich hier nichts auf 6.6, 6.1 oder 5.10 übertragen — dafür braucht es eine eigens dafür gebaute Basis.generate_target.pylehnt jeden anderen Banner ab, anstatt einen Header zu erzeugen, der auf dem Gerät einen Fehler auslösen würde.
Status: funktionsfähig. Am 2026-07-26 auf dem Gerät ab einem sauberen Boot verifiziert, unter beiden Preloader-Varianten —
uid=0(root) context=u:r:kernel:s0, SELinux permissiv,suinstalliert. Root ist nicht persistent: DieLD_PRELOAD-Zeile muss nach jedem Boot erneut ausgeführt werden. Die pselect-Race ist nicht 100 %: Ein fehlgeschlagener Lauf kann das Telefon in den Kernel-Panic treiben, und ein erneuter Versuch nach dem Neustart ist normal. SieheWARHOL_PORT.mdfür die vollständige Aufstellung.
Dieses Gerät benötigt den Kernel-MTE-Fix. Der unveränderte Upstream-Popsicle kann es nicht rooten — er dreht endlos in
kernel page retry N/12. Siehe Kernel-MTE.
Die Builds gibt es in zwei Preloader-Varianten, PRELOADER=retail (Standard) und
PRELOADER=eng. Sie wählen unterschiedliche Zielverzeichnisse, sodass keiner die Adressen des
anderen überschreiben kann — siehe Preloader-Variante.
Die folgenden Fakten wurden aus dem Retail-Full-OTA-Paket ausgelesen — Metadaten, das
A/B-Payload-Manifest und die aus payload.bin extrahierten boot/vendor_boot/dtbo-Images.
Der Kernel ist hier wichtiger als das SoC: warhol liegt auf dem gleichen GKI-Branch wie
Upstream-Popsicle (android16-5, 4K-Seiten), einen Patchlevel entfernt — 6.12.38
gegenüber dem verifizierten 6.12.23 von Popsicle. Struktur-Offsets müssen dennoch pro Build
neu erzeugt werden; nur die Form des Exploits überträgt sich. Ein anderes OS3.0.x-Build
benötigt sein eigenes target.h.
Die Exploit-Kette ist versionsmäßig an den GKI-Branch gebunden, nicht an das SoC. Das
rt_mutex-Waiter-Layout, das pselect-Stack-Overlay und die pipe_buffer-Struktur-Offsets
folgen alle dem Kernel, daher schlägt eine 6.12/android16-Basis eine Basis desselben
Herstellers auf einem älteren Branch.
x-spy/CVE-2026-43499-popsicle ist die einzige öffentliche Implementierung, die auf der
6.12/android16-GKI-Linie verifiziert wurde (Xiaomi 17 / Pro / Ultra, Kernel
6.12.23-android16-5), daher stammt source/ daraus und wird so nah wie möglich am Upstream
gehalten — abgesehen von zwei Zeilen (siehe Kernel-MTE, das dieses Gerät
überhaupt erst zum Funktionieren braucht).
Der Großteil der MediaTek-Abweichungen beschränkt sich auf die Zielgenerierung zur Buildzeit, in zwei Teilen:
p0_phys_offset und p0_kernel_phys_load aus einer Qualcomm-xbl_config-Partition, die warhol nicht hat. generate_target.py --dtb liest stattdessen die DRAM-Basis aus dem /memory-Knoten des vendor_boot-FDT und rundet sie genauso ab, wie arm64_memblock_init es tut. Die physische Ladeadresse des Kernels wird mit --preloader aus der mb_kernel-Reservierung des Preloaders ausgelesen; die Differenz ergibt für dieses Gerät 0, und lk weigert sich, einen an einer anderen Stelle platzierten Kernel zu booten.boot.img aus und nicht als nacktes arm64-Image, daher dekomprimiert der Generator es vor der Analyse.WARHOL_PORT.md §2 enthält die vollständige Herleitung.
MediaTek-lk lädt den Kernel nicht an eine Compile-Zeit-Konstante — es sucht eine
DRAM-Reservierung namens mb_kernel und stellt sicher, dass er exakt dort gelandet ist. Diese
Reservierung nimmt der Preloader vor, weshalb die auf dem Telefon laufende
Preloader-Version hier überhaupt ein Build-Eingang ist: Sie ist das einzige Element außerhalb
von boot.img, das P0_KERNEL_PHYS_LOAD verschieben kann, und ein falscher Wert dort bedeutet
einen falschen Linear-Map-Alias und ein totes Telefon statt eines fehlgeschlagenen Exploits.
Daher zwei Varianten, die als getrennte Zielverzeichnisse geführt werden:
make preload # PRELOADER=retail -> out/preload-<device>.so
make PRELOADER=eng preload # -> out/preload-<device>-eng.so
make both # both of the above, and print their sha256
Beide Artefakte werden unter verschiedenen Namen nebeneinander abgelegt. Bei dieser Firmware fallen sie byteidentisch aus — das ist das Ergebnis der folgenden Analyse und keine Abkürzung daran vorbei, daher werden die beiden weiterhin getrennt gebaut und benannt.
Lies die Speicherlayout-Tabellen der beiden Preloader selbst mit:
python3 tools/preloader_memlayout.py <preloader.bin> --diff <preloader_eng.bin>
Bei dieser Firmware sind die beiden Tabellen identisch, mb_kernel.start = 0x80000000 in
beiden, daher ist das target.h des eng-Builds byteidentisch mit dem des Retail-Builds und
beide Builds erzeugen dasselbe preload.so.
Was tatsächlich im Exploit ankommt, ist die Differenz
P0_KERNEL_PHYS_LOAD − P0_PHYS_OFFSET, und ihre beiden Terme stützen sich nicht auf gleich
belastbare Belege:
P0_KERNEL_PHYS_LOAD — gemessen aus dem eng-Image, als mb_kernel.start oben.P0_PHYS_OFFSET — wurde argumentiert statt gemessen, da es memstart_addr ist, das der Kernel aus dem /memory-Knoten übernimmt, den lk zur Laufzeit aus dem erzeugt, was der Preloader nach der DRAM-Initialisierung meldet, nicht aus dem statischen FDT, den der Generator liest. Das Argument war, dass beide Builds den gesamten DRAM-Pfad teilen — dieselben dramc/emi/mblock/memory_layout-Quellen, kein Speicherlayout-Code unter den Nur-eng-Strings — und dass die eine zusätzliche Reservierung des eng-Preloaders, ein dynamisches 3-MiB-security_fe_rsv mit mapping=1, weder einen Eintrag mit fester Adresse verschieben noch ein Loch am unteren Ende des DRAM reißen kann. Der Lauf auf dem Gerät hat es entschieden: Die Linear-Map-Aliase landeten unter eng, also stimmt der Term.Vollständige Herleitung in
targets/warhol-OS3.0.304.0.WPSJPXM-eng/NOTES.md.
Notizen vom Lauf auf dem Gerät:
ro.boot.verifiedbootstate=green und flash.locked=1. Das war eine offene Frage, bis es ausprobiert wurde; entschieden wird das durch den in Efuse gebrannten Root-Key-Hash, und dieses Build ist mit denselben Schlüsseln signiert wie das Retail-Build.%s META DIS ist ein reiner Retail-String, während eng eine vollständige META-Implementierung mitführt, die Retail nicht hat (Enable fastmeta., FAST META GPIO: %d, META_COM PORT: %d, read_meta_proinfo, …). Hier bootete es direkt zu Android, aber falls ein mit eng geflashtes Telefon jemals woanders landet, ist das die wahrscheinliche Ursache, und kein target.h-Wert ist daran beteiligt.mb_kernel verschiebt, divergieren die beiden Ziele — sie als getrennte Verzeichnisse zu führen, verhindert, dass das still geschieht.Dieser Kernel läuft mit KASAN_HW_TAGS — /proc/cmdline enthält
kasan.stack_ring_size=524288 — daher tragen Slab-Zeiger ein Allokations-Tag in den Bits
59:56. Upstream-Popsicle nimmt ungetaggte Zeiger an und kann dieses Gerät daher überhaupt
nicht rooten: Es läuft endlos in [-] kernel page retry N/12 mode=1, ohne dass das Log einen
Grund nennt.
Zwei Zeilen in source/ beheben das, und beide befinden sich in warhol-mte-fix.patch:
Nur die Prüfungen untaggen. Die Zeiger selbst bleiben getaggt, denn das Tag ist das korrekte für den Speicher, auf den sie zeigen — genau das benötigt eine Kernel-Dereferenzierung von ihnen.
Lies dafür nicht arm64.memtag.bootctl: Diese Property ist die Userspace-MTE-Steuerung und
sagt nichts darüber aus, ob der Kernel seine eigenen Allokationen taggt.
Dieses Repository ist ein Port. Die Anerkennung für die Exploit-Kette gebührt dem Upstream.
source/ exploit core (upstream popsicle + the kernel-MTE fix; see warhol-mte-fix.patch)
src/ main.c slide.c fops.c pipe.c util.c preload.c su_daemon.c + kernelsnitch/
targets/ per-build generated target.h, one dir per fingerprint x preloader variant
warhol-OS3.0.304.0.WPSJPXM/ PRELOADER=retail
warhol-OS3.0.304.0.WPSJPXM-eng/ PRELOADER=eng
tools/
payload_dump.py extract partitions from an A/B payload.bin (verifies size+sha256)
kernel_banner.py read the Linux banner out of a boot.img
bootinfo.py dump boot / vendor_boot header fields
generate_target.py target.h generator; upstream's, MediaTek-only, plus kernel decompression
preloader_memlayout.py dump/diff a MediaTek preloader's static DRAM reservation table
lz4legacy.py LZ4 legacy-frame decompressor (kernel images)
out/ build artifacts (gitignored)
Es wird nichts gebaut, bis ein target.h existiert — das Makefile schlägt laut fehl, anstatt
ein Binary gegen den falschen Kernel auszugeben.
# 1. extract boot.img straight out of the OTA zip (payload.bin is stored uncompressed,
# so --base seeks into the zip; no need to unpack 7.6 GB first)
python3 tools/payload_dump.py <ota.zip> --base 5081 -p boot,vendor_boot,dtbo -o <dir>
# 2. confirm the kernel banner — every offset downstream is pinned to it
python3 tools/kernel_banner.py <dir>/boot.img
python3 tools/bootinfo.py <dir>/boot.img
# 3. generate the target header. --preloader reads the kernel's physical load address
# out of the preloader's mb_kernel reservation; pass the preloader the phone runs.
python3 tools/generate_target.py \
--boot <dir>/boot.img --dtb <dir>/vendor_boot.img --preloader <preloader.bin> \
-o targets/warhol-OS3.0.304.0.WPSJPXM/target.h
# 4. build
make preload # DEVICE=warhol-OS3.0.304.0.WPSJPXM, PRELOADER=retail by default
Ohne ein Preloader-Image zur Hand ist --kernel-phys-delta 0 die ältere Form und erzeugt für
diese Firmware denselben Header — aber es ist eine Annahme statt einer Auslesung, also
--preloader bevorzugen. Werden beide übergeben, prüft der Generator sie gegeneinander und
bricht bei Abweichung ab.
--base 5081 ist der Local-Header-Offset von payload.bin für dieses spezielle OTA; für jedes
andere Paket liest du ihn aus ota-property-files in META-INF/com/android/metadata.
Erfordert ein Android-NDK (NDK_ROOT / ANDROID_NDK_HOME) und llvm-objdump.
# <device> is the target name: <fingerprint> for PRELOADER=retail, <fingerprint>-eng for eng
adb push out/preload-<device>.so /data/local/tmp/preload.so
adb shell chmod 0644 /data/local/tmp/preload.so
adb shell "LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true"
adb shell "/data/local/tmp/su -c id"
cred und der SELinux-Zustand werden im Speicher gepatcht, daher muss das nach jedem Boot
wiederholt werden. Ein Bootloader-Unlock ist nicht erforderlich.
Zur Forschung an Hardware, die dir gehört. Die Ausführung kann ein Gerät in eine Bootloop versetzen oder unbrauchbar machen (bricken) und führt zum Erlöschen der Garantie. Es besteht keinerlei Gewährleistung.
Die Upstream-Projekte sind von ihren jeweiligen Autoren lizenziert; dieser Port übernimmt deren Bedingungen.
| Punkt | Wert | Quelle |
|---|
| Codename | warhol | pre-device=warhol |
| Build-Fingerabdruck | Xiaomi/warhol_global/warhol:16/BP2A.250605.031.A3/OS3.0.304.0.WPSJPXM:user/release-keys | post-build |
| Inkrement | OS3.0.304.0.WPSJPXM (JP global) | post-build-incremental |
| Android | 16, SDK 36 | post-sdk-level=36 |
| Sicherheitspatch | 2026-05-01 | post-security-patch-level |
| OTA-Typ | A/B (payload.bin, CrAU, 39 Partitionen) | ota-type=AB |
| SoC | MediaTek MT6993 (Dimensity 9500) | mediatek,mt6993-*-Compatibles im vendor_boot-DTB; cmdline bootopt=64S3,32N2,64N2 |
| Kernel | 6.12.38-android16-5-g1d46253471dd-ab15048002-4k, clang 19.0.1 | Banner aus dem extrahierten boot.img gelesen |
| Boot-Image | Header v4, Kernel 18,898,125 B, LZ4-Legacy-komprimiert, kein Ramdisk | tools/bootinfo.py |
PRELOADER= | Preloader auf dem Telefon | Zielverzeichnis | Status |
|---|
retail (Standard) | das ausgelieferte preloader_<device>.bin, byteidentisch mit dem preloader_raw.img der Fastboot-ROM | targets/warhol-OS3.0.304.0.WPSJPXM/ | ✅ am 2026-07-26 auf dem Gerät verifiziert |
eng | das Engineering-Build, preloader_<device>_eng.bin | targets/warhol-OS3.0.304.0.WPSJPXM-eng/ | ✅ am 2026-07-26 auf dem Gerät verifiziert |
| Datei | Änderung | Grund |
|---|
src/util.c | kernelsnitch_setup(..., mte_enabled=1) | bei 0 versucht die mm_struct-Suche nur ungetaggte Kandidaten und kann nie einen getaggten Zeiger erreichen — ein struktureller Fehlschlag, kein Pech |
src/util.c | is_kernel_ptr() / is_direct_ptr() untaggen vor der Bereichsprüfung | ein aus dem Kernelspeicher gelesener getaggter Zeiger würde sonst als nicht-in-linear-map abgelehnt (direct-entry-fatal reason=bad-task-or-cpu) |
src/kernelsnitch/kernelsnitch.h | Tag-Sweep < 15 → < 16 | Tag 0xf ist der ungetaggte/Match-All-Zeiger, daher deckt der Sweep jetzt auch einen Kernel ohne MTE ab — mte_enabled=1 ist in beiden Fällen sicher |
| Repository | Rolle hier |
|---|
| https://github.com/MobiusM/CVE-2026-43499 | Original-CVE-2026-43499-PoC / Crash-Trigger |
| https://github.com/x-spy/CVE-2026-43499-popsicle | Basis dieses Ports. Xiaomi 17 Pro Max (popsicle), Kernel 6.12.23-android16-5; source/ und tools/generate_target.py stammen von hier |
| https://github.com/Kananosa/CVE-2026-43499-For-Xiaomi-17T-chagall | Nächstverwandtes Gerät (Xiaomi 17T, chagall, ebenfalls MediaTek). Von Popsicle abgeleitet, enthält ein vorgebautes preload.so — dessen einkompilierte Konstanten bestätigten den physischen Lade-Offset |
| https://github.com/MiCode/Xiaomi_Kernel_OpenSource | Xiaomi-Kernelquellen zum Abgleich des Strukturlayouts |