セキュリティ
GhostLock、15年間すべての Linux ディストリビューションに存在したスタック UAF
GhostLock, a stack-UAF that has existed in ALL Linux distributions for 15 years (nebusec.ai)
要約
この記事は、Linux カーネルに 15 年以上にわたり存在し、すべての主要ディストリビューションに影響を与えている重大な脆弱性である GhostLock (CVE-2026-43499) について詳述しています。この脆弱性は、特権や特別なカーネル設定なしに、特権昇格やコンテナエスケープに悪用される可能性があります。根本原因は、`remove_waiter()` 関数が `FUTEX_CMP_REQUEUE_PI` 操作におけるプロキシパスで誤って使用され、本来クリーンアップされるべきタスクとは異なるタスクの `pi_blocked_on` ポインタを NULL に設定してしまうことにあります。これにより、解放されたカーネルスタックメモリへのダングリングポインタが残り、スタック UAF (Use-After-Free) の脆弱性が生じます。
全文翻訳
技術的な内容をあまり求めていませんか?バグの概要を簡単に確認してください。バグの概要を読む → GhostLock (CVE-2026-43499) は、VEGA によって発見された Linux カーネルの脆弱性で、2011 年以降、すべての主要ディストリビューションに存在しています。このバグをトリガーするために、特別なカーネル設定や権限は必要ありません。97% の安定した特権昇格およびコンテナエスケープに変換されたことで、Google は kernelCTF で 92,337 ドルを私たちに報酬として与えました。この書き込みでは、エクスプロイトの技術的な詳細をカバーします。
vulnerability summary
GhostLock (CVE-2026-43499) は、特権を持たないローカル攻撃者が以下を実行できるようにします。
通常のスレッド syscall のみを使用して、カーネルスタックメモリへのダングリングカーネルポインタを取得する。
ほぼ任意のメモリ位置にポインタを書き込む。
関数テーブルをハイジャックして制御フローハイジャックを取得し、最終的にルートアクセスを取得する。
GhostLock は Linux 2.6.39 で導入され、Linux 7.1 で修正されました。Linux カーネルに 15 年以上にわたって存在していました。パッチが適用されていないすべての Linux ディストリビューションが影響を受けており、最新の LTS バージョンへのアップグレードを検討する必要があります。
お使いのブラウザは video タグをサポートしていません。
vulnerability analysis
エクスプロイト戦略を直接見たい
概要
GhostLock は、rtmutex のリワーク (8161239a8bcc、「rtmutex: PI アルゴリズムを簡素化し、最高優先度タスクがロックを取得できるようにする」) で導入され、約 15 年間そのままになっていましたが、2026 年 4 月の修正 (3bfdc63936dd、「rtmutex: remove_waiter() で current の代わりに waiter::task を使用する」) で修正されました。影響を受ける範囲は v2.6.39-rc1 から v7.1-rc1 までで、CONFIG_FUTEX_PI=y のみが要件であり、ケーパビリティやユーザー名前空間は不要です。
remove_waiter() は kernel/locking/rtmutex.c で current->pi_blocked_on をクリアします。これは、current がウェイトの所有者である通常の遅延パスでは正しいです。プロキシパスでは間違っています。rt_mutex_start_proxy_lock() は、別タスクのために rt_mutex_waiter をエンキューし、エラー時にはロールバックしますが、current はウェイトではなく要求者です。
ウェイトオブジェクトは、FUTEX_WAIT_REQUEUE_PI でスリープしているタスクのスタック上に存在します。FUTEX_CMP_REQUEUE_PI は、そのウェイトをターゲット PI futex にプロキシします。rtmutex チェーンウォークがデッドロックを報告すると、ロールバックはロックからウェイトをデキューしますが、要求者の pi_blocked_on をクリアします。ウェイトタスクは pi_blocked_on を自身のスタックフレームに指したままにし、ウェイトがユーザー空間に戻るとすぐにポップされます。その後のそのタスクを通る PI チェーンウォークは、ダングリングポインタをたどります。
根本原因
これは、多くの他のライフサイクルバグと同じ形状をしています。つまり、関数が、それが書かれたことのない呼び出し元によって再利用されているのです。
ヘルパー関数 remove_waiter() は、元々 1 つのシナリオのために書かれていました。つまり、スレッドが自身のロックをブロックし、その後自身でクリーンアップするというシナリオです。そのため、常に current (実行中のスレッドが何であれ) がクリーンアップすべきウェイトであると仮定し、それに応じて current->pi_blocked_on をクリアしていました。
しかし、Requeue-PI はその仮定を破ります。rt_mutex_start_proxy_lock() を介して、このヘルパーは現在、別のスリープ中のスレッドのためにクリーンアップするために使用されています。そのパスでは、current は実際のウェイトではなく、FUTEX_CMP_REQUEUE_PI を発行したスレッドです。
__rt_mutex_start_proxy_lock() が -EDEADLK を返すと、remove_waiter()、つまり誤用されたヘルパーを通じてロールバックします。
1 int __sched rt_mutex_start_proxy_lock(struct rt_mutex_base *lock, 2 struct rt_mutex_waiter *waiter, 3 struct task_struct *task) 4 { 5 int ret; 6 raw_spin_lock_irq(&lock->wait_lock); 7 ret = __rt_mutex_start_proxy_lock(lock, waiter, task); 8 if (unlikely(ret)) 9 remove_waiter(lock, waiter); // ret == -EDEADLK 10 raw_spin_unlock_irq(&lock->wait_lock); 11 return ret; 12 }
remove_waiter() は、間違ったタスクをスクラブします。
1 static void __sched remove_waiter(struct rt_mutex_base *lock, 2 struct rt_mutex_waiter *waiter) 3 { 4 ... 5 raw_spin_lock(¤t->pi_lock); 6 rt_mutex_dequeue(lock, waiter); 7 current->pi_blocked_on = NULL; // waiter->task であるべき 8 raw_spin_unlock(¤t->pi_lock); 9 ... 10 }
waiter はスリープ中のスレッド自身のスタック上に存在するオブジェクトであり、ここでの current はリクエストを行ったスレッドです。修正では、waiter->task->pi_lock をロックし、代わりに waiter->task->pi_blocked_on をクリアします。この問題は lockdep をすり抜けます。lockdep は pi_lock が保持されていることはチェックしますが、それが誰のものかはチェックしません。
-EDEADLK パスをトリガーする。
-EDEADLK ロールバックに到達するには、3 つの futex ワードと 3 つのスレッドからなる PI 依存関係サイクルが必要です。f_pi_chain は PI futex で、ウェイトタスクによって最初にロックされます。f_pi_target は PI futex で、オーナータスクによって最初にロックされます。これがリクエストターゲットです。f_wait は、ウェイトが FUTEX_WAIT_REQUEUE_PI でブロックする通常の futex です。シーケンスは次のとおりです。
ウェイトは f_pi_chain を取得し、FUTEX_WAIT_REQUEUE_PI(f_wait -> f_pi_target) でブロックします。その rt_mutex_waiter は現在、スタック上にあります。オーナーは f_pi_target を取得し、ウェイトが保持している f_pi_chain でブロックします。メインスレッドは FUTEX_CMP_REQUEUE_PI(f_wait -> f_pi_target) を呼び出します。リクエストは、ウェイトを f_pi_target にプロキシしようとします。f_pi_target のオーナーはすでに f_pi_chain を介してウェイトの後ろにブロックされているため、チェーンウォークはループ waiter -> f_pi_target -> owner -> f_pi_chain -> waiter を閉じます。これは -EDEADLK を返し、バグのあるロールバックを実行します。ウェイトはダングリング pi_blocked_on を持ったままウェイクアップします。
ここで重要な順序は、リクエスターがウェイトがまだ解放されるオブジェクトを所有している間にウェイトをロールバックすることであり、サイクルがステージングされると、それはそれ自体で発生します。サイクルが解決された後、時間的プレッシャーはまったくありません。ウェイトはダングリング pi_blocked_on を持ったままユーザー空間に残り、後続の sched_setattr() がチェーンをウォークするといつでもトリガーされる可能性があります。UAF ウィンドウは完全に開いています。
問題は、解放されたオブジェクトがカーネルスタック上に存在することです (futex syscall からの ret を「解放」と呼ぶ場合、スタック UAF)。それを回復するには、同じスタックの同じ深さ (オフセット) に制御されたバイトを戻すことができる syscall を見つける必要があります。
スタック UAF をトリガーする
3 つの futex サイクルのステージングにより、ウェイトタスクは pi_blocked_on が古い FUTEX_WAIT_REQUEUE_PI フレームを指すダングリング状態のままユーザー空間に残ります。その下のすべては、その 1 つのポインタに依存します。理解を深めるために 3 つのスレッドを使用していますが、レースに勝って UAF をトリガーするには、1 つの CPU コアで十分です。
GhostLock からの最初のプリミティブ
この時点で、解放されたカーネルスタックへのポインタを保持しており、それを rt_mutex_waiter として逆参照するカーネルアクセスを自由にトリガーできます。そのスタックに制御されたバイトをスプレーして、rt_mutex_waiter を完全に偽装できます。偽装された形状によっては、この 1 回のアクセスでいくつかのプリミティブが得られます。主な 2 つは次のとおりです。
任意の (ただし制約された) アドレスにポインタを書き込む
任意の (ただし制約された) アドレスにゼロバイト 8 バイトを書き込む
プリミティブが発火する前にいくつかのポインタ逆参照と整合性チェックが実行され、発火後カーネルは正常に返され、クラッシュしません。したがって、私たちの主な質問は、それぞれ以下のセクションで回答されます。
解放されたスタックメモリをどのように回復するか (スプレー)? → スタックの再利用
偽の rt_mutex_waiter を組み込みの構造チェックを通過させ、有効に見えるポインタを偽装するにはどうすればよいか? → 偽のウェイトから書き込みへ
どの書き込みプリミティブを使用し、どこに書き込むか?
プリミティブは「任意」のアドレスについて何を制約するか? → inet6_protos の使用
エクスプロイトの詳細
エクスプロイトの概要
プリフェッチ → カーネルイメージスライドと physmap ベースをリークする。
GhostLock → ウェイトタスクの pi_blocked_on にダングリング rt_mutex_waiter を残す。(スタック) UAF
回復 → PR_SET_MM_MAP を使用してウェイト自身のカーネルスタックを回復し、解放されたフレーム上に偽の rt_mutex_waiter を偽装する。
任意アドレスライター → Rtmutex rb-tree erase: 1 つの制約されたポインタ書き込み (その内容を回復できる)、関数テーブルを含む構造体を上書きする: inet6_protos[IPPROTO_UDP] = <CEA ポインタ>。
CPU エントリ領域 → ホスト {偽の inet6_protocol、ピボットスロット、ROP スタック} すべて既知のダイレクトマップアドレスに配置。
CFH をトリガーする → ループバック IPv6 UDP パケットが上書きされたハンドラを呼び出し、ピボットする。
DirtyMode → 1 回の書き込みで core_pattern のモードビットを反転させ、残りは純粋なユーザー空間 LPE。
私は経験豊富な...