
Prova de conceito sem privilégios e escalonamento local de privilégios x86_64 para um use-after-free do Binder no kernel Linux (CVE-2026-64468), com laboratório KASAN e diferencial vulnerável/corrigido.
binder_free_transaction() do kernel Linux
Este repositório contém, para o use-after-free do Binder do kernel Linux corrigido
pelo commit upstream
f223d27a546c1e1f48d38fd67760e78f068fe8c4:
binder_chain_64468.c — uma prova de conceito sem privilégios que atinge o
bug e permite que o kernel o comprove, com um laboratório KASAN e um teste
diferencial vulnerável/corrigido (lab/, run.sh, verify.sh).exploit.c — uma escalada de privilégios local autossuficiente para x86_64.
Compila com gcc -O2 -pthread -o exploit exploit.c, executa como um usuário
comum e termina em um shell root.demo/ — um laboratório que inicializa um userland real do Debian 13 em
um kernel sem patch, para que o exploit possa ser compilado pelo próprio gcc
do alvo e executado na máquina que ele então assume.Tudo é executado como um usuário comum (uid/gid 1000, sem capabilities, sem namespaces) contra kernels upstream padrão sem nenhum patch de qualquer tipo.
Aviso
Este código deliberadamente disputa tempos de vida de objetos do kernel e depois sequestra o fluxo de controle do kernel. Uma corrida perdida corrompe o estado do heap do kernel e pode causar pânico ou travar a máquina. Execute-o apenas em uma VM isolada e descartável que você possua. Não o execute em um host.
| Estado | Afirmação |
|---|---|
| Confirmado | A vulnerabilidade é real, é alcançável a partir de um processo sem privilégios, e a correção upstream a remove. |
| Demonstrado | A dereferência vulnerável de um binder_proc moribundo é alcançada naturalmente e repetidamente no kernel sem patch, e nunca no kernel corrigido. |
| Demonstrado | O use-after-free completo, reportado pelo KASAN, no kernel sem patch. |
| Demonstrado | Recuperação do binder_proc liberado com bytes controlados pelo atacante, sequestro do fluxo de controle do kernel e escalada de privilégios para uid 0 a partir de um usuário sem privilégios, em x86_64. |
| Não afirmado | Qualquer resultado em um fornecedor específico ou dispositivo Android. Apenas os kernels upstream listados abaixo foram testados, em x86_64. |
| Não afirmado | Que o exploit fornecido funcione sem modificações contra um kernel de distribuição. Consulte Quais sistemas são afetados: ele precisa de offsets por kernel, e em todas as distribuições de propósito geral pesquisadas o dispositivo binder não é alcançável por um usuário sem privilégios em primeiro lugar. |
binder_free_transaction() lê o processo alvo da transação
sob t->lock, libera esse lock e, em seguida, adquire o lock interno do alvo:```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 */
Nada mantém `target_proc` vivo através dessa lacuna. Um processo que está sendo
destruído em paralelo pode alcançar `binder_proc_dec_tmpref() -> kfree()` no meio,
então o lock é adquirido em memória liberada. A correção upstream fixa `t->to_thread` enquanto
`t->lock` ainda está mantido, o que mantém o processo proprietário vivo até que o
lock interno tenha sido usado e liberado.
A vulnerabilidade foi relatada por **Alice Ryhl** e corrigida por **Carlos
Llamas**, ambos do Google. O
[relatório original](https://lore.kernel.org/all/[email protected]/)
carrega o trace KASAN de referência.
### Alcançando o acesso vulnerável
O único chamador que pode alcançar um `to_proc` *estrangeiro* é
`binder_send_failed_reply()`, e ele só percorre até `t->from_parent` quando
`t->from` é `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 é atribuído em exatamente um lugar, e a atribuição é protegida
algumas linhas antes pela verificação de pilha de transações ruim do binder: uma thread só pode
enviar uma transação síncrona enquanto o topo de sua pilha é uma transação que ela está
recebendo. Portanto, para cada elo dessa cadeia, o remetente do filho e o
receptor do pai são a mesma thread.
Isso tem uma consequência marcante. binder_thread_release() percorre a
pilha da thread que está morrendo com proc->inner_lock mantido e escreve ambos os```
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
em **iterações adjacentes de uma única caminhada**, cada uma sob o respectivo
`t->lock`. Um caminhante só descobre que `child->from == NULL` depois que a caminhada liberou
`child->lock`, e precisa de `parent->lock` para seu próprio snapshot. Portanto, toda a
oportunidade é o intervalo entre o `spin_unlock(&child->lock)` dessa caminhada e o seu
`spin_lock(&parent->lock)` — algumas instruções. A caminhada de liberação mantém um
spinlock e não pode ser preemptada ali; apenas uma interrupção pode atrasá-la.
É por isso que a corrida é estreita e por que tanto a prova de conceito quanto o
exploit são probabilísticos.
### O que a prova de conceito constrói
`binder_chain_64468.c` constrói a cadeia mais curta que alcança o
acesso vulnerável, de modo que o trabalho do caminhante dentro desse intervalo seja o menor que o binder
permite — duas aquisições de `t->lock` e um `kfree()`, sem `inner_proc_lock`
estrangeiro, sem `wake_up` e sem entrega de resposta:```
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 então libera o seu fd de binder, de modo que binder_deferred_release() libera
Y_1..Y_K e, por fim, libera o binder_proc, enquanto cada thread B
emite simultaneamente 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
Um terceiro processo é o gerenciador de contexto do binder, usado apenas para distribuir os
handles que os outros dois precisam. `exploit.c` reutiliza exatamente essa construção.
### Duas condições independentes
Um relatório do KASAN precisa de ambas:
* **o acesso vulnerável** — o walker deve adquirir `parent->lock` dentro da
janela estreita acima, para que ele capture um `to_proc` ainda vivo; e
* **o pouso da liberação dentro da janela** — o walker deve então perder sua CPU
entre soltar `t->lock` e adquirir o lock interno da vítima, e permanecer fora dela
até que a liberação adiada tenha terminado e liberado o `binder_proc`.
A primeira condição tem seu próprio oráculo que não precisa do KASAN: o binder imprime```
binder: binder_free_proc: Unexpected outstanding_txns -1
quando isso acontece, porque o walker e binder_thread_release() então ambos
decrementam o mesmo contador para uma transação. Note que isso não é uma
diferença vulnerável/corrigida por si só — a correção interrompe o free, não o
segundo decremento — então aparece em ambos os kernels. É usado aqui apenas para
mostrar o caminho de código vulnerável sendo exercitado.