
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.
| 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) |
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:
| 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 |
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.
| 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 |
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.