
Exploit del kernel Android GKI 6.12 para CVE-2026-43499, que encadena un fallo de reversión de rt_mutex con una sobrescritura de pila en pselect para obtener root en dispositivos Samsung y Pixel.
Un exploit del kernel de Linux para la línea GKI 6.12 de Android (dispositivos
Samsung y Pixel) dirigido a CVE-2026-43499: un rollback defectuoso de
remove_waiter() en rt_mutex_start_proxy_lock() que usa current en lugar de
waiter::task, dejando el pi_blocked_on de un waiter apuntando a su
rt_mutex_waiter de la pila del kernel (que luego se saca). Combinado con una
sobrescritura de la pila del fd_set de pselect() y un recorrido falso de
rb_tree impulsado por sched_setattr del consumidor, produce una primitiva de
escritura en el kernel determinista y root completo.
El exploit se ejecuta como una biblioteca compartida LD_PRELOAD (preload.so)
e instala un demonio su root más un fondo de pantalla como artefactos
posteriores al root.
Estado: desarrollo activo. El objetivo m1q (ZF1) es el foco de la puesta en marcha. El bootstrap pipei tmp_page-uname es la ruta activa actual: el recorrido está probado limpio en el dispositivo (build #33: regiones rt_mutex sembradas por hijo) y el barrido completo de 168 candidatos está en curso. La ruta configfs CFI es un callejón sin salida en m1q (Rust ashmem — sin slot fops inyectable) y sigue siendo la ruta para objetivos C-ashmem. Los offsets por objetivo varían según el dispositivo — verifíquelos contra el binario real del kernel antes de confiar en ellos.
Cuando FUTEX_CMP_REQUEUE_PI reencola un waiter y el recorrido de la cadena de
detección de deadlock del reencolado devuelve -EDEADLK,
__rt_mutex_start_proxy_lock() hace rollback mediante remove_waiter()
(rtmutex.c:1535). El rollback desencola correctamente el waiter del árbol de
espera pero limpia el pi_blocked_on del llamador del reencolado en lugar
de waiter->task->pi_blocked_on. El pi_blocked_on del WAITER queda colgando
en su rt_mutex_waiter de la pila, que se saca una vez que el futex expira.
Tras el timeout, la región de la pila del kernel del waiter se reutiliza:
core_sys_select() copia los tres fd_sets en ese búfer de pila (la ruta de pila
nfds < 344 en ZF1). Un array de palabras fd_set manipulado vuelve a
materializar un rt_mutex_waiter falso / task falsa / rt_mutex falso (con una
raíz de rb_tree cuyos punteros están controlados). Un hilo consumidor entonces
llama a sched_setattr_tid(waiter) → rt_mutex_adjust_pi() → rb_erase_cached,
lo que produce una escritura en dirección arbitraria de un valor controlado.
Toda la primitiva requiere el ciclo de la cadena PI: el propietario mantiene
f_pi_target y también se bloquea en f_pi_chain (mantenido por el waiter), de
modo que el recorrido de la cadena llega a owner → chain → waiter → target →
owner y falla con -EDEADLK. Eliminar el ciclo hace que el reencolado devuelva
éxito con efecto nulo en el kernel.
sched_blocked_reason (primaria en m1q, probada en
dispositivo): el PC de retorno guardado de un kworker bloqueado
(stack_trace_save_tsk) se lee del ring buffer y se compara con el offset
de worker_thread compilado. Se ejecuta antes de usar cualquier palabra de
la ruta boot_id para que los objetivos de escritura de alias de datos
(data_addr() = alias p0 + slide_p0_offset) sigan siendo correctos con
un slide distinto de cero.SLIDE_LOGGERS_0_1 en los datos del sysctl boot_id mediante
un alias de mapa lineal; el valor filtrado reconstruye stext.
Aparcada en m1q: su objetivo W1 (SLIDE_RANDOM_BOOT_ID_DATA_OFF, RAM
física baja) tiene escriturabilidad dependiente de la página — el slide
mueve el objetivo a través de los límites de página en cada arranque, por
lo que ~6/7 ejecuciones fallan. Solo se ejercita explícitamente mediante
SLIDE_FORCE_BOOTID (prueba de mecanismo) con tree_pc/tree_left
redirigidos a la página rociada.Las palabras fd_set de pselect vuelven a materializar un rt_mutex_waiter falso
en la pila del kernel del waiter; el sched_setattr_tid(waiter) de un hilo
consumidor lo recorre mediante rt_mutex_adjust_pi(). Mapa de palabras ZF1
(verificado por disasm): PSELECT_WAITER_WORD_SHIFT = 0, palabra 12 = task
(@+0x50), palabra 13 = lock (@+0x58), palabra 14 = wake_state (@+0x60 = 3).
Con un rt_mutex falso sembrado el recorrido se completa limpiamente: las
rb_erase W1 *(tree_left)=tree_pc / W2 *(tree_pc&~3+8)=tree_left de [7] son
las únicas escrituras en el kernel; [11] (setprio / dequeue_pi) y el wake de [9]
se omiten ambos (el waiter falso local con prio 100 mantiene el nodo de pila
fuera del top-waiter). Cada finalización que entra al recorrido desde el build
#19 sobrevive en el dispositivo; los fallos deterministas anteriores eran un
error de colocación de payload de 8 bytes (SKB_DATA_DELTA), no un fallo del
cuerpo del recorrido.
Fase opcional STAGE3=1 que bifurca un hijo puente, cambia su mm->pgd a
una tabla de páginas falsa preparada (3 niveles: PGD→L1→L2, hoja de bucle RX +
hoja de pila RW), y deja que el hijo ejecute un blob de ensamblador solo de
registros que parchea su propio cred a través de una ventana de escaneo físico
de 2MB — lectura/escritura física de toda la RAM sin las primitivas
configfs/pipe. Actualmente solo está conectada al objetivo m1q y no se ha
ejecutado en dispositivo.
41 objetivos en src/targets/<codename>-<build>/, cada uno requiere como mínimo
un target.h (offsets de símbolos del kernel, KIMAGE_TEXT_BASE, base del mapa
directo, offsets de structs). Los objetivos Pixel (comet, tokay, tegu,
caiman, komodo, frankel, mustang, rango, stallion, blazer)
sobrescriben fuentes compartidas; los objetivos Samsung (m1q-*) añaden lógica
específica del dispositivo.
make list-projects # full list
Los diseños de structs del kernel difieren por dispositivo — los offsets deben verificarse contra el binario real del kernel / kallsyms para cada objetivo.
Salida: build/<PROJECT>/bin/preload.so (biblioteca compartida cargada vía
LD_PRELOAD).
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
El build embebe un binario PIE su_daemon (src/su_daemon.c, compilado a
build/embed/su_daemon_aarch64_pie) y assets/wallpaper.webp en preload.so
mediante src/su_blob.S / src/wallpaper_blob.S.
Empuje las salidas del build a /data/local/tmp, luego ejecute el exploit bajo
LD_PRELOAD. Ejecute SIN tee en un objetivo de puesta en marcha en vivo — tee
almacena en búfer su propio stdio y pierde la cola del log en un pánico del
kernel (la salida estándar del exploit no tiene búfer, por lo que redirigir
directamente a un archivo preserva cada línea):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
En caso de éxito el demonio escucha en /data/local/tmp/temp_su.sock y su se
instala en /apex/com.android.virt/bin.
tools/run_pipei.sh empuja el binario, verifica su sha256 en el dispositivo
(un preload.so obsoleto produce veredictos silenciosamente ilegibles — la
escalera una vez ejecutó un build < #22 cuyo formato de log difiere), luego
ejecuta el barrido completo de 168 candidatos dividido en 3x56 para que un
pánico pierda como máximo un fragmento (~10 min). La salida estándar del
exploit se transmite directamente a través de adb (sin redirección en el
dispositivo — un tee del lado del dispositivo pierde la cola en un pánico); se
mantiene una copia en el host de cada fragmento en
/tmp/m1q_pipei_chunk{1..3}_stream.log:
sh tools/run_pipei.sh
PASS = cualquier fragmento muestra tmp_page uname changed + after uname='CatOS. PIPEI_CHILD_REGIONS debe establecerse al menos al tamaño del
fragmento o las regiones rt_mutex por hijo permanecen sin sembrar y el barrido
falla en el candidato 0. La cobertura completa también requiere
TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 — sin él solo se barre una página del
bloque mm liberado de 8 páginas. run_pipei_slim.sh es la variante de
diagnóstico de un solo candidato; run_m1q_ladder.sh es la batería de
recorrido/aislamiento (oráculo B16-2, mecanismo de slide, pasos del barrido).
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
Los objetivos sobrescriben fuentes compartidas mediante el mecanismo pick_src
del Makefile: si existe src/targets/<PROJECT>/<file>.c se usa en lugar del
src/<file>.c compartido; de lo contrario se usa la versión compartida.
-EDEADLK (errno 35). Un ciclo ausente se
manifiesta como success=1 con efecto nulo en el kernel.PSELECT_WAITER_WORD_SHIFT debe ser 0 (profundidades de pila ZF1
verificadas desde el ELF). Otros objetivos aún usan 1 por defecto — verifique
por dispositivo antes de confiar en ellos.p0_data_alias() usa DIRECT_MAP_BASE (el mapa lineal), que difiere de
P0_PAGE_OFFSET en ZF1. Los alias de datos para objetos del kernel
desreferenciados deben usar el mapa lineal.pr_error() es fatal (exit(-1)), a diferencia de las macros de
advertencia/información.SLIDE_LOGGERS_0_1, no
SLIDE_NFULNL_LOGGER; la palabra es un alias de mapa lineal no
aleatorizado, por lo que la ruta boot_id es independiente del slide (úsala
solo con slide=0 — ejecuta primero la fuga de tracefs).Este es código de exploit para dispositivos Android y está destinado a la investigación de seguridad, pruebas de vulnerabilidad autorizadas y trabajo de defensa en hardware que usted posee o tiene permiso explícito de probar. El uso indebido en dispositivos que no posee es ilegal. El autor no es responsable de dispositivos inutilizados, kernels inutilizados o garantías anuladas.
mm_structmm_structwrite_iter inyectable y el objeto de heap
ASHMEM_SET_NAME es un KVec cuyo diseño no se alinea con el private_data de
configfs_bin_write_iter. m1q pivota a una escritura kmalloc en su lugar:
reclamar el bloque de orden 3 de mm filtrado como objetos pipe_inode_info y
usar la escritura pselect W1 (*(tree_left) = tree_pc) para sobrescribir el
slot tmp_page de un candidato (+0x90) con la página del namespace UTS
(init_uts_ns). Una escritura de abanico planta un nombre marcador en el
offset sysname y uname() informa "CatOS" — una escritura en el kernel
verificable sin dependencia de objetos estáticos. Los objetivos C-ashmem
mantienen la ruta configfs.preload.c instala el demonio su embebido (montado en tmpfs en
/apex/com.android.virt/bin, más variantes de namespace adbd y locales) y
cambia el fondo de pantalla.| Variable | Por defecto | Propósito |
|---|
PSELECT_ROUTE_SHIFT | tiempo de compilación | Desplazamiento de palabra fd_set A/B para la ruta principal (m1q) |
SLIDE_SHIFT | tiempo de compilación | Desplazamiento de palabra fd_set A/B para la ruta de slide |
PSELECT_ROUTE_DELAY_USEC | 50000 | Retardo del consumidor antes de sched_setattr (debe ser > 0) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Sincronización/fijación de la ruta de slide |
SKIP_SLIDE | 0 | Usar respaldo de mapa directo (slide = 0) |
SLIDE_ONLY | 0 | Ejecutar solo el slide de KASLR y salir |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | Sobrescribir offsets de alias p0 / boot_id del slide |
SLIDE_FORCE_BOOTID | 0 | Forzar la ruta boot_id pselect tras el slide de tracefs (prueba de mecanismo) |
KSNITCH_COLLISIONS | 4 | Número de colisiones de KernelSnitch |
STAGE3 | 0 | Habilitar la fase del hijo puente pgd-swap (m1q) |
STAGE3_DRYRUN | 0 | Preparar/verificar tablas y abortar antes del swap |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | Ajustes de rociado/diseño de fd_set |
TMP_PAGE_UNAME | 0 | Ejecutar el bootstrap pipei tmp_page-uname de m1q |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | Modo de barrido; split_order barre las 8 páginas del bloque mm liberado |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | Rango del barrido (inicio/cantidad del fragmento) |
PIPEI_CHILD_REGIONS | 0 | Regiones rt_mutex sembradas por hijo; DEBE ser >= fragmento del barrido o las regiones sin sembrar fallan en el candidato 0 |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | Pares de pipe de reclamación/drenaje; aumentar en un fallo de reclamación |
PIPEI_SPONGE_MB | 3072 | Esponja de inundación de orden 0 mantenida durante la reclamación (build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | Prio del top-waiter; inerte con regiones sembradas (guarda heredada de árbol obsoleto, solo A/B) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | Offset intra-página del payload A/B (solo si un dispositivo difiere de -0xe78) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | Nombre marcador / tiempo de retención tras el éxito |
SKB_DATA_DELTA es -0xe78 (medido en dispositivo, build #19) — la cabeza
real de fragmento skb es 8 bytes menos que el -0xe80 derivado del disasm; ese
error de 8 bytes causó todos los fallos deterministas de pselect de los builds
#6-18 (entrada al recorrido → desreferencia basura). Valor compartido de GKI
6.12; las copias de util.c por objetivo llevan un ajuste A/B de entorno
SKB_DATA_DELTA. NO apile PSELECT_SKB_DELTA_OVERRIDE sobre la constante
fijada.ASHMEM_SET_NAME no se
alinea con configfs_bin_write_iter. Use el bootstrap pipei tmp_page. Las
guardas PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF en el
target.h de m1q redirigen las escrituras [11] del recorrido fuera de los fops
estáticos RO.payload+0x6000 + idx*0x40) dentro
de la página reclamada. Los barridos con fake_lock compartido fallaban en el
candidato ~9-12 (nodos obsoletos de pila del kernel reciclados), y las
regiones vacías del build #32 fallaban en el candidato 0 (wake de [9]
owner==NULL sobre la fake_task llena de 0x41). PIPEI_CHILD_REGIONS debe
establecerse o las regiones permanecen sin sembrar.