
Android-LPE-Exploit für CVE-2026-43499 gegen das OPPO PMG110 (Kernel 6.6). Nutzt futex PI UAF aus, um Root-Rechte zu erlangen und über LD_PRELOAD einen su-Daemon zu installieren.
CVE-2026-43499 (futex PI rt_mutex_waiter use-after-free) lokale Privilegieneskalation, portiert auf das OPPO PMG110 / K15 Pro+ — MediaTek MT6991, ColorOS 16.
Eine Datei übertragen, ausgeführt über LD_PRELOAD:
adb push out/preload-pmg110-16.0.9.400.so /data/local/tmp/preload.so
adb shell chmod 644 /data/local/tmp/preload.so
adb shell LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true
Bei Erfolg bleibt ein persistentes su zurück:
adb shell /data/local/tmp/su -c id # uid=0(root)
Auf dem Gerät verifiziert (2026-07-27): uid=0 in etwa 35 Sekunden bei einem reinen Lauf ohne Umgebungsüberschreibungen, und su antwortet anschließend aus einer gewöhnlichen unprivilegierten adb shell:
$ adb shell "/data/local/tmp/su -c 'echo 0 > /proc/sys/kernel/kptr_restrict'"
$ adb shell "/data/local/tmp/su -c 'grep -w init_task /proc/kallsyms'"
ffffffe89033e780 D init_task
Der Exploit und die su-Installation sind beide auf diesem Gerät verifiziert. Was diese gerootete Shell anschließend zurücklas, bestätigt außerdem P0_KERNEL_PHYS_LOAD, die Symbol-Offsets und KS_MTE_TAGGED=0 unabhängig vom Exploit — siehe targets/pmg110-16.0.9.400/NOTES.md.
| Gerät | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991 (Dimensity 9500s) |
| Kernel | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, 4K-Seiten) |
| Build | ColorOS 16 / PMG110_16.0.9.400(CN01) — gleiche Kernel-Bytes wie 16.0.8.300 |
| Bug | CVE-2026-43499, nicht gepatcht in diesem Image (per Disassemblierung belegt, nicht durch die Version) |
Es führt Write 1 (SELinux permissive) und Write 2 (cred → init_cred) aus, bringt einen Kindprozess auf uid=0 und installiert von dort den eingebetteten su-Daemon.
su ist kein zweites Artefakt: su_daemon.c wird als eigenständiges aarch64-PIE gebaut und per .incbin in das .rodata der Bibliothek eingebettet, sodass es in preload.so mitfährt und zur Laufzeit wieder herausgeschrieben wird. Der Weg von warhol-root, unverändert.ksud, kein KernelSUsu wird an drei Orten installiert, weil einer davon derjenige ist, den du tatsächlich erreichst:
| Pfad | Warum |
|---|---|
/apex/com.android.virt/bin/su | auf einem tmpfs, das über dieses Verzeichnis gemountet ist; auf dem PATH einer Root-Shell |
/data/local/tmp/su | aus einer einfachen adb shell ohne PATH-Spielereien erreichbar |
/apex/com.android.virt/bin/su im Mount-Namespace von adbd | über setns installiert, sodass eine neue adb shell es sieht |
Der Daemon lauscht auf /data/local/tmp/temp_su.sock und protokolliert nach /data/local/tmp/su_daemon.log. Root bleibt über einen Neustart hinweg nicht bestehen — führe die LD_PRELOAD-Zeile nach jedem Boot erneut aus.
Für die KernelSU-Installation verwende stattdessen den /data/local/tmp/a/e-Weg in ghostlock-oneplus.
Alles außer dem Exploit-Kern stammt von warhol-root, übernommen statt neu erfunden:
targets/<device>/, zur Build-Zeit nach source/src/ gestagt, sodass ein Wechsel von DEVICE niemals die Header des vorherigen Geräts zurücklassen kannsource/Makefile (NDK, falls vorhanden, sonst Host-clang gegen den NDK-Sysroot) sowie die zweistufige Embed-Regel, die vor dem Linken der .so das build/embed/su_daemon_aarch64_pie erzeugtsu_daemon.c und su_blob.S sind byte-identisch mit denen von warhol-root, und su_install.c ist dessen preload.c-InstallerDer Exploit-Kern stammt nicht von warhol-root. warhol-root ist popsicle, das an GKI 6.12 / android16 gebunden ist und dessen generate_target.py jedes andere Banner ablehnt. PMG110 läuft auf 6.6 / android15, daher ist der Kern hier der ghostlock-6.6-Baum — selbst ein Abkömmling desselben Codes (kernelsnitch/utils.h und timeutils.h sind zwischen den beiden Repos byte-identisch), weiterentwickelt.
| Datei | Beziehung |
|---|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | von ghostlock, byte-identisch |
su_daemon.c su_blob.S | von warhol-root, byte-identisch (su_blob.S fügt zwei .hidden-Zeilen hinzu — siehe Build) |
su_install.c | warhol-roots preload.c-Installer, in eine eigene Datei verschoben, weil das preload.c dieses Baums bereits eine andere Aufgabe hat |
main.c | von ghostlock, plus der su-Aufruf im gerooteten Kind und die Ergebnisberichterstattung |
preload.c | nur hier — der Konstruktor und das Dual-Sink-Log |
offsets.h | nur die Struct-Definition; der Eintrag wird aus targets/<device>/device_offsets.h gestagt |
Jede Zeile des eigentlichen Exploits — Write 1, Write 2, KernelSnitch, der pselect-Weg — ist in beiden Bäumen derselbe Code.
Das ist der eine strukturelle Unterschied, und er ist dadurch erzwungen, dass die beiden Bäume auf unterschiedliche Weise Root erlangen.
warhol-root rootet den Exploit-Prozess selbst und ruft install_embedded_su() daher direkt aus run_direct_root() auf. Hier tauscht Write 2 den cred-Zeiger eines geforkten Kindes, und der Parent bleibt der unprivilegierte Aufrufer, sodass das Kind in child_main() der einzige Kontext ist, der die Installation durchführen kann — dort läuft sie.
Beide Bäume tragen denselben schwachen install_embedded_su()-Stub in util.c, der ENOSYS zurückgibt; die starke Definition bereitzustellen, ist das, was den Weg aktiviert. Das ist wissenswert, denn ein Build, der su_install.c irgendwie weglässt, linkt und läuft weiterhin — er meldet nur su=0/38 und installiert nichts.
make # = make preload -> out/preload-<DEVICE>.so
make DEVICE=<name> # use targets/<name>/
make devices # list available DEVICE values
make info # show the selected target and the resolved toolchain
Die Toolchain wird von selbst gefunden: zuerst ANDROID_NDK_HOME / ANDROID_NDK_ROOT, dann die üblichen NDK-Installationsorte für Linux und macOS, und falls all das fehlschlägt, Host-clang gegen den NDK-Sysroot. Setze ANDROID_NDK_HOME nur, um die Suche zu überschreiben. make info gibt aus, was gewählt wurde.
Der Build besteht aus zwei Stufen, was der Teil ist, den man kennen sollte:
su_daemon.c → build/embed/su_daemon_aarch64_pie, ein eigenständiges aarch64-PIEsu_blob.S bettet diese Binärdatei per .incbin in .rodata ein, und das Ganze wird zu der einen preload.so gelinktEin make clean und ein Rebuild sind also der einzige Weg, das eingebettete su zu ändern — su_daemon.c allein zu bearbeiten genügt, die Abhängigkeit ist deklariert, aber der Blob ist ein Build-Artefakt und wird nicht versioniert.
targets/<device>/{target.h,device_offsets.h} werden bei jedem Build erneut nach source/src/ gestagt, sodass ein veralteter Header eines anderen Geräts nicht stillschweigend übernommen werden kann.
out/*.so wird nicht versioniert (gleiche Konvention wie warhol-root) — klonen und make.