
Непривилегированный proof-of-concept и локальное повышение привилегий для x86_64 при use-after-free в Binder ядра Linux (CVE-2026-64468), с лабораторией KASAN и дифференциальным сравнением уязвимой/исправленной версий.
binder_free_transaction())
Этот репозиторий содержит, для использования-после-освобождения в Binder ядра Linux, исправленного
коммитом из основной ветки
f223d27a546c1e1f48d38fd67760e78f068fe8c4:
binder_chain_64468.c — непривилегированное доказательство концепции, которое достигает
ошибки и позволяет ядру доказать её, с лабораторией KASAN и
дифференциальным тестом уязвимое/исправленное (lab/, run.sh, verify.sh).exploit.c — автономное локальное повышение привилегий для x86_64.
Компилируется с помощью gcc -O2 -pthread -o exploit exploit.c, запускается от обычного
пользователя и завершается root-оболочкой.demo/ — лаборатория, которая загружает реальное пользовательское окружение Debian 13 на
незапатченном ядре, чтобы эксплойт можно было скомпилировать собственным gcc цели и
запустить на машине, которой он затем овладевает.Всё запускается от обычного пользователя (uid/gid 1000, без capabilities, без namespaces) против стандартных ядер из основной ветки без каких-либо патчей.
Предупреждение
Этот код намеренно гоняется за временем жизни объектов ядра, а затем перехватывает поток управления ядра. Проигранная гонка повреждает состояние кучи ядра и может вызвать панику или зависание машины. Запускайте его только в изолированной, одноразовой виртуальной машине, которой вы владеете. Не запускайте его на хосте.
| Состояние | Утверждение |
|---|---|
| Подтверждено | Уязвимость реальна, достижима из непривилегированного процесса, и исправление из основной ветки устраняет её. |
| Продемонстрировано | Уязвимое разыменование умирающего binder_proc достигается естественным образом и многократно на незапатченном ядре и никогда — на исправленном. |
| Продемонстрировано | Полное использование-после-освобождения, о котором сообщает KASAN, на незапатченном ядре. |
| Продемонстрировано | Перехват освобождённого binder_proc байтами, контролируемыми атакующим, перехват потока управления ядра и повышение привилегий до uid 0 от непривилегированного пользователя на x86_64. |
| Не заявляется | Какие-либо результаты на конкретном устройстве вендора или Android. Тестировались только перечисленные ниже ядра из основной ветки, на x86_64. |
| Не заявляется | Что поставляемый эксплойт работает без изменений против дистрибутивного ядра. См. Какие системы затронуты: ему нужны смещения для каждого ядра, и на каждом рассмотренном дистрибутиве общего назначения устройство binder в первую очередь недоступно непривилегированному пользователю. |
binder_free_transaction() считывает целевой процесс из транзакции
под t->lock, снимает эту блокировку, а затем захватывает внутреннюю блокировку цели:```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.
### Reaching the vulnerable access
The only caller that can reach a *foreign* `to_proc` is
`binder_send_failed_reply()`, and it only walks to `t->from_parent` when
`t->from` is `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 назначается ровно в одном месте, и это присваивание защищено несколькими строками выше проверкой плохого стека транзакций в binder: поток может отправлять синхронную транзакцию только тогда, когда вершина его стека — это транзакция, которую он получает. Следовательно, для каждого звена этой цепочки отправитель дочернего потока и получатель родительского потока — это один и тот же поток.
Это имеет важное следствие. binder_thread_release() обходит стек умирающего потока, удерживая proc->inner_lock, и записывает оба```
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
в **соседних итерациях одного обхода**, каждая под своим
`t->lock`. Обходчик узнаёт, что `child->from == NULL`, только после того, как обход
освободил `child->lock`, и ему нужен `parent->lock` для собственного снимка. Так что вся
возможность — это промежуток между `spin_unlock(&child->lock)` этого обхода и его
`spin_lock(&parent->lock)` — пара инструкций. Освобождающий обход удерживает
спинлок и не может быть вытеснен там; только прерывание может его задержать.
Именно поэтому гонка узкая и почему и proof of concept, и
эксплойт являются вероятностными.
### Что строит proof of concept
`binder_chain_64468.c` строит кратчайшую цепочку, которая достигает
уязвимого доступа, чтобы работа обходчика внутри этого промежутка была как можно меньше, насколько позволяет binder — два захвата `t->lock` и один `kfree()`, без постороннего
`inner_proc_lock`, без `wake_up` и без доставки ответа:```
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 затем закрывает свой binder fd, поэтому binder_deferred_release() освобождает
Y_1..Y_K и в итоге освобождает binder_proc, в то время как каждый поток B
одновременно выполняет 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
Третий процесс — это контекст-менеджер binder, используемый только для раздачи
дескрипторов, которые нужны двум другим. `exploit.c` повторно использует именно эту конструкцию.
### Два независимых условия
Для отчёта KASAN необходимы оба:
* **уязвимый доступ** — обходчик должен захватить `parent->lock` внутри
узкого окна, описанного выше, чтобы он зафиксировал ещё живой `to_proc`; и
* **приземление освобождения внутри окна** — обходчик должен затем потерять свой CPU
между снятием `t->lock` и захватом внутренней блокировки жертвы, и оставаться вне его,
пока отложенное освобождение не завершится и не освободит `binder_proc`.
Первое условие имеет собственный оракул, которому не нужен KASAN: binder выводит```
binder: binder_free_proc: Unexpected outstanding_txns -1
whenever it happens, because the walker and binder_thread_release() then both
decrement the same counter for one transaction. Note this is not a
vulnerable/fixed differential by itself — the fix stops the free, not the second
decrement — so it appears on both kernels. It is used here only to show the
vulnerable code path being exercised.