WADA-DEV(7) $ /ja/blog/copy-fail-lpe-vm-experiment/

NAME

copy-fail-lpe-vm-experiment

SYNOPSIS

話題になったカーネル LPE「Copy Fail」を、影響範囲の数字だけで脆弱/安全を即断せず、使い捨て VM にパッチ前カーネルを載せて公式 PoC を実際に走らせてみた。732 バイトの Python 一発で非 root から uid=0 へ。レースも無く 100% 決定的だった。記事のチェック法が「パッチ判定ではない」と気づいた話と、緩和の検証まで。

DESCRIPTION

はじめに

カーネルのローカル権限昇格(LPE)脆弱性「Copy Fail」(CVE-2026-31431)が話題になった。SNS や記事では「影響範囲は 〜6.19.12」「このコマンドを叩いて出たら脆弱」という情報が飛び交うが、数字とワンライナーだけ見て「うちは脆弱/安全」と即断するのは気持ち悪い。

なので、本番機をいじる代わりにパッチ前カーネルを載せた使い捨て VM を立て、公式 PoC を実際に走らせて、どのくらい簡単に root を取られるのかを手で確かめた。結論から言うと、732 バイトの Python 一発で非 root ユーザーから uid=0(root) まで、レースも何も無く 100% 決定的に上がれた。あわせて、よく出回っていた「チェック法」が実はパッチ判定になっていないこと、そして緩和策が効くこともその場で検証した。

数字だけで安心しない

Copy Fail のような派手な CVE が出ると、まず出回るのは「影響を受けるバージョン一覧」と「これを実行して X が出たら脆弱」というワンライナーだ。便利だが、二つ落とし穴がある。

  1. 影響範囲の数字は「修正前」の話で、ディストリのバックポートを反映していない。 Arch なら自分が載せているパッケージの fixed version をトラッカーで見ないと、上流の数字だけでは判断できない。
  2. 出回る「チェック法」がパッチ判定とは限らない。 今回見かけたものは「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/supage cache を 4 バイトずつ書き換えるというもの。
  • 非 root の ubuntu から実行して、一瞬で uid=0(root)レースも何も無く 100% 決定的に上がる。

LPE と聞くと「タイミングを突くレース条件で、何度か試して稀に成功する」イメージを持っていたが、これは違った。条件が揃えば必ず通る。これが「自分のマシンで」「パッチ前なら」起きると体で分かると、固定して更新を止める運用がいかに割に合わないかが腹落ちする。

もう一つの教訓は、怖がる方向を間違えないことだ。「野良の make install が危険」なのは LPE 以前からで、そもそも sudo make install なら LPE すら要らず最初から root だ。LPE が足すのは「非 root 実行でも被害がマシン全体へ拡大する」という部分であって、信用できないコードを動かした時点で(root かどうかに関係なく)自分の鍵もトークンもデータも危ない、という前提は変わらない。

緩和も実際に効くことを確かめた

昇格が通ることを確認したあと、緩和策も同じ VM で試した。攻撃経路である algif_aeadmodprobe で封じる方法だ。

# 攻撃に使われるモジュールのロードを潰す(パッチと独立に効く)
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 で効果を確認済み)。

参考リンク

TAGS

security · Linux · kernel · CVE · 権限昇格 · QEMU

SEE ALSO