
Exploit de escalada local de privilegios (LPE) para Android de CVE-2026-43499 dirigido al OPPO PMG110 (kernel 6.6). Utiliza UAF de futex PI para obtener root e instalar un daemon su mediante LD_PRELOAD.
CVE-2026-43499 (use-after-free de rt_mutex_waiter en futex PI), escalada de privilegios local, portada al OPPO PMG110 / K15 Pro+ — MediaTek MT6991, ColorOS 16.
Un solo archivo enviado, ejecutado mediante 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
En caso de éxito, queda un su persistente:
adb shell /data/local/tmp/su -c id # uid=0(root)
Verificado en el dispositivo (2026-07-27): uid=0 en unos 35 segundos desde una ejecución simple sin anular variables de entorno, y su respondiendo después desde una adb shell normal y sin privilegios:
$ 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
El exploit y la instalación de su están verificados en este dispositivo. Lo que esa shell con root leyó después también confirma P0_KERNEL_PHYS_LOAD, los offsets de símbolos y KS_MTE_TAGGED=0 independientemente del exploit — ver targets/pmg110-16.0.9.400/NOTES.md.
Ejecuta Write 1 (SELinux permisivo) y Write 2 (cred → init_cred), consigue que un proceso hijo tenga uid=0 y desde ahí instala el demonio su integrado.
su no es un segundo artefacto: su_daemon.c se compila como un PIE aarch64 independiente y se incrusta con .incbin en el .rodata de la librería, de modo que viaja dentro de preload.so y se vuelve a escribir en tiempo de ejecución. La vía de warhol-root, sin cambios.ksud, sin KernelSUsu se instala en tres lugares, porque uno de ellos es al que llegarás realmente:
El demonio escucha en /data/local/tmp/temp_su.sock y registra en /data/local/tmp/su_daemon.log. El root no persiste entre reinicios: vuelve a ejecutar la línea de LD_PRELOAD después de cada arranque.
Para instalar KernelSU, usa en su lugar la vía /data/local/tmp/a/e de ghostlock-oneplus.
Todo excepto el núcleo del exploit es de warhol-root, tomado en lugar de reinventado:
targets/<device>/, preparadas en source/src/ en tiempo de compilación, de modo que cambiar DEVICE nunca pueda dejar atrás las cabeceras del dispositivo anteriorsource/Makefile (NDK si lo hay, clang del host contra el sysroot del NDK si no) y la regla de incrustación en dos etapas que produce build/embed/su_daemon_aarch64_pie antes de enlazar el .sosu_daemon.c y su_blob.S son byte-idénticos a los de warhol-root, y su_install.c es su instalador preload.cEl núcleo del exploit no es de warhol-root. warhol-root es popsicle, que está fijado a GKI 6.12 / android16 y cuyo generate_target.py rechaza cualquier otro banner. El PMG110 es 6.6 / android15, así que el núcleo aquí es el árbol 6.6 de ghostlock — descendiente a su vez del mismo código (kernelsnitch/utils.h y timeutils.h son byte-idénticos entre los dos repos), desarrollado más a fondo.
Cada línea del exploit en sí — Write 1, Write 2, KernelSnitch, la vía pselect — es el mismo código en ambos árboles.
Esta es la única diferencia estructural, y viene impuesta porque los dos árboles obtienen root de formas distintas.
warhol-root obtiene root en el propio proceso del exploit y por eso llama a install_embedded_su() directamente desde run_direct_root(). Aquí Write 2 intercambia el puntero cred de un hijo bifurcado y el padre sigue siendo el llamador sin privilegios, así que el hijo en child_main() es el único contexto que puede hacer la instalación — ahí es donde se ejecuta.
Ambos árboles llevan el mismo stub débil install_embedded_su() en util.c que devuelve ENOSYS; proporcionar la definición fuerte es lo que activa la vía. Conviene saberlo porque una compilación que por lo que sea deje fuera su_install.c igualmente enlaza y se ejecuta — solo informa de su=0/38 y no instala nada.
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 cadena de herramientas se encuentra sola: primero ANDROID_NDK_HOME / ANDROID_NDK_ROOT, luego las ubicaciones habituales de instalación del NDK para Linux y macOS y, si todas fallan, clang del host apuntando al sysroot del NDK. Define ANDROID_NDK_HOME solo para anular la búsqueda. make info imprime lo que eligió.
La compilación es de dos etapas, que es la parte que conviene saber:
su_daemon.c → build/embed/su_daemon_aarch64_pie, un PIE aarch64 independientesu_blob.S incrusta ese binario con .incbin en .rodata, y todo el conjunto se enlaza en el único preload.soAsí que make clean y una recompilación son la única forma de cambiar el su integrado — basta con editar su_daemon.c, la dependencia está declarada, pero el blob es un artefacto de compilación y no está bajo seguimiento.
targets/<device>/{target.h,device_offsets.h} se vuelven a preparar en source/src/ en cada compilación, de modo que una cabecera obsoleta de otro dispositivo no puede recogerse silenciosamente.
out/*.so no está bajo seguimiento (misma convención que warhol-root) — clona y ejecuta make.
El .so se compila con -fvisibility=hidden y exporta cero símbolos. Una librería LD_PRELOAD gana la resolución de símbolos para todo el proceso, así que cualquier cosa que exportara podría hacer sombra a un símbolo homónimo en el binario anfitrión o en libc. Ese indicador solo gobierna la generación de código C, así que su_blob.S marca sus dos símbolos como .hidden a mano — sin esas líneas, los límites del blob serían lo único que la librería seguiría exportando.
Ninguna de estas fue necesaria en la ejecución verificada.
[*] 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 es el exploit teniendo éxito; todo lo que va después es la instalación. Ambos se informan por separado a propósito, y también el veredicto:
El del medio es la distinción que merece la pena tener: dice que los offsets de target.h son correctos para esta compilación y que el problema está en algún punto de la instalación, que es algo completamente distinto de depurar. su=0/38 (ENOSYS) en su interior significa concretamente que se enlazó el stub débil.
mm_struct leak failed seguido de prepare_kernel_page retry N/24 no es un fallo. Es el progreso del bucle, y la ejecución exitosa también lo muestra. Nada ha fallado hasta que se agotan los 24 intentos y aparece prepare_kernel_page timeout. Del mismo modo, probing cfi ... expected=9 con child uid = 2000 es una ronda fallida de diez.
No juzgues una ejecución por un registro truncado — ese error costó una ronda completa de diagnóstico erróneo aquí.
Una ejecución completamente fallida también es normal. La condición de carrera de pselect no es 100% fiable: una ejecución puede perderla cinco veces seguidas y terminar en Write 1 failed, y la siguiente la gana al primer intento con ret=9. Observado en este dispositivo. ret=4 expected=9 es lo que parece perder la carrera, no un target.h equivocado — un fallo no es motivo para volver a derivar los offsets. Vuelve a ejecutarla.
Solo para investigación de seguridad autorizada y fines educativos.
| Dispositivo | OPPO PMG110 / K15 Pro+ / OP61E5L1 |
| SoC | MediaTek MT6991 (Dimensity 9500s) |
| Kernel | 6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, páginas de 4K) |
| Compilación | ColorOS 16 / PMG110_16.0.9.400(CN01) — mismos bytes de kernel que 16.0.8.300 |
| Bug | CVE-2026-43499, sin corregir en esta imagen (demostrado por desensamblado, no por versión) |
| Ruta | Por qué |
|---|
/apex/com.android.virt/bin/su | en un tmpfs montado sobre ese directorio; en el PATH de una shell con root |
/data/local/tmp/su | accesible desde un adb shell normal sin juegos de PATH |
/apex/com.android.virt/bin/su en el mount namespace de adbd | instalado mediante setns, de modo que una nueva adb shell lo ve |
| Archivo | Relación |
|---|
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/* | de ghostlock, byte-idénticos |
su_daemon.c su_blob.S | de warhol-root, byte-idénticos (su_blob.S añade dos líneas .hidden — ver Compilación) |
su_install.c | el instalador preload.c de warhol-root, movido a su propio archivo porque el preload.c de este árbol ya tiene otra función |
main.c | de ghostlock, más la llamada a su en el hijo con root y el informe de resultados |
preload.c | solo aquí — el constructor y el registro de doble destino |
offsets.h | solo la definición de la struct; la entrada se prepara desde targets/<device>/device_offsets.h |
| Variable | Efecto |
|---|
GHOSTLOCK_LOG | destino del registro (por defecto /data/local/tmp/.ghostlock.log); la salida va a stdout y al archivo |
GHOSTLOCK_KS_VERBOSE=1 | imprime las direcciones de colisión y los rangos de barrido de KernelSnitch |
GHOSTLOCK_KS_THRESHOLD=<n> | anula el multiplicador del umbral de colisión |
GHOSTLOCK_MTE=1 | barre también las etiquetas de puntero del kernel (15 veces más lento) |
GHOSTLOCK_PHYS_LOAD=0x... | anula la dirección de carga física del kernel |
PSELECT_SHIFT=<n> | anula el desplazamiento de la superposición de pila (reemplaza, no suma) |
| Veredicto | Significado |
|---|
EXPLOIT OK | root, y su responde |
EXPLOIT OK, SU INSTALL FAILED | Write 1 y Write 2 acertaron; solo falló la instalación |
EXPLOIT FAILED | las escrituras no acertaron |
ABORTED | la ejecución murió antes de poder informar — lee la última línea [!] |
| Ruta | Contenido |
|---|
source/src/preload.c | constructor: ejecuta el exploit, informa, se detiene |
source/src/main.c | el exploit en sí (Write 1 / Write 2) |
source/src/su_daemon.c | el binario su — compilado de forma independiente como PIE aarch64, no enlazado en el .so |
source/src/su_blob.S | incrusta ese PIE con .incbin en el .rodata del .so |
source/src/su_install.c | escribe el blob de vuelta, arranca el demonio y lo comprueba |
source/src/target.h | destino de preparación (ignorado por git) |
targets/<device>/target.h | estructura en tiempo de compilación: offsets de structs, constantes de physmap, formas de slab y futex |
targets/<device>/device_offsets.h | offsets de símbolos globales de kallsyms |
tools/extract_device.py | boot.img → offsets, campos de struct BTF, resultado de la superposición pselect |
tools/preloader_memlayout.py | MediaTek preloader → P0_KERNEL_PHYS_LOAD |
tools/qemu_verify.py | arranca el kernel bajo QEMU: mide la superposición de pila y comprueba la estabilidad del mapa lineal |
tools/device_probe.sh | comprobación previa desde una adb shell sin privilegios |