AIが0-dayをN-dayのように感じさせる
AIを活用してLinuxカーネルのnet/schedサブシステムでuse-after-free脆弱性を発見し、LPEエクスプロイトを作成した技術的詳細。レース条件の最適化手法、AIによるバグ発見・PoC作成・レース改善の加速について解説。タイマーfd/epollやエラーパス再構築により、エクスプロイト時間を15分から5秒に短縮した。
AIが0-dayをN-dayのように感じさせる
はじめに
net/tlsのバグのn-day分析と、パッチ済みのnet/rxrpcバグのエクスプロイト作成に続き、著者は0-dayのバグハンティングに移りました。AIの助けを借りて、net/schedでuse-after-free(UAF)バグを発見し、それに基づいてLPEエクスプロイトを作成しました。このブログでは、そのエクスプロイトの技術的詳細と、TyphoonPwn2026のCentOS 9デスクトップをターゲットに最適化した方法について述べます。さらに、kernel/events/core.cで発見した他の2つの悪用可能なバグについても簡単に紹介します。
AIの活用
以前のnet/rxrpcのn-dayエクスプロイトと比較して、今回はすべての側面を根本から完全に理解するよりも、迅速にエクスプロイトを完成・改善することに焦点を当てました。そのため、AIを利用してバグの発見、KASAN PoCの作成、レース条件の改善など、プロセスのさまざまな側面を高速化しました。AIは素早い反復に確かに役立ちましたが、推論能力は依然として不十分であり、明確な盲点がありました。特に微調整の際には、自身の判断を働かせることが重要でした。
net/schedの概要
net/schedはLinuxのパケットスケジューリングサブシステムで、デバイスドライバの上の層に位置し、パケットをいつ、どの順序で、送信するかどうかを決定します。また、netlinkを介してパケット処理ルールを設定するAPIを提供します。各ネットワークデバイスはQdisc(キューイング規律)に接続され、すべての設定データを保持します。net/schedでは、チェーン、フィルター、アクションを導入してパケットの処理方法を決定します。チェーンはフィルターの順序付きリストであり、フィルターはパケットの特定の属性をチェックし、それに基づいて実行するアクションを決定します。net/schedはコンポーネントの再利用を最大化するように設計されており、複数のネットワークデバイスが同じQdiscを共有できます。重要なのは、アクションが同じネットワーク名前空間内で共有でき、「インデックス」によって一意に識別されることです。per-netのradixツリーaction_idrがすべてのアクションオブジェクトを記録し、インデックスによる検索を可能にします。これはtcf_idr_check_alloc関数によって行われます。
バグ
この脆弱性はロックの不一致です。tcf_idr_check_alloc()はrcu_read_lock()のみでアクションidrにアクセスするのに対し、アクションの解放はidrinfo->lockとrtnl_lock()を保持して行われますが、RCU grace periodを待たずに(つまり生のkfreeで)行われます。そのため、アクションが検索中に解放され、UAFシナリオが発生する可能性があります。UAFを成功させるには、同じウィンドウ内でアクションを解放し、かつ上書き(tcfa_refcntを非ゼロに設定)する必要があります。競合の基本構造は、CPU0でアクション検索、CPU1でアクション削除、CPU2で回収・tcfa_refcnt設定です。
初期段階ではRTM_NEWACTIONとRTM_DELACTIONを使用してバグを証明しましたが、これらの操作にはinit名前空間でのCAP_NET_ADMINが必要であり、使用不可能でした。代わりにRTM_NEWTFILTERとRTM_DELTFILTERを使用します。clsact qdiscとflowerフィルター(DOIT_UNLOCKEDフラグを設定)を使用することで、rtnl_lockを回避できます。このエクスプロイトには非特権ユーザーネームスペースの有効化も必要です。
レース最適化
最初の最適化は、timerfdとepollを用いたウィンドウ拡大手法です。timerfdで特定の時間を設定し、ハードウェア割り込みをトリガーして待機者全員にシグナルを送ります。多数のepollオブジェクトをアタッチすることで待機者リストを長くし、CPUを停滞させます。2つ目の最適化は、別のスレッドで別々のチェーンにフィルターを作成することです。各チェーンには独自のmutexがあり、独立したチェーンを使用することで競合が驚くほど高速化されました。3つ目の最適化は、エラーパスを利用した主要な再構築です。2種類のレーススレッド(binderとdeleter)を設定します。
Binderスレッドはフィルター作成のエラーパスを利用します。アクション42と無効形式のアクションを含む2つのアクションを持つフィルター作成リクエストを送信します。アクション42は正常に取得されますが(ここで競合が発生)、2つ目のアクションの読み取りが失敗して処理が中断され、フィルターは作成されず、アクション42もidrに挿入されません。これにより、新しく作成されたフィルターを削除するための2回目のnetlink操作のオーバーヘッドが削減されます。Deleterスレッドはアクション42を含むフィルターを作成して削除します。その役割は、アクション42をidrに挿入することです。最終設定では、N個のbinderスレッドと1個のdeleterスレッドが別々のCPUで実行され、すべてアクション42を操作します。これにより、エクスプロイト時間が15分以上から約5秒に短縮されました。
kASLRリーク
エクスプロイトの開始時に、著者はEntryBleed実装を使用してkASLRをリークしました。
エクスプロイトのプリミティブ
tc_actionは豊富なオブジェクトであり、回収後のコードパスには多くの有用なプリミティブがあります。例えば、user_cookie->dataとuser_cookieの任意kfree(データのみのエクスプロイトに使用可能)です。このエクスプロイトでは、ops仮想テーブルの間接呼び出しを使用しました。tc_actionはkmalloc-256キャッシュから割り当てられます。これをuser_key_payloadオブジェクトで回収します。最初の8バイトにNULLへのポインタ(kASLRリークで取得)を設定することで、クラッシュを回避します。より良い方法は、データ長を192バイトに設定して一時バッファが別のkmem_cacheに落ちるようにすることです。
RCU
RIP制御を得る前に、Linux RCUの仕組みを簡単に説明します。RCUは安価な同期機構で、すべての読み取りはgrace period内で行われ、書き込みはその後に行われます。call_rcu(head, func)関数は、ターゲットオブジェクト内のrcu_head構造体へのポインタと、そのオブジェクトを更新(この場合は解放)する関数ポインタを受け取り、更新を遅延させます。call_rcuはそれをper-cpuリンクリストに追加し、grace period後にすべての関数が呼び出されます。rcu_head構造体にはnextポインタとfunc関数ポインタが含まれます。user_key_payload内のrcu_headを制御することで、制御フローを乗っ取ることができます。