
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.
| 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. |
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`.