
権限のない概念実証と、LinuxカーネルBinderのuse-after-free(CVE-2026-64468)に対するx86_64ローカル権限昇格。KASANラボと、脆弱/修正版の差分比較を備えています。
binder_free_transaction() のプロセスライフタイムuse-after-free
このリポジトリには、上流コミット
f223d27a546c1e1f48d38fd67760e78f068fe8c4
によって修正されたLinuxカーネルBinderのuse-after-freeに関する以下の内容が含まれています:
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、ケーパビリティなし、名前空間なし)として実行されます。
警告
このコードは意図的にカーネルオブジェクトのライフタイムを競合させ、その後カーネルの制御フローを乗っ取ります。競合に負けるとカーネルのヒープ状態が破損し、マシンがパニックまたはハングする可能性があります。所有する隔離された使い捨てVMでのみ実行してください。ホスト上では実行しないでください。
| 状態 | 主張 |
|---|---|
| 確認済み | この脆弱性は実在し、非特権プロセスから到達可能であり、上流の修正によって除去されます。 |
| 実証済み | 消滅しつつあるbinder_procの脆弱な逆参照が、パッチ未適用のカーネル上で自然かつ繰り返し発生し、修正済みカーネルでは決して発生しません。 |
| 実証済み | KASANによって報告された完全なuse-after-freeが、パッチ未適用のカーネル上で発生します。 |
| 実証済み | 解放されたbinder_procを攻撃者制御のバイトで再利用し、カーネルの制御フローを乗っ取り、x86_64上で非特権ユーザーからuid 0への権限昇格を達成します。 |
| 主張なし | 特定のベンダーまたは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`:
このギャップの間、`target_proc` を生かし続けるものは何もない。並行して破棄
されているプロセスは、その間に `binder_proc_dec_tmpref() -> kfree()` に到達
できるため、解放済みメモリ上でロックが取得されることになる。上流の修正では、
`t->lock` が保持されている間 `t->to_thread` をピン留めし、内側のロックが使用
されて解放されるまで所有プロセスを生かし続ける。
この脆弱性は、Google の **Alice Ryhl** によって報告され、同じく Google の
**Carlos Llamas** によって修正された。[元のレポート](https://lore.kernel.org/all/[email protected]/)
には、参照となる KASAN トレースが記載されている。
### 脆弱なアクセスに到達する方法
*外部* の `to_proc` に到達できる唯一の呼び出し元は
`binder_send_failed_reply()` であり、`t->from` が `NULL` の場合にのみ
`t->from_parent` へと辿る:```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 は正確に 1 か所でのみ代入され、その代入は数行前にバインダーの 不正なトランザクションスタック チェックによって保護されています。つまり、スレッドは、そのスタックの先頭が自身が 受信している トランザクションである場合にのみ、同期トランザクションを送信できます。したがって、そのチェーンのすべてのリンクについて、子の送信者と親の受信者は 同じスレッド です。
これには重大な結果があります。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
`binder_chain_64468.c`は、脆弱なアクセスに到達する最短のチェーンを構築するため、そのギャップ内でのウォーカーの作業はbinderが許す限り小さくなります — 2回の`t->lock`取得と1回の`kfree()`で、外部の`inner_proc_lock`も`wake_up`も応答配信もありません:
in **adjacent iterations of a single walk**, each under the respective
`t->lock`. A walker learns `child->from == NULL` only once the walk has released
`child->lock`, and needs `parent->lock` for its own snapshot. So the entire
opportunity is the gap between that walk's `spin_unlock(&child->lock)` and its
`spin_lock(&parent->lock)` — a couple of instructions. The release walk holds a
spinlock and cannot be preempted there; only an interrupt can delay it.
This is why the race is narrow and why both the proof of concept and the
exploit are probabilistic.
### What the proof of concept builds
`binder_chain_64468.c` constructs the shortest chain that reaches the
vulnerable access, so the walker's work inside that gap is as small as binder
allows — two `t->lock` acquisitions and one `kfree()`, with no foreign
`inner_proc_lock`, no `wake_up` and no reply delivery:```
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 がそのバインダー 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
第三のプロセスはバインダーコンテキストマネージャであり、他の2つが必要とするハンドルを配布するためだけに使用される。`exploit.c`はこの正確な構造を再利用する。
### 2つの独立した条件
KASANレポートには次の両方が必要である:
* **脆弱なアクセス** — ウォーカーが上記の狭いウィンドウ内で`parent->lock`を取得し、まだ生存している`to_proc`のスナップショットを取得すること;および
* **ウィンドウ内での解放の着地** — ウォーカーが`t->lock`を解放してから被害者の内部ロックを取得するまでの間にCPUを失い、遅延解放が完了して`binder_proc`が解放されるまでCPUに戻らないこと。
最初の条件にはKASANを必要としない独自のオラクルがある:バインダーは次のように出力する。```
binder: binder_free_proc: Unexpected outstanding_txns -1
それが発生するたびに、ウォーカーとbinder_thread_release()の両方が1つのトランザクションに対して同じカウンタをデクリメントするためです。これはそれ自体では脆弱性/修正済みの差分ではないことに注意してください — 修正は解放を止めるものであり、2回目のデクリメントを止めるものではないため、両方のカーネルに現れます。ここでは、脆弱なコードパスが実行されていることを示すためだけに使用されています。
概念実証は、解放されたメモリへの4バイトのデクリメントで停止します。それをuid 0にするには4つの要素が必要であり、そのどれもバグ自体からは得られません。バグは何も漏洩させないからです。
struct binder_procは648バイトで、通常のGFP_KERNELのkzalloc — __GFP_ACCOUNTではない — で割り当てられます。そのため、同じサイズの他のすべての非アカウント割り当てとともにkmalloc-1kに入り、kmalloc-cg-*の背後に隔離されません。この単一の事実が、オブジェクトをそもそも再利用可能にしているのです。
kfree()の瞬間はユーザー空間からは観測できず、ウォーカーが再びオブジェクトに触れる瞬間も同様です。したがって、タイミングを合わせる対象はありません。そのため、スプレーはポンプとして実行されます。つまり、遅延解放を実行したCPU上で、ウォーカーが実行されている間、kmalloc-1kオブジェクトの割り当てと解放を継続的に行います。
これにはSystem Vメッセージが使用されます。alloc_msg()は、48バイトのヘッダーとペイロードからなる通常の非アカウントkmallocであるため、976バイトのメッセージは1024バイトの割り当てになります。クォータはuidごとではなくキューごとであり、msgrcv()は同期的に解放します。測定されたスループット: 毎秒約198,000回の割り当て、失敗ゼロ。
add_key/user_key_payloadが最初に試されましたが、これは罠です。そのペイロードはuidごとのバイトクォータ(kernel.keys.maxbytes、デフォルト20000)に対して課金され、これはキーガベージコレクタがキーを破棄したときにのみ解放されます。そのため、タイトな割り当て/解放ループはミリ秒単位でこれを枯渇させます。測定結果: 2,121,009回の失敗に対して27,151回の成功した割り当て — スプレーの98.7%が静かに何もしておらず、これはスロットを獲得できないスプレーとまったく同じように見えます。KEYCTL_INVALIDATEは状況を悪化させました(4,775回の成功)。GC作業をキューに入れるためです。
ウォーカーは解放されたbinder_procの4つのフィールドに触れます(オフセットはターゲットビルドでpaholeを使用して測定):
| オフセット | フィールド | ウォーカーが行うこと |
|---|---|---|
| 108 | int outstanding_txns | それをデクリメントする |
| 113 | bool is_frozen | それを読み取る |
| 120 | wait_queue_head_t freeze_wait | outstanding_txns == 0 && is_frozenの場合にそれを走査する |
| 624 | spinlock_t inner_lock | それを取得して解放する |