はじめに
カーネルのローカル権限昇格(LPE)脆弱性「Copy Fail」(CVE-2026-31431)が話題になった。SNS や記事では「影響範囲は 〜6.19.12」「このコマンドを叩いて出たら脆弱」という情報が飛び交うが、数字とワンライナーだけ見て「うちは脆弱/安全」と即断するのは気持ち悪い。
なので、本番機をいじる代わりにパッチ前カーネルを載せた使い捨て VM を立て、公式 PoC を実際に走らせて、どのくらい簡単に root を取られるのかを手で確かめた。結論から言うと、732 バイトの Python 一発で非 root ユーザーから uid=0(root) まで、レースも何も無く 100% 決定的に上がれた。あわせて、よく出回っていた「チェック法」が実はパッチ判定になっていないこと、そして緩和策が効くこともその場で検証した。
数字だけで安心しない
Copy Fail のような派手な CVE が出ると、まず出回るのは「影響を受けるバージョン一覧」と「これを実行して X が出たら脆弱」というワンライナーだ。便利だが、二つ落とし穴がある。
- 影響範囲の数字は「修正前」の話で、ディストリのバックポートを反映していない。 Arch なら自分が載せているパッケージの fixed version をトラッカーで見ないと、上流の数字だけでは判断できない。
- 出回る「チェック法」がパッチ判定とは限らない。 今回見かけたものは「
algif_aeadがロードされているか」を見るタイプだった。だがこれは攻撃に使うモジュールが載っているかを見ているだけで、カーネルにパッチが当たっているかは何も言っていない。ロードされていなくてもmodprobeできれば攻撃経路は復活しうるし、ロードされていてもパッチ済みなら昇格はしない。つまり、このチェックは脆弱性の有無とは別物だ。
ここに気づいてしまうと、「自分の環境は本当のところどうなのか」をコマンドの出力ではなく挙動で知りたくなる。けれど本番機で root 昇格 PoC を走らせるのは筋が悪い。そこで使い捨て VM の出番になる。
パッチ前カーネルを VM に隔離して、本物を走らせる
方針はシンプルだ。
- 本番機(パッチ済み)は触らない。
- パッチ前のカーネルが載った VM を使い捨てで立てる。
- そこに公式 PoC(自作スニペットではなく一次情報)を持ち込んで走らせる。
- 終わったら VM ごと捨てる。
ポイントは「コンテナではなく VM」であること。Copy Fail のようなカーネル LPE は、成功すれば コンテナの分離も貫通する(カーネルは共有なので当然)。隔離したいなら、カーネルごと分離される VM でなければ意味がない。
使い捨て VM を立てて PoC を当てる
cloud image に SSH 鍵を仕込んだ cloud-init の user-data を用意して、KVM で直に起動する。hostfwd でホストの 2222 番をゲストの 22 番へ転送し、localhost:2222 に SSH する形にした。
# パッチ前カーネルを載せた使い捨て VM を起動(ユーザー権限・SLIRP ネット)qemu-system-x86_64 -enable-kvm -m 2048 -smp 2 \ -drive file=sandbox.qcow2,format=qcow2,if=virtio \ -drive file=seed.iso,format=raw,if=virtio \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-pci,netdev=net0 -nographic使い捨て前提なので、SSH の host key 検証は外しておくと取り回しが楽だ(本番では絶対にやらない設定)。
SSHOPT="-p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"ssh $SSHOPT ubuntu@localhostゲストにログインしたら、まず uname -r でパッチ前カーネルであることを確認し、公式 PoC を一次情報から取得して走らせる。出回っているコピペではなく、出どころのはっきりした PoC を使うのは、何が再現できたのかを正しく語るために大事だ。
なぜ「そんなに簡単」なのか
走らせて一番ぞっとしたのは、その素っ気なさだった。
- PoC の本体は 732 バイトの Python 一発。
- 仕掛けは AF_ALG(カーネルの暗号 API)+ splice で、
/usr/bin/suの page cache を 4 バイトずつ書き換えるというもの。 - 非 root の
ubuntuから実行して、一瞬でuid=0(root)。レースも何も無く 100% 決定的に上がる。
LPE と聞くと「タイミングを突くレース条件で、何度か試して稀に成功する」イメージを持っていたが、これは違った。条件が揃えば必ず通る。これが「自分のマシンで」「パッチ前なら」起きると体で分かると、固定して更新を止める運用がいかに割に合わないかが腹落ちする。
もう一つの教訓は、怖がる方向を間違えないことだ。「野良の make install が危険」なのは LPE 以前からで、そもそも sudo make install なら LPE すら要らず最初から root だ。LPE が足すのは「非 root 実行でも被害がマシン全体へ拡大する」という部分であって、信用できないコードを動かした時点で(root かどうかに関係なく)自分の鍵もトークンもデータも危ない、という前提は変わらない。
緩和も実際に効くことを確かめた
昇格が通ることを確認したあと、緩和策も同じ VM で試した。攻撃経路である algif_aead を modprobe で封じる方法だ。
# 攻撃に使われるモジュールのロードを潰す(パッチと独立に効く)echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/copyfail.confこれを入れた状態で PoC を再実行すると、昇格は通らなくなった。パッチを当てるのが本筋だが、すぐにカーネルを上げられない事情があるなら、この緩和が独立に効く保険になる。
検証が終わったら、VM のプロセスが残っていないかを確認して片付ける。使い捨てなのでディスクイメージごと捨ててよい。
pgrep -af qemu-system # 残骸がないか確認してから片付けまとめ
- 影響範囲の数字や「これを叩け」式のチェックを鵜呑みにしない。出回っていたチェックはパッチ判定ではなく、攻撃モジュールが載っているかを見ているだけだった。
- 気になるなら本番を触らず、パッチ前カーネルの使い捨て VM で本物の PoC を走らせて挙動で確かめる。コンテナを貫通する脅威なので、隔離はコンテナではなく VM。
- Copy Fail は 732 バイトで非 root → root、レース無しで決定的。LPE は「運が悪いと起きる」ものではないと体で分かった。
- 本筋はパッチ。すぐ上げられないなら
algif_aeadを封じる緩和が独立に効く(VM で効果を確認済み)。