
Sin privilegios, prueba de concepto y escalada local de privilegios x86_64 para un use-after-free del kernel de Linux en Binder (CVE-2026-64468), con laboratorio KASAN y diferencial vulnerable/corregido.
binder_free_transaction() del kernel Linux
Este repositorio contiene, para el use-after-free de Binder del kernel Linux corregido por
el commit upstream
f223d27a546c1e1f48d38fd67760e78f068fe8c4:
binder_chain_64468.c — una prueba de concepto sin privilegios que alcanza el
fallo y permite que el kernel lo demuestre, con un laboratorio KASAN y una
comparativa vulnerable/corregido (lab/, run.sh, verify.sh).exploit.c — una escalada de privilegios local autónoma para x86_64.
Se compila con gcc -O2 -pthread -o exploit exploit.c, se ejecuta como un usuario
normal y termina en una shell de root.demo/ — un laboratorio que arranca un userland real de Debian 13 en un
kernel sin parche, de modo que el exploit pueda compilarse con el propio gcc del
objetivo y ejecutarse en la máquina que luego toma el control.Todo se ejecuta como un usuario normal (uid/gid 1000, sin capacidades, sin namespaces) contra kernels upstream estándar sin parches de ningún tipo.
Advertencia
Este código deliberadamente compite por las vidas útiles de los objetos del kernel y luego secuestra el flujo de control del kernel. Una carrera perdida corrompe el estado del heap del kernel y puede provocar un pánico o un cuelgue de la máquina. Ejecútalo solo en una VM aislada y desechable que sea tuya. No lo ejecutes en un host.
binder_free_transaction() lee el proceso objetivo de la transacción
bajo t->lock, libera ese bloqueo y luego adquiere el bloqueo interno del objetivo:```c
spin_lock(&t->lock);
target_proc = t->to_proc;
spin_unlock(&t->lock);
if (target_proc) {
binder_inner_proc_lock(target_proc); /* use after free */
Nothing keeps `target_proc` alive across that gap. A process that is being torn
down in parallel can reach `binder_proc_dec_tmpref() -> kfree()` in between, so
the lock is taken on freed memory. The upstream fix pins `t->to_thread` while
`t->lock` is still held, which keeps the owning process alive until the inner
lock has been used and released.
The vulnerability was reported by **Alice Ryhl** and fixed by **Carlos
Llamas**, both of Google. The
[original report](https://lore.kernel.org/all/[email protected]/)
carries the reference KASAN trace.
### Alcanzando el acceso vulnerable
El único llamador que puede alcanzar un `to_proc` *externo* es
`binder_send_failed_reply()`, y solo recorre hasta `t->from_parent` cuando
`t->from` es `NULL`:```c
target_thread = binder_get_txn_from_and_acq_inner(t);
if (target_thread) { ...; binder_free_transaction(t); return; }
next = t->from_parent;
binder_free_transaction(t);
t = next;
from_parent se asigna exactamente en un solo lugar, y la asignación está protegida unas
líneas antes por la comprobación de pila de transacciones incorrecta de binder: un hilo solo puede
enviar una transacción síncrona mientras la parte superior de su pila sea una transacción que está
recibiendo. Por lo tanto, para cada eslabón de esa cadena, el remitente del hijo y el
receptor del padre son el mismo hilo.
Eso tiene una consecuencia contundente. binder_thread_release() recorre la
pila del hilo moribundo con proc->inner_lock retenido y escribe ambos```
iteration j : [holds child->lock ] child->from = NULL ; unlock child->lock
spin_lock(parent->lock)
iteration j+1 : [holds parent->lock] parent->to_proc = NULL
en **iteraciones adyacentes de un único recorrido**, cada una bajo su
respectivo `t->lock`. Un caminante solo descubre que `child->from == NULL` una vez que el recorrido ha liberado
`child->lock`, y necesita `parent->lock` para su propia instantánea. Así que toda la
oportunidad es el hueco entre el `spin_unlock(&child->lock)` de ese recorrido y su
`spin_lock(&parent->lock)` — un par de instrucciones. El recorrido de liberación mantiene un
spinlock y no puede ser adelantado allí; solo una interrupción puede retrasarlo.
Por eso la condición de carrera es estrecha y por qué tanto la prueba de concepto como el
exploit son probabilísticos.
### Qué construye la prueba de concepto
`binder_chain_64468.c` construye la cadena más corta que alcanza el
acceso vulnerable, de modo que el trabajo del caminante dentro de ese hueco sea lo más pequeño que binder
permite — dos adquisiciones de `t->lock` y un `kfree()`, sin `inner_proc_lock` externo,
sin `wake_up` y sin entrega de respuesta:```
B thread i --e2 (sync, code 0x4442414b)--> P thread Y_i
P thread Y_i --e1 (sync, code 0x54414c4c)--> B thread i (nested target)
B thread i stack: [ e2 outgoing , e1 incoming (top) ]
P thread Y_i stack: [ e2 incoming , e1 outgoing (top) ]
P then drops its binder fd, so binder_deferred_release() releases
Y_1..Y_K and finally frees the binder_proc, while every B thread
concurrently issues BINDER_THREAD_EXIT:```
binder_thread_release(B_i) nulls e1->to_proc (own proc) and e2->from
binder_send_failed_reply(e1) e1->from == NULL once Y_i was released
-> binder_free_transaction(e1) target_proc already NULL, kfree(e1)
-> binder_free_transaction(e2) target_proc == P <-- vulnerable access
Un tercer proceso es el gestor de contexto de binder, usado únicamente para repartir los
handles que los otros dos necesitan. `exploit.c` reutiliza exactamente esta construcción.
### Dos condiciones independientes
Un informe de KASAN necesita ambas:
* **el acceso vulnerable** — el walker debe tomar `parent->lock` dentro de la
estrecha ventana anterior, de modo que capture una instantánea de un `to_proc` aún vivo; y
* **el aterrizaje del free dentro de la ventana** — el walker debe entonces perder su CPU
entre soltar `t->lock` y tomar el lock interno de la víctima, y permanecer fuera de ella
hasta que la liberación diferida haya terminado y haya liberado el `binder_proc`.
La primera condición tiene su propio oráculo que no necesita KASAN: binder imprime```
binder: binder_free_proc: Unexpected outstanding_txns -1
cuando ocurre, porque el walker y binder_thread_release() entonces ambos
decrementan el mismo contador para una transacción. Ten en cuenta que esto no es
una diferencia vulnerable/corregida por sí misma — el fix detiene el free, no el segundo
decremento — por lo que aparece en ambos kernels. Se usa aquí solo para mostrar la
ruta de código vulnerable siendo ejercitada.
La prueba de concepto se detiene en un decremento de 4 bytes de memoria liberada. Convertir eso en uid 0 necesita cuatro cosas, y ninguna de ellas proviene del bug en sí: el bug no filtra nada.
struct binder_proc tiene 648 bytes y se asigna con un GFP_KERNEL
kzalloc simple — no __GFP_ACCOUNT. Por lo tanto, aterriza en kmalloc-1k,
junto con cualquier otra asignación no contabilizada de ese tamaño, y no está
aislada detrás de kmalloc-cg-*. Ese único hecho es lo que hace que el objeto
sea reclamable en absoluto.
El momento del kfree() no es observable desde userspace, y tampoco lo es el
momento en que el walker toca el objeto de nuevo, por lo que no hay nada contra lo que sincronizarse.
El spray, por tanto, funciona como una bomba: asigna y libera objetos kmalloc-1k
continuamente, desde la CPU que ejecutó el release diferido, durante el tiempo que
los walkers estén en ejecución.
Se usan mensajes System V para ello. alloc_msg() es un kmalloc
no contabilizado simple de un encabezado de 48 bytes más el payload, por lo que un mensaje de 976 bytes es una asignación de 1024 bytes; la cuota es por cola en lugar de por uid; y msgrcv() libera
de forma síncrona. Rendimiento medido: ~198,000 asignaciones por segundo, cero
fallos.
Se probó primero add_key/user_key_payload y es una trampa. Su payload se
carga contra una cuota de bytes por uid (kernel.keys.maxbytes, 20000 por defecto)
que solo se libera cuando el recolector de basura de claves destruye la clave, por lo que
un bucle ajustado de asignar/liberar lo agota en milisegundos: medido 27,151
asignaciones exitosas frente a 2,121,009 fallos — el 98.7% del spray haciendo
silenciosamente nada, lo que parece exactamente un spray que nunca gana el slot.
KEYCTL_INVALIDATE lo empeoró (4,775 éxitos), porque pone en cola trabajo de GC.
El walker toca cuatro campos del binder_proc liberado (offsets medidos con
pahole en la build objetivo):
Con outstanding_txns == 1 e is_frozen == 1, el decremento llega a cero y
el walker llama a wake_up_interruptible_all(&proc->freeze_wait).
__wake_up_common entonces calcula curr = head.next - 24 y llama a
*(head.next - 8): un puntero a función leído desde donde apunte head.next,
que es un valor que el objeto reclamado proporciona.
Ese puntero tiene que llegar a memoria que el atacante controla, en una dirección de kernel, y el bug no filtra nada. Ambas direcciones provienen en su lugar del timing de prefetch — el mismo canal que KASLD (Brendan Coles, MIT), cuya implementación deriva de esto:
Texto del kernel. Un prefetch de una dirección de kernel mapeada se resuelve en el
recorrido de la tabla de páginas y se retira de forma mediblemente más rápida que uno de una dirección no mapeada, incluso
aunque el acceso nunca se vuelva arquitectónicamente visible. Escanear los slots de 2 MiB
del rango de texto muestra la imagen como una secuencia de slots rápidos; su primer slot
es _text.
El mapa directo. Con CONFIG_RANDOMIZE_MEMORY el mapa directo está
aleatorizado en unidades de 1 GiB, por lo que también hay que localizarlo. A diferencia del texto,
cubre toda la RAM, por lo que es la secuencia contigua más larga de slots mapeados. Se
necesitaron dos refinamientos para hacerlo utilizable:
Lo que la ejecución comienza de forma fiable no es page_offset_base en sí, sino el primer
slot que el kernel podría mapear con una página de 1 GiB — el que cubre los 4 GiB físicos.
Por debajo de eso, el agujero PCI y las reservas de firmware fuerzan páginas de 2 MiB cuyo
recorrido más largo no es separable de lo no mapeado aquí. Ese slot es exactamente lo que
el spray necesita, por lo que es lo que se usa.
Entonces ~60% de la memoria física se llena con copias de una página de 4 KiB manipulada, por lo que un offset fijo desde ese ancla está respaldado por la página manipulada sea cual sea el layout que resulte.
freeze_wait.head.next -> entry1 (in the sprayed page)
entry1.func = mov 0x28(%rdi),%rdi ; mov 0x18(%rdi),%rax ; jmp *0x58(%rax) RDI <- entry1+0x28 = &cred RAX <- cred+0x18 jmp *(RAX+0x58) = commit_creds -> commit_creds(cred)
entry2.func = mov $-1,%rax ; ret __wake_up_common breaks out on ret < 0
No stack pivot and no `iretq`: the hijacked thread returns out of its `ioctl()`
normally and is simply back in userspace, as root.
### The forged cred, and the bug that would have wasted it
The dispatcher gadget takes its `RAX` from `cred+0x18`, and `cred+0x18` is
`euid`/`egid`, so immediately after the chain `euid` is the low half of a kernel
pointer. That is cosmetic. What is not cosmetic is that `prepare_creds()` —
which **every later `fork()` and `execve()` calls** — dereferences three fields
with no NULL check:```c
get_group_info(new->group_info); /* refcount_inc(&gi->usage) */
get_uid(new->user); /* refcount_inc(&u->__count) */
new->ucounts = get_ucounts(new->ucounts);
Una credencial falsificada que deja user, ucounts y group_info en NULL otorga uid 0 y luego provoca un pánico en la máquina en el primer execve — es decir, el exploit reportaría éxito e inmediatamente destruiría el sistema. Por lo tanto, la cadena apunta user, ucounts y group_info a los globales reales del kernel root_user, init_ucounts e init_groups, cuyas direcciones provienen de la misma base _text. Con esos establecidos, el hilo con privilegios elevados posee CAP_SETUID sobre init_user_ns, por lo que llama a setresuid(0,0,0) y el kernel instala una credencial root limpia y asignada por el kernel sobre la falsificada. Solo entonces se realiza cualquier otra acción.
El privilegio se transfiere al proceso padre mediante una copia del binario del exploit con setuid-root, que se desvincula a sí mismo antes de ejecutar el shell, de modo que el shell root se ejecuta en el proceso principal en una terminal limpia y no queda nada con setuid atrás.
uname -r no es suficiente. Los kernels de los proveedores suelen aplicar backports de correcciones de binder sin cambiar a la versión mainline correspondiente; inspecciona el código fuente o el changelog del paquete.
La corrección está etiquetada como Cc: stable, por lo que las ramas stable y de proveedores reciben backports.
Son preguntas diferentes, y la segunda es la que determina el impacto.
El código vulnerable se compila dondequiera que CONFIG_ANDROID_BINDER_IPC esté establecido. Eso incluye las distribuciones de propósito general — pero en todas las encuestadas, el driver es un módulo que no se carga por defecto, e incluso cuando se carga, init_binder_device() registra el dispositivo misc sin establecer miscdev.mode, por lo que devtmpfs crea /dev/binder como 0600 root:root. En Android es ueventd quien lo abre a 0666, que es exactamente por qué el bug importa allí y en su mayoría no aquí.
Configuraciones leídas de los paquetes de kernel propios de las distribuciones:
La exposición práctica en una distribución de escritorio es, por tanto, indirecta: cualquier cosa que cargue binder y lo abra — Waydroid, Anbox, un emulador de Android o un runtime de contenedores — reintroduce exactamente la alcanzabilidad de Android en una máquina cuyo kernel aún tiene el bug.
Estas no afectan a la vulnerabilidad; afectan a este exploit.
Ambos kernels son árboles upstream estándar. Nada está parcheado.
CONFIG_KASAN_GENERIC en el primer laboratorio es un detector, no un habilitador: la condición de carrera es idéntica sin él. CONFIG_PREEMPT es un requisito previo real, y es lo que Android incluye.
La arquitectura no es un factor para el bug — es un error de ciclo de vida en C independiente de la arquitectura. Es un factor muy importante para el exploit: los gadgets, el canal de prefetch y el diseño del mapa directo son todos x86_64.
Requisitos: clang, lld, make, cpio, gzip, qemu-system-x86_64, docker (solo para ensamblar el rootfs de Debian), un clon local del árbol git de Linux y gcc.
gcc -O2 -pthread -o exploit exploit.c ./exploit
Para un kernel distinto al de `demo/`, extrae primero sus offsets:```sh
./mkoffsets.sh /path/to/vmlinux > offsets.h
gcc -O2 -pthread -DEXPLOIT_OFFSETS='"offsets.h"' -o exploit exploit.c
Tunables, todos opcionales, todos leídos del entorno:
CVE64468_SECONDS, CVE64468_THREADS, CVE64468_SPRAY_PERCENT,
CVE64468_CALL_OFFSET_MB, CVE64468_KASLR_ATTEMPTS, CVE64468_DELAY_MAX_US,
CVE64468_DELAY_STEP_US, CVE64468_STAGGER_US, CVE64468_VERBOSE,
CVE64468_SHELL.
LINUX_GIT=/path/to/linux ./lab/build.sh # x86_64 LINUX_GIT=/path/to/linux TARGET_ARCH=arm64 ./lab/build.sh # arm64 ./run.sh vulnerable ./verify.sh 600 16
El script crea dos worktrees separados en los commits anteriores, se niega a ejecutarse
si cualquiera de los worktrees está sucio, verifica que el árbol vulnerable carece del fix y
que el árbol corregido lo contiene, compila ambos kernels y empaqueta la prueba de concepto
en un initramfs.
### El laboratorio de explotación```sh
./demo/build-kernel.sh # vulnerable kernel, KASAN off, hardening on
./demo/build-rootfs.sh # Debian 13 userland + gcc, as an initramfs
./demo/run-demo.sh # boot it; this is what the recording shows
./demo/verify.sh logs/ # unattended reliability run, one guest per trial
demo/run-demo.sh arranca la máquina invitada y entrega la consola al uid 1000, que
compila exploit.c con el propio gcc de la máquina invitada y lo ejecuta.
Las imágenes del kernel, los árboles de trabajo, los árboles del sistema de archivos raíz y los initramfs son artefactos de laboratorio y no están versionados.
docs/example-output.txt es la transcripción real del kernel vulnerable, incluido
el informe KASAN; docs/patched-negative-output.txt es el control del kernel
corregido; docs/e2e-results.json es el resultado legible por máquina.
La cadena de llamadas reportada coincide exactamente con el informe ascendente:``` BUG: KASAN: slab-use-after-free in queued_spin_lock_slowpath+0x62/0x6a0 Read of size 4 at addr ffff888100282270 by task cve64468-B/91 CPU: 6 UID: 1000 PID: 91 Comm: cve64468-B Not tainted 7.2.0-rc1+ #2 PREEMPT _raw_spin_lock+0x55/0x60 binder_free_transaction+0x80/0x1f0 binder_send_failed_reply+0x98/0x340 binder_thread_release+0x528/0x5e0 binder_ioctl+0x1ca/0xeb0 __x64_sys_ioctl+0x8c9/0xcf0
Allocated by task 93: binder_open+0xb5/0x7b0
Freed by task 9: kfree+0x113/0x310 binder_deferred_func+0x104b/0x1180 process_scheduled_works+0x69f/0xbb0
`UID: 1000` es la identidad no privilegiada del propio proof of concept: la víctima
`binder_proc` se asigna con `binder_open()`, se libera mediante la workqueue
diferida de binder, y el walker la lee después de la liberación.
Ejecución registrada, 2026-08-16, `./verify.sh 1200 16 3` — tres invitados QEMU/KVM
concurrentes por variante, 10 vCPUs cada uno, 16 hilos, 1200 s por variante, **sin
parches de kernel en ninguno de los dos lados**:
| | Vulnerable `114a116aaa5f` | Corregido `f223d27a546c` |
| --- | --- | --- |
| Intentos | 158,384 | 158,471 |
| Walkers | 2,534,144 | 2,535,536 |
| Fallos de configuración | 0 | 0 |
| Acceso vulnerable (`Unexpected outstanding_txns -1`) | 1,127 | 934 |
| **KASAN `slab-use-after-free`** | **8** | **0** |
Ese es el diferencial: la misma carga de trabajo, el mismo número de intentos con
una diferencia del 0.06%, y el use-after-free solo en el kernel vulnerable sin
parchear.
El acceso vulnerable aparece en *ambos* kernels, y eso es esperado — el parche
evita que el proceso se libere dentro de la ventana, no el segundo decremento de
`outstanding_txns`. Por eso esa línea se usa solo como un oráculo barato y nunca
como el diferencial.
### Escalada de privilegios

`docs/lpe-output.txt` es una transcripción real de un invitado de demostración, y
`docs/lpe-demo.cast` es la grabación Asciinema completa y sin editar a partir de la
cual se renderizó la animación anterior (`asciinema play docs/lpe-demo.cast` la
reproduce en su totalidad). La animación omite la larga parte central de la carrera
de esa grabación — la consola serie del invitado transmite el debug de binder
durante toda la carrera de ~24 minutos, lo que se renderizaría en decenas de
megabytes de registro desplazándose — conservando el preámbulo de Debian y el shell
de root; la propia línea del exploit `hit after 26661 attempts ... in 1437s`
indica exactamente lo que se omitió. Ambas son ejecuciones únicas: un invitado que
no gana dentro de su presupuesto se apaga, y la grabación simplemente se repite en
lugar de editarse para forzar una victoria.
Consulta *Fiabilidad* más abajo para conocer la tasa de éxito medida.
## Fiabilidad
La carrera es probabilística en ambos aspectos, por lo que una ejecución fallida es
el comportamiento esperado algunas veces, no un exploit roto.
### Seguridad de memoria
Rates derivadas en el kernel vulnerable, a partir de la ejecución anterior:
| Cantidad | Valor |
| --- | --- |
| Tasa de intentos | ~44 intentos/s por invitado, ~132/s entre tres |
| Acceso vulnerable | 7.1e-3 por intento |
| Liberación que aterriza dentro de la ventana, dado el acceso | 7.1e-3 |
| Informe KASAN | ~1 por cada 20,000 intentos, es decir, aproximadamente uno cada 2.5 minutos a esta tasa |
### Escalada de privilegios
Cada invitado es un ensayo independiente: su propio KASLR, su propia aleatorización
del direct-map, y un invitado que gana deja de competir. La cifra es, por tanto, una
**tasa de éxito por arranque**, no por intento.
Campaña registrada, 2026-08-16 23:32 UTC, `GUESTS=6 CPUS=8 MEMORY_MB=9216 ./demo/verify.sh`
— seis invitados QEMU/KVM concurrentes, `114a116aaa5f` sin parchear, userland Debian 13,
presupuesto de 60 minutos cada uno, exploit compilado dentro del invitado, iniciado como uid 1000:
| | |
| --- | --- |
| Invitados que alcanzaron uid 0 | **2 de 6** |
| Tiempo hasta root | 623 s y 1,344 s |
| Intentos en la carrera ganadora | 13,839 y 31,125 |
| Intentos de cada invitado que no ganó | ~95,000 durante los 3,600 s completos |
| Intentos totales en la campaña | 427,676 (6.8 M walkers de pila) |
| Accesos vulnerables observados (`Unexpected outstanding_txns -1`) | 256 |
| **Bloqueos, oopses o pánicos del kernel** | **0**, en 9 horas-invitado |
Hay dos cosas que merece la pena destacar de esa tabla.
**Prácticamente toda carrera ganada se convirtió en root.** El laboratorio KASAN mide
la probabilidad de que la liberación aterrice dentro de la ventana, dado el acceso
vulnerable, en 7.1e-3. Aplicado a los 256 accesos observados aquí, eso predice ~1.8
use-after-frees en toda la campaña — y se obtuvieron 2 roots. El reclaim, el
descubrimiento de direcciones y la cadena no son el cuello de botella; la carrera lo es.
**Nada se bloqueó.** Ningún invitado sufrió un oops en nueve horas-invitado, incluidos
los cuatro que nunca ganaron. O la cadena se dispara contra una página rociada y
correctamente ubicada, o nunca se dispara en absoluto — que es para lo que sirve el
rechazo por mayoría en la etapa 2.
Ese rechazo sí se activa en la práctica. Arrancar seis invitados a la vez en un host ya
cargado produjo uno que se rindió en la etapa 2 con```
[*] direct map not found; refusing to fire at an unverified address
y salió sin competir. Ese es el comportamiento previsto: un arranque desperdiciado es el resultado correcto cuando el canal de sincronización no puede alcanzar consenso, y es mucho mejor que la alternativa de disparar la cadena a una dirección que nunca fue confirmada.
docs/lpe-results.json contiene la forma legible por máquina, incluido el SHA-256 del kernel e initramfs exactos utilizados.
Ejecutar las máquinas invitadas de forma concurrente no es solo cuestión de paralelismo. En un host con contención, KVM desprograma las vCPU invitadas, y ese es exactamente el retraso que necesita la segunda condición: el caminante tiene que perder su CPU entre soltar t->lock y tomar el bloqueo interno de la víctima. Una sola máquina invitada en un host inactivo se midió en aproximadamente una vigésima parte de la tasa de acceso vulnerable de tres máquinas invitadas concurrentes.
Sin embargo, hay un límite superior. Con ocho máquinas invitadas de 8 vCPU cada una en un host de 32 hilos, con ~5 GiB de spray de mapa directo por máquina invitada, el host entró en swap y tres de las ocho máquinas invitadas no progresaron en absoluto. Seis es el valor predeterminado incluido.
Los hilos auxiliares opcionales de preferencia dentro de la máquina invitada se midieron con un costo de aproximadamente tres veces la tasa de intentos sin mejorar la tasa de aciertos, y no se utilizan.
Un factor ambiental resultó importar más de lo esperado: la salida de depuración propia de binder. Con binder.debug_mask en su valor predeterminado, el controlador emite una gran cantidad de tráfico pr_info limitado por tasa durante la carrera, y la presión de printk y del bloqueo de consola que eso crea alarga exactamente la ventana de preferencia que necesita la segunda condición. Silenciarlo con binder.debug_mask=0 para una consola más limpia — lo obvio para una grabación — redujo de forma medible la tasa de aciertos en las pruebas: las máquinas invitadas superaron con creces los recuentos de intentos de ambos ganadores sin un acierto. Por lo tanto, demo/run-demo.sh deja la depuración de binder en su valor predeterminado, y una consola silenciosa es una opción explícita. Esta es una propiedad del laboratorio, no del exploit — pero es una buena ilustración de cuánto depende esta carrera de la fluctuación de sincronización a nivel de sistema en lugar de cualquier cosa que el propio exploit controle.
/dev/binder, envía transacciones de binder y sale de los hilos de binder. No instala nada y no deja nada atrás.panic=1 oops=panic para que una ejecución termine en lugar de continuar con un estado corrupto. Siempre reinicie desde un arranque limpio.<[email protected]> (Twitter: @aramosf)Verificado el 2026-08-16 con SearchSploit (copia local de Exploit-DB) y búsqueda web de CVE-2026-64468, binder_free_transaction y f223d27a546c. No se encontró ningún exploit público ni prueba de concepto para este CVE; SearchSploit solo devuelve entradas de binder de Android más antiguas y no relacionadas. Esta es una verificación puntual, no una garantía permanente.
| Estado | Afirmación |
|---|
| Confirmado | La vulnerabilidad es real, es alcanzable desde un proceso sin privilegios y el parche upstream la elimina. |
| Demostrado | La desreferencia vulnerable de un binder_proc moribundo se alcanza de forma natural y repetida en el kernel sin parche, y nunca en el kernel parcheado. |
| Demostrado | El use-after-free completo, notificado por KASAN, en el kernel sin parche. |
| Demostrado | La recuperación del binder_proc liberado con bytes controlados por el atacante, el secuestro del flujo de control del kernel y la escalada de privilegios a uid 0 desde un usuario sin privilegios, en x86_64. |
| No afirmado | Cualquier resultado en un proveedor o dispositivo Android específico. Solo se probaron los kernels upstream enumerados a continuación, en x86_64. |
| No afirmado | Que el exploit incluido funcione sin modificaciones contra un kernel de distribución. Consulta Qué sistemas están afectados: necesita offsets específicos por kernel y, en todas las distribuciones de propósito general analizadas, el dispositivo binder no es alcanzable por un usuario sin privilegios en primer lugar. |
| Offset | Campo | Lo que hace el walker |
|---|
| 108 | int outstanding_txns | lo decrementa |
| 113 | bool is_frozen | lo lee |
| 120 | wait_queue_head_t freeze_wait | lo recorre si outstanding_txns == 0 && is_frozen |
| 624 | spinlock_t inner_lock | lo toma y lo libera |
| Estado | Commit | Notas |
|---|
| Linaje introducido | a370003cc301 | Nombrado por la etiqueta Fixes: del upstream |
| Vulnerable validado | 114a116aaa5f | Padre directo de la corrección; incluye la corrección vecina de CVE-2026-64469, por lo que el par aísla CVE-2026-64468 por sí solo |
| Mainline corregido | f223d27a546c | La corrección bajo prueba |
| Distribución | Kernel | ANDROID_BINDER_IPC | Dispositivo | SLAB_BUCKETS | RANDOM_KMALLOC_CACHES | ¿Alcanzable sin privilegios? |
|---|
| Debian 13 (trixie) | 6.12.101 | m | ANDROID_BINDER_DEVICES="binder", BINDERFS desactivado | y | desactivado | No — módulo no cargado; /dev/binder es 0600 |
| Debian 12 (bookworm) | 6.1.0 | m | ANDROID_BINDER_DEVICES="binder", BINDERFS desactivado | n/a (pre-6.11) | n/a (pre-6.6) | No — igual |
| Ubuntu 24.04 LTS | 6.8.0 | m | BINDERFS=m, ANDROID_BINDER_DEVICES="" | n/a (pre-6.11) | y | No — requiere root para mount -t binder |
| Ubuntu 22.04 LTS | 5.15.0 | m | BINDERFS=m, ANDROID_BINDER_DEVICES="" | n/a | n/a | No — igual |
| Android (AOSP / proveedor) | 6.1, 6.6, 6.12 GKI | y | /dev/binder, /dev/hwbinder, /dev/vndbinder | — | — | Sí — el driver es el IPC de la plataforma y es accesible mundialmente |
| Opción | Efecto aquí |
|---|
CONFIG_SLAB_BUCKETS (6.11+) | Fatal para este reclaim. Aísla msg_msg en sus propios buckets de kmalloc, por lo que la bomba nunca puede aterrizar en el slot de binder_proc. Debian 13 lo establece. Habría que encontrar otra asignación no contabilizada de 1 KiB. El kernel 6.6, que es el que ejecutan los dispositivos Android de interés, lo precede por completo. |
CONFIG_RANDOM_KMALLOC_CACHES (6.6+) | Divide kmalloc-1k en varios caches por sitio de llamada, por lo que la bomba tiene que acertar en el mismo; un impuesto de 1 entre 16 sobre el reclaim, no un muro. Ubuntu lo establece, Debian no. |
Aislamiento de tablas de páginas (nopti no usado) | Fatal para el descubrimiento de direcciones. El prefetch no puede ver el texto del kernel con PTI activo, y el exploit detecta esto y se detiene. PTI está compilado en todas las distribuciones anteriores, pero la CPU decide si está activo: está desactivado en hardware no afectado por Meltdown, que es donde se midieron estos resultados. |
CONFIG_SLAB_FREELIST_RANDOM, ..._HARDENED | Habilitados en el laboratorio, como los envían las distribuciones. Sin efecto medible: la bomba no predice el orden de la freelist, simplemente asigna una gran cantidad de objetos. |
KASLR (RANDOMIZE_BASE, RANDOMIZE_MEMORY) | Habilitado. Derrotado por las etapas de prefetch; sin nokaslr. |
| Desplazamientos por kernel | La cadena necesita commit_creds, tres globales relacionados con cred y dos gadgets, como desplazamientos desde _text. mkoffsets.sh los extrae de un vmlinux objetivo; sin ellos, el exploit dispara a direcciones incorrectas. Esto es una propiedad por compilación de cualquier exploit de kernel, no una defensa. |
Laboratorio de seguridad de memoria (lab/) | Laboratorio de explotación (demo/) |
|---|
| Versión del kernel | 7.2.0-rc1+ | 7.2.0-rc1+ |
| Commit vulnerable | 114a116aaa5f0295376cdf12da743c5bce3b20ce | mismo |
| Commit corregido | f223d27a546c1e1f48d38fd67760e78f068fe8c4 | — (la explotación se mide solo en el kernel vulnerable) |
| Arquitectura | x86_64 (KVM) y arm64 (TCG) | x86_64 (KVM) |
| Compilador | Ubuntu clang 21.1.8 / LLD 21.1.8 | mismo |
| KASAN | activado — es el detector | desactivado — cambia el diseño del slab y haría que cualquier reclaim no fuera representativo |
| Endurecimiento del slab | — | SLAB_FREELIST_RANDOM, SLAB_FREELIST_HARDENED activados; SLAB_BUCKETS, RANDOM_KMALLOC_CACHES desactivados |
| KASLR | — | RANDOMIZE_BASE, RANDOMIZE_MEMORY activados |
| Userland | initramfs mínimo | Debian GNU/Linux 13 (trixie), con el gcc propio de la distribución |
| Identidad inicial | uid 1000, gid 1000, sin capacidades, sin namespaces | igual |
| Línea de comandos de arranque | console=ttyS0 rdinit=/init panic=1 oops=panic kasan_multi_shot=1 | console=ttyS0 loglevel=4 rdinit=/init — sin nopti, sin nokaslr, sin mitigations=off |