
Exploit LPE per Android per CVE-2026-43499 che ha come target OPPO PMG110 (kernel 6.6). Utilizza futex PI UAF per ottenere root e installare un daemon su tramite LD_PRELOAD.
CVE-2026-43499 (use-after-free di rt_mutex_waiter in futex PI) escalation dei privilegi locale, portata su OPPO PMG110 / K15 Pro+ — MediaTek MT6991, ColorOS 16.
Un solo file trasferito, eseguito tramite 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
In caso di successo viene lasciato un su persistente:
adb shell /data/local/tmp/su -c id # uid=0(root)
Verificato sul dispositivo (2026-07-27): uid=0 in circa 35 secondi da un'esecuzione semplice senza override delle variabili d'ambiente, e su risponde successivamente da una normale adb shell non privilegiata:
$ 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
Sia l'exploit sia l'installazione di su sono verificati su questo dispositivo. Ciò che quella shell con privilegi di root ha poi letto conferma anche P0_KERNEL_PHYS_LOAD, gli offset dei simboli e KS_MTE_TAGGED=0 indipendentemente dall'exploit — vedi targets/pmg110-16.0.9.400/NOTES.md.
| Device | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991 (Dimensity 9500s) |
| Kernel | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, 4K pages) |
| Build | ColorOS 16 / PMG110_16.0.9.400(CN01) — stessi byte del kernel di 16.0.8.300 |
| Bug | CVE-2026-43499, non corretto in questa immagine (mostrato dal disassembly, non dalla versione) |
Esegue Write 1 (SELinux permissive) e Write 2 (cred → init_cred), porta un processo figlio a uid=0 e da lì installa il demone su incorporato.
su non è un secondo artefatto: su_daemon.c è compilato come PIE aarch64 autonomo e incluso con .incbin nel .rodata della libreria, quindi viaggia dentro preload.so e viene riscritto su file in fase di esecuzione. È la via di warhol-root, invariata.ksud, niente KernelSUsu viene installato in tre posti, perché uno di questi è quello a cui accederai davvero:
| Path | Why |
|---|---|
/apex/com.android.virt/bin/su | su un tmpfs montato sopra quella directory; nel PATH di una shell root |
/data/local/tmp/su | raggiungibile da una semplice adb shell senza giochi di PATH |
/apex/com.android.virt/bin/su nel mount namespace di adbd | installato via setns, quindi una nuova adb shell lo vede |
Il demone ascolta su /data/local/tmp/temp_su.sock e scrive il log in /data/local/tmp/su_daemon.log. Root non è persistente tra i riavvii — rilancia la riga LD_PRELOAD dopo ogni avvio.
Per l'installazione di KernelSU, usa invece la via /data/local/tmp/a/e in ghostlock-oneplus.
Tutto tranne il nucleo dell'exploit è di warhol-root, preso invece che reinventato:
targets/<device>/, copiati in source/src/ in fase di build, così cambiando DEVICE non si possono mai lasciare indietro gli header del dispositivo precedentesource/Makefile (NDK se presente, altrimenti clang dell'host contro il sysroot NDK) e la regola di embedding in due fasi che produce build/embed/su_daemon_aarch64_pie prima di linkare il .sosu_daemon.c e su_blob.S sono byte-identici a quelli di warhol-root, e su_install.c è il suo installer preload.cIl nucleo dell'exploit non è di warhol-root. warhol-root è popsicle, ancorato a GKI 6.12 / android16 e il suo generate_target.py rifiuta qualsiasi altro banner. PMG110 è 6.6 / android15, quindi il nucleo qui è l'albero ghostlock 6.6 — a sua volta discendente dello stesso codice (kernelsnitch/utils.h e timeutils.h sono byte-identici tra i due repository), ulteriormente sviluppato.
| File | Relationship |
|---|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | di ghostlock, byte-identici |
su_daemon.c su_blob.S | di warhol-root, byte-identici (su_blob.S aggiunge due righe .hidden — vedi Build) |
su_install.c | l'installer preload.c di warhol-root, spostato in un file proprio perché il preload.c di questo albero ha già un compito diverso |
main.c | di ghostlock, più la chiamata a su nel figlio con root e la segnalazione del risultato |
preload.c | solo qui — il costruttore e il log a doppia destinazione |
offsets.h | solo definizione della struct; la voce è copiata da targets/<device>/device_offsets.h |
Ogni riga dell'exploit vero e proprio — Write 1, Write 2, KernelSnitch, la via pselect — è lo stesso codice in entrambi gli alberi.
Questa è l'unica differenza strutturale, ed è imposta dal fatto che i due alberi ottengono root in forme diverse.
warhol-root porta a root il processo dell'exploit stesso e quindi chiama install_embedded_su() direttamente da run_direct_root(). Qui Write 2 sostituisce il puntatore cred di un figlio forkato e il padre resta il chiamante non privilegiato, quindi il figlio in child_main() è l'unico contesto che può fare l'installazione — ed è lì che viene eseguita.
Entrambi gli alberi hanno lo stesso stub weak install_embedded_su() in util.c che restituisce ENOSYS; fornire la definizione strong è ciò che attiva la via. Vale la pena saperlo perché una build che per qualche motivo esclude su_install.c viene comunque linkata e funziona — riporta solo su=0/38 e non installa nulla.
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
La toolchain viene trovata da sola: prima ANDROID_NDK_HOME / ANDROID_NDK_ROOT, poi le solite posizioni di installazione di NDK per Linux e macOS e, se tutte falliscono, clang dell'host con target sul sysroot NDK. Imposta ANDROID_NDK_HOME solo per sovrascrivere la ricerca. make info stampa cosa ha scelto.
La build è in due fasi, ed è la parte che vale la pena conoscere:
su_daemon.c → build/embed/su_daemon_aarch64_pie, un PIE aarch64 autonomosu_blob.S include con .incbin quel binario in .rodata, e il tutto viene linkato nell'unico preload.soQuindi make clean e una rebuild sono l'unico modo per cambiare il su incorporato — modificare il solo su_daemon.c è sufficiente, la dipendenza è dichiarata, ma il blob è un artefatto di build e non è tracciato.
targets/<device>/{target.h,device_offsets.h} vengono ricopiati in source/src/ a ogni build, così un header obsoleto di un altro dispositivo non può essere raccolto silenziosamente.
out/*.so non è tracciato (stessa convenzione di warhol-root) — clona e fai make.
Il .so è compilato con -fvisibility=hidden ed esporta zero simboli. Una libreria LD_PRELOAD vince la risoluzione dei simboli per l'intero processo, quindi qualsiasi cosa esportata potrebbe oscurare un simbolo con lo stesso nome nel binario host o in libc. Quel flag governa solo la generazione di codice C, quindi su_blob.S marca i suoi due simboli come .hidden a mano — senza quelle righe i limiti del blob sarebbero l'unica cosa che la libreria esporterebbe ancora.