
CVE-2026-31431の概念実証エクスプロイト。splice()と細工されたソケットオプションを介したLinuxカーネルのローカル権限昇格であり、任意のカーネルメモリ書き込みを可能にしてsetuidバイナリを上書きします。
copyFail.pyは、Linuxカーネルのローカル権限昇格の脆弱性を実証する概念実証(PoC)エクスプロイトです。splice()システムコールを巧妙に細工されたソケットオプションと組み合わせて悪用することで、任意のカーネルメモリを破壊する能力を獲得します。この能力を利用して、ディスク上の任意のsetuidバイナリをカスタムELF実行ファイルで上書きし、改ざんされたバイナリを実行して昇格した権限を取得します。
攻撃タイプ: ローカル権限昇格(LPE) 影響: 完全なrootシェルアクセス(setuid-rootバイナリを標的にした場合) 複雑度: 低(単一スクリプト、外部依存関係なし)
使用方法:
./copyFail_cleaned.py [target_binary] # デフォルト: /usr/bin/su
元のPoCは/usr/bin/suをハードコードしていましたが、根本的なカーネル脆弱性は任意のファイルに適用されます — 攻撃者は選択した任意のsetuidバイナリを上書きできます。クリーンアップ版はターゲットを指定するためのオプションのコマンドライン引数を受け付け、この動作を明示的にしています。
Linuxカーネルは、2つのファイルディスクリプタ間でデータをカーネル内部で完全にコピーする splice()システムコールを提供しています — 先にユーザースペースにデータをコピーすることはありません。これは「ゼロコピーI/O」と呼ばれ、Webサーバーやリバースプロキシなどの高スループットアプリケーションでパフォーマンス向上のために使用されます。
この脆弱性は、サポートされていないプロトコルレベルのソケットオプションで構成されたPACKETソケットに対してsplice()が使用された場合に、カーネルが特定のソケット構造の内部状態を適切に検証しないために発生します。これらのオプションを慎重に細工することで、攻撃者はカーネルポインタを破壊し、その後splice()を使用してカーネルメモリの書き込みを任意の場所にリダイレクトできます。
簡単に言うと、カーネルには内部でデータを移動するためのショートカットがあり、このエクスプロイトはそのショートカットを欺いて、データを誤った場所 — 具体的にはシステムバイナリのメモリ内コピー — に書き込ませます。
エクスプロイトは明確な一連のステップに従います:
エクスプロイトはraw PACKETソケット(AF_PACKET / SOCK_RAW)を作成し、意図的に不正なアドレスにバインドします。次に、Linuxが認識しないSolarisレベルの定数であるSOL_PNIO(プロトコルレベル279)を使用してsetsockopt()を呼び出します。
これが重要な理由: カーネルがsetsockopt()でサポートされていないプロトコルレベルに遭遇すると、データを適切に検証しない汎用ハンドラにフォールスルーします。これにより、カーネルメモリが一貫性のない、悪用可能な状態のままになります。
accept()を介してソケット接続を確立した後、エクスプロイトはsendmsg()を使用して巧妙に細工された補助(制御)メッセージを送信します。これらのメッセージは意図的に不一致の長さヘッダを持っています — 一部は実際より短いと主張し、他は実際より長いと主張します。
これが重要な理由: カーネルの制御メッセージパーサーは、ポインタ演算にこれらの長さを使用します。不一致の長さにより、パーサーが意図した境界を超えて読み書きし、隣接するカーネル構造 — 具体的には、後でsplice()が追従するポインタ — を破壊します。
エクスプロイトはパイプを作成し、splice()を2回呼び出します:
/usr/bin/suからパイプへデータをスプライスします。これが重要な理由: splice()呼び出しはステップ2で仕込まれた破損したカーネルポインタを追従し、ソケットバッファの代わりに攻撃者が選択したカーネルメモリアドレスにデータを書き込ませます。これにより、エクスプロイトは任意のカーネルメモリ書き込みプリミティブを獲得します。
エクスプロイトのペイロードは160バイトのELF実行ファイルです(下記参照)。エクスプロイト関数の各呼び出しは正確に4バイトを書き込みます。したがって、メインループは40回実行されます(160 / 4 = 40)。毎回:
40回の反復の後、/usr/bin/suの最初の160バイトが上書きされています。
最後に、エクスプロイトはos.system("su")を実行します。カーネルはページキャッシュ(現在は実際のsuバイナリの代わりに攻撃者のシェルコードが含まれている)から変更された/usr/bin/suをロードし、シェルコードが実行されます。
埋め込まれたペイロードは、以下のシェルコードを含む160バイトのx86-64 ELF実行ファイルに展開されます:
; システムコール105(execveat)を試行 — 古いカーネルでは利用できない場合あり
xor eax, eax
xor edi, edi
mov al, 0x69 ; システムコール105
syscall
; フォールバック: システムコール59(execve)— 信頼性の高い経路
lea rdi, [rip+0xf] ; RDI = "/bin/sh"へのポインタ
xor esi, esi ; RSI = NULL(envp)
push 0x3b ; システムコール59番号
pop eax
cdq ; RDX = NULL(argv)
syscall
; クリーンに終了
xor edi, edi
push 0x3c ; システムコール60(exit)
pop eax
syscall
; データセクション: "/bin/sh\0\0\0"
機能: 引数なしで/bin/shを起動し、suを実行したプロセスの権限を継承します。エクスプロイトがroot(またはsuにrootアクセスを付与するケーパビリティ)として実行される場合、結果として得られるシェルはrootシェルになります。
実行しないこと:
これは、研究用PoCとして一貫した、単純な一発型の権限昇格です。
copyFail.py
├── hex_to_bytes() — 16進文字列デコーダ
├── exploit_splice() — コアエクスプロイト(ソケットセットアップ + 破壊 + splice)
│ ├── フェーズ1: PACKETソケット作成、バインド、setsockopt(SOL_PNIO)
│ ├── フェーズ2: accept()接続
│ ├── フェーズ3: 巧妙に細工された補助メッセージでsendmsg()
│ ├── フェーズ4: pipe() + splice()でカーネルメモリを破壊
│ └── フェーズ5: recv()試行(破壊を確固たるものにする)
└── メインループ:
├── /usr/bin/suを開く(読み取り専用)
├── 埋め込まれたペイロードを展開(zlib → 160バイトELF)
├── ループ: 反復ごとに4バイトを注入(合計40回)
└── 改ざんされたsuを実行 → rootシェル
エクスプロイトは/usr/bin/suをO_RDONLY(読み取り専用)で開きます。書き込みアクセスは不要です。なぜなら、破壊はカーネルページキャッシュ — 変更をディスクにフラッシュする前にカーネルが使用するファイルのメモリ内コピー — を通じて発生するからです。splice()ベースの書き込みは通常のファイル権限を完全にバイパスし、直接カーネルメモリに到達します。
/usr/bin/suバイナリ(パッケージマネージャとハッシュを比較)setsockopt()呼び出しsplice()システムコールsysctlを使用して、非特権ユーザーによるAF_PACKETソケット作成を制限copyFail.py(配布版)copyFail_cleaned.py(同じディレクトリ)net/packet/、fs/splice.c、net/core/sock.cこのレポートはセキュリティ研究および防御分析のみを目的としています。
| 定数 | 値 | エクスプロイトでの目的 |
|---|
AF_PACKET | 17 | rawパケットソケットファミリ |
SOCK_RAW | 3 | rawソケットタイプ |
SOL_PNIO | 279 | サポートされていないプロトコルレベル(Solaris) |
MSG_DONTWAIT | 0x400 | 非ブロッキングsendmsgフラグ |