タグアーカイブ Linux

Copy Fail脆弱性にCloudflareがどう立ち向かったか

Copy Fail脆弱性にCloudflareがどう立ち向かったか

2026年4月29日、Linuxカーネルにローカル権限昇格をもたらす脆弱性「Copy Fail(CVE-2026-31431)」が公開された。この脆弱性を悪用すれば攻撃者はroot権限を取得でき、多くのサーバが影響を受け得る深刻なものだ。

Cloudflareは世界中の330都市に展開する大規模なLinuxサーバ基盤を運用している。同社は開示直後からセキュリティチームとエンジニアリングチームが動き、既存の振る舞い検知が数分で攻撃パターンを特定できることを確認し、また再起動不要の緩和策としてeBPF LSMを展開した。結果として顧客データへの影響やサービス停止は一切発生していない。

Copy Fail脆弱性(CVE-2026-31431)の全容

Copy Fail脆弱性(CVE-2026-31431)の全容

Linuxカーネルのcrypto APIには、AF_ALGソケットファミリ経由で一般ユーザプロセスが暗号化・復号を要求できる仕組みがある。ここで問題となったのは「aead」テンプレートを用いるモジュール `algif_aead` の欠陥だ。2017年に導入されたin-place最適化によって、復号時に割り当てられた出力領域を超えた4バイトの書き込みが発生するようになっていた。

攻撃者はまず `splice()` システムコールを使い、`/usr/bin/su` のようなsetuidバイナリのファイル記述子からページキャッシュの参照を暗号化操作のscatterlistにチェインさせる。その状態で `recvmsg()` を呼ぶと、本来許される範囲外の4バイトがターゲットのページキャッシュに書き込まれる。汚染されたバイナリを `execve()` で実行すれば、root権限で任意のコードが動くという筋書きだ。

1. 攻撃用AF_ALGソケットを作成
socket(AF_ALG, SOCK_SEQPACKET, 0) でAEADテンプレートにバインド
2. splice()でページキャッシュ参照を注入
setuidバイナリ(例:/usr/bin/su)のファイル記述子からページキャッシュを暗号scatterlistにチェイン
3. recvmsg()で範囲外書き込み
復号処理中に4バイトのスクラッチデータがターゲットページキャッシュへ書き込まれる
4. execve()で改ざんバイナリを起動、root権限取得
ページキャッシュが汚染された状態でsetuid-rootプログラムを実行し、シェルコードがrootとして動作
※攻撃者は任意のファイル、オフセット、書き込む4バイトの内容を制御可能

このエクスプロイトの流れは、Cloudflareのブログで詳述された技術情報と、Xint Codeによる元の開示記事を基にしている。Linuxコミュニティはコミット a664bf3d603d で2017年の最適化を差し戻しており、それが正式な修正となる。

CloudflareのLinuxカーネル管理プロセス

CloudflareのLinuxカーネル管理プロセス

CloudflareはカスタムLinuxカーネルを自前でビルドし、コミュニティの長期サポート(LTS)バージョンをベースにしている。新型カーネルの選定からグローバル展開まで、およそ4週間のサイクルでシステム的なアップデートと再起動を実施している。公開前に既知のセキュリティパッチがマージされるのが常だが、Copy Failの修正はメインラインにマージされてから1ヶ月経っても主要LTSラインへのバックポートが完了していなかった。このタイムラグが生じたため、Cloudflareの大部分のサーバは6.12 LTSカーネルを稼働させており、脆弱性が残る状態だった。

Cloudflareの多層防御:検知から再起動不要の緩和まで

Cloudflareの多層防御:検知から再起動不要の緩和まで

振る舞いベースの検出が数分で作動

Cloudflareのエンドポイントには、プロセスの振る舞いを常時監視する検知プラットフォームが導入されている。特定の脆弱性シグネチャに頼るのではなく、通常とは異なる実行パターンを検出する仕組みだ。専用ルールの更新や人の介入なしに、社内で実施した検証でエクスプロイトの試行が数分以内に悪性と判定され、アラートが発報された。これは攻撃チェーン全体(スクリプトインタプリタ → AF_ALG経由の暗号サブシステム呼び出し → 権限昇格バイナリの実行)を一つの振る舞いパターンとして捉えた結果だ。

脅威ハンティングと過去48時間のログ調査

セキュリティチームは「公開前から悪用されていた可能性を前提にする」という原則に立ち、エクスプロイトが残すカーネルログの痕跡を独自の集約ログ基盤で検索した。また、関係するシステムへの全アクセスログを収集し、接続元や実行コマンドを再構成、システムバイナリのハッシュ整合性を検証した。その結果、過去48時間においてCloudflareのインフラ上で悪用された証拠は一切確認されなかった。

eBPF LSMによる緊急緩和の展開

根本対策であるパッチ済みカーネルのロールアウトには時間がかかるため、チームは無効化モジュール `algif_aead` の削除をまず試みた。しかし実際にはレガシーな社内サービスがcrypto APIを利用しており、削除すると障害を招くことがステージング環境のテストで判明した。そこで再起動不要の外科的対策として、BPF Linux Security Module(bpf-lsm)を使ったプログラムを導入した。

# 素朴な緩和(モジュール無効化)は依存関係のため断念
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf
rmmod algif_aead 2>/dev/null || true

bpf-lsmプログラムは `socket_bind` LSMフックにアタッチし、AF_ALGソケットを開こうとするバイナリのパスをホワイトリストと照合する。許可リストにないバイナリからの `bind()` 呼び出しは拒否するため、悪用の入口を完全に封鎖する。このアプローチを採る前に、Prometheus eBPF Exporterを使って艦隊全体のAF_ALG利用実態を可視化し、許可リストに載せるべき正当なサービスが本当に1つだけであることを検証した。

# eBPF LSMプログラムの擬似フロー
- ソケットファミリがAF_ALGでなければ通過
- AF_ALGの場合、呼び出し元バイナリのパスを許可リストと照合
- 許可されていればbindを許可、それ以外は拒否

これにより、大部分のサーバはパッチ済みカーネルが配布されるまでの間、bpf-lsmによって保護された。テスト用ノードで実際にエクスプロイトコードを実行し、PermissionError が返され攻撃が不可能になったことを確認している。

緩和前(非パッチカーネル)
攻撃者のbind()成功 → recvmsg()で範囲外書き込み → root取得
bpf-lsm導入後(再起動なし)
非許可バイナリからのAF_ALG bindを拒否 → PermissionError → 攻撃失敗
※許可リスト内の正規サービスは影響を受けずに動作を継続

一連の対応から得た教訓と今後の改善

一連の対応から得た教訓と今後の改善

Cloudflareは今回の対応を通じていくつかの改善点を特定した。まず、カーネルAPIの依存関係をより深く可視化し、将来の緊急緩和時にサービス停止を避けられるようにすること。次にbpf-lsm自体の展開速度やログの充実を図り、ランタイム防御の即応性を高めること。さらに、カーネルコンフィギュレーションの監査を進め、使われていないモジュールや機能を事前にビルドから除去することで攻撃対象領域を縮小していく方針だ。

今回のインシデントで、Cloudflareは顧客影響ゼロを達成した。パッチ済みカーネルのリリースとbpf-lsmによるレイヤーが艦隊全体に行き渡り、脆弱性が悪用される余地は残らなかった。Linuxコミュニティの責任ある開示、社内の可視化ツール、そしてbpf-lsmというプリミティブが、迅速な防御を可能にしたといえる。

この記事のポイント

  • Linuxカーネルの脆弱性「Copy Fail」はローカルからroot権限を奪取できる深刻な問題
  • Cloudflareは公開と同時に既存の振る舞い検知で即座に捕捉し、過去ログの脅威ハンティングで未然悪用がないことを確認
  • 再起動不要の緩和策としてeBPF LSMを導入し、AF_ALGソケットへの不正アクセスをホワイトリスト方式で遮断
  • 根本パッチのロールアウトと並行して運用し、結果的に顧客データやサービスへの影響は皆無
  • 可視化ツール(Prometheus eBPF Exporter)の事前整備が緩和策の意思決定を支えた
BPFバックドアのマジックパケットをZ3で自動生成する手法

BPFバックドアのマジックパケットをZ3で自動生成する手法

Linuxマルウェア解析の現場で、手作業による逆アセンブリがボトルネックになっている。特に、Berkeley Packet Filter(BPF)ソケットプログラムに隠された「マジックパケット」待ち受け型のバックドアは、フィルタが数百命令に及ぶこともあり、解析に膨大な時間を要する。

Cloudflareのセキュリティ研究者らはこの課題に対し、シンボリック実行とZ3定理証明器を組み合わせた自動化手法を開発した。これにより、従来は数時間から数日かかっていたマジックパケットの特定を、数秒で完了させられるようになった。本記事では、その技術的アプローチと実装の詳細を解説する。

BPFがマルウェアに利用される理由

BPFがマルウェアに利用される理由

Berkeley Packet Filter(BPF)は、ネットワークスタックから特定のパケットを効率的に取り出すためのカーネル内技術だ。tcpdumpなどのツールでおなじみの「クラシックBPF」は、2つのレジスタしか持たないシンプルな仮想マシンで、高速なパケットフィルタリングを実現する。

ユーザー空間から見えなくなる特性

このBPFがマルウェア作者に好まれる理由は、その「不可視性」にある。カーネル深くで動作するBPFプログラムは、特定の条件を満たすパケットだけをユーザー空間のプロセスに渡すことができる。逆に言えば、条件を満たさないパケットは、ユーザー空間のネットワーク監視ツールから完全に隠蔽できる。

これにより、攻撃者は「マジックパケット」と呼ばれる特定のバイト列を含むパケットが到着するまで、バックドアを完全に休眠状態に保てる。通常のポートスキャンでは検出されず、ネットワーク上に痕跡を残さない、極めて隠密性の高い持続的脅威(APT)が実現する。

手動解析の限界

マルウェア対策の研究者がこの種のバックドアを分析する場合、BPFのバイトコードを逆アセンブルし、条件分岐を一つずつ追跡する必要があった。20命令程度の単純なフィルタなら問題ないが、実際には100命令を超える複雑なロジックを持つサンプルが観測されている。

Cloudflare Blogの記事によると、複雑なBPFプログラムの手動解析には「少なくとも1日」を要する場合があったという。この時間的コストが、脅威の早期分析と対策の迅速な展開を妨げるボトルネックとなっていた。

BPFDoorの実例から見るBPFフィルタ

BPFDoorの実例から見るBPFフィルタ

この手法の具体例として、高度なLinuxバックドア「BPFDoor」のBPFフィルタを見てみる。Fortinetが分析したサンプル(ハッシュ値: 82ed617816453eba2d755642e3efebfcbd19705ac626f6bc8ed238f4fc111bb0)の逆アセンブル結果は次の通りだ。

(000) ldh [0xc]                   ; オフセット12から2バイト読み込み(EtherType)
(001) jeq #0x86dd, jt 2, jf 6     ; 0x86DD(IPv6)なら002へ、そうでなければ006へ
(002) ldb [0x14]                  ; オフセット20から1バイト読み込み(プロトコル)
(003) jeq #0x11, jt 4, jf 15      ; 0x11(UDP)なら004へ、そうでなければ015(DROP)へ
(004) ldh [0x38]                  ; オフセット56から2バイト読み込み(宛先ポート)
(005) jeq #0x35, jt 14, jf 15     ; 0x35(DNSポート53)なら014(ACCEPT)へ、そうでなければ015へ
(006) jeq #0x800, jt 7, jf 15     ; 0x800(IPv4)なら007へ、そうでなければ015へ
(007) ldb [23]                    ; オフセット23から1バイト読み込み(プロトコル)
(008) jeq #0x11, jt 9, jf 15      ; 0x11(UDP)なら009へ、そうでなければ015へ
(009) ldh [20]                    ; オフセット20から2バイト読み込み(フラグメント)
(010) jset #0x1fff, jt 15, jf 11  ; フラグメントされていれば015へ、そうでなければ011へ
(011) ldxb 4*([14]&0xf)           ; インデックスレジスタXに(IHL & 0xF)*4をロード
(012) ldh [x + 16]                ; オフセットX+16から2バイト読み込み(宛先ポート)
(013) jeq #0x35, jt 14, jf 15     ; 0x35(DNSポート53)なら014へ、そうでなければ015へ
(014) ret #0x40000 (ACCEPT)       ; パケット受理
(015) ret #0 (DROP)               ; パケット破棄

このフィルタは、IPv6パケットとIPv4パケットの両方の経路でDNSポート(53)へのUDPパケットを待ち受ける。IPv4の経路ではさらに、パケットがフラグメントされていないこと、IPヘッダ長が標準の20バイトであることなどの追加チェックが入る。

ACCEPTに至る2つの経路

上記のコードから、パケットがACCEPT(受理)される条件は2つの経路で満たされることがわかる。

  • 経路1(IPv6): EtherTypeが0x86DD(IPv6)→ プロトコルが0x11(UDP)→ 宛先ポートが0x35(53)
  • 経路2(IPv4): EtherTypeが0x0800(IPv4)→ プロトコルが0x11(UDP)→ フラグメントなし → IPヘッダ長が5(20バイト)→ 宛先ポートが0x35(53)

手動で分析すれば、これらの条件から「DNSポート53へのUDPパケット」がマジックパケットの候補だと推測できる。しかし、より複雑な算術演算やビット演算が絡むフィルタの場合、この推測は困難を極める。

シンボリック実行とZ3による自動化

シンボリック実行とZ3による自動化

Cloudflareの研究者らは、この「制約条件を満たす入力値の発見」という問題を、シンボリック実行と定理証明器Z3によって自動化するアプローチを取った。

シンボリック実行の基本概念

シンボリック実行とは、プログラムの入力を具体的な値ではなく「記号(シンボル)」として扱い、実行経路を数学的な制約の集合として表現する手法だ。BPFプログラムの場合、入力となるネットワークパケットの各バイトを未知の変数とみなす。

プログラムが条件分岐(jeqなど)に到達すると、「変数Aが値Bと等しい」という制約が真となる経路と、偽となる経路の両方を探索する。最終的にACCEPT命令に到達する経路において、変数が満たすべきすべての制約を収集する。

Z3定理証明器による制約解決

収集された制約を、Microsoft Researchが開発した定理証明器「Z3」に与える。Z3はこれらの制約を満たす具体的な変数値(つまり、パケットの各バイトの値)を自動的に計算する。

このプロセスは、複数の連立方程式を解くことに似ている。ただし、方程式が単純な等号ではなく、ビット演算、比較、条件分岐を含む複雑な論理式となる点が異なる。

最短経路の探索アルゴリズム

すべてのACCEPT経路を探索する前に、まず最短の経路を見つける。これは、後続のシンボリック実行の計算コストを抑えるためだ。擬似コードで示すと、次のような幅優先探索(BFS)が用いられる。

paths = []
queue = deque([(0, [0])])  # (プログラムカウンタ, 経路履歴)

while queue:
    pc, path = queue.popleft()
    if pc >= len(instructions):
        continue

    instruction = instructions[pc]

    if instruction.class == return_instruction:
        if instruction_constant != 0:  # ACCEPTの場合
            paths.append(path)
        continue  # DROPまたはACCEPTでこの経路の探索終了

    if instruction.class == jump_instruction:
        if instruction.operation == unconditional_jump:
            next_pc = pc + 1 + instruction_constant
            queue.append((next_pc, path + [next_pc]))
            continue

        # 条件付きジャンプの場合、真偽両方の経路を探索
        pc_true = pc + 1 + instruction.jump_true
        pc_false = pc + 1 + instruction.jump_false
        queue.append((pc_true, path + [pc_true]))
        queue.append((pc_false, path + [pc_false]))
    else:
        # 逐次実行命令の場合、次の命令へ
        queue.append((pc + 1, path + [pc + 1]))

このアルゴリズムを先ほどのBPFDoorフィルタに適用すると、より短いIPv6経路(命令000→001→002→003→004→005→014)が最短経路として特定される。

BPFシンボリック実行マシンの実装

BPFシンボリック実行マシンの実装

最短経路がわかれば、次はその経路上でシンボリック実行を行うマシンを実装する。Cloudflareが開発した「BPFPacketCrafter」クラスの骨格は以下のようになる。

class BPFPacketCrafter:
    MIN_PKT_SIZE = 64           # 最小パケットサイズ
    LINK_ETHERNET = "ethernet"  # イーサネットヘッダから始まる
    MEM_SLOTS = 16              # スクラッチメモリM[0]〜M[15]

    def __init__(self, instructions, pkt_size=128, ltype="ethernet"):
        self.instructions = instructions
        self.pkt_size = max(self.MIN_PKT_SIZE, pkt_size)
        self.ltype = ltype

        # シンボリックなパケットバイト(各バイトが独立した変数)
        self.packet = [BitVec(f"pkt_{i}", 8) for i in range(self.pkt_size)]

        # シンボリックなレジスタ(32ビット)
        self.A = BitVecVal(0, 32)  # アキュムレータ
        self.X = BitVecVal(0, 32)  # インデックスレジスタ

        # スクラッチメモリ
        self.M = [BitVecVal(0, 32) for _ in range(self.MEM_SLOTS)]

ここでBitVecはZ3が提供するビットベクトル(固定長のビット列)型で、パケットの各バイトを8ビットの未知変数として表現する。レジスタAとXも同様に32ビットのビットベクトルとしてモデル化される。

BPF命令のZ3操作へのマッピング

シンボリック実行マシンは、BPFの各命令を対応するZ3の操作に変換しながら実行する。例えば、加算命令(ADD)は次のように処理される。

def _execute_ins(self, insn):
    cls = insn.cls
    if cls == BPFClass.ALU:  # 算術論理演算命令
        op = insn.op
        src_val = BitVecVal(insn.k, 32) if insn.src == BPFSrc.K else self.X
        if op == BPFOp.ADD:
            self.A = self.A + src_val  # Z3の加算演算でレジスタAを更新

比較命令(jeq)の場合は、条件式を制約として記録し、分岐先のプログラムカウンタへ実行を進める。クラシックBPFの命令セットは小さいため、このマッピングは比較的容易に実装できる。

制約の収集とパケット生成

最短経路に沿ってシンボリック実行を進めると、ACCEPT命令に到達した時点で、パケット変数が満たすべき制約の集合が完成する。Z3ソルバーはこの制約集合を解き、各pkt_i変数に具体的なバイト値を割り当てる。

得られた制約の例を、Z3が内部で生成する式の形で示すと以下のようになる。

0x86DD == ZeroExt(16, Concat(pkt_12, pkt_13))
0x11 == ZeroExt(24, pkt_20)
0x35 == ZeroExt(16, Concat(pkt_56, pkt_57))

これは、「オフセット12-13の2バイト(ビッグエンディアン)が0x86DD(IPv6)と等しい」「オフセット20の1バイトが0x11(UDP)と等しい」「オフセット56-57の2バイトが0x35(ポート53)と等しい」という3つの制約を表す。

Z3がこれらの制約を満たす解(例えばpkt_12=0x86, pkt_13=0xDD, pkt_20=0x11, ...)を求めると、それを実際のバイト列に変換する。最後に、Pythonのパケット操作ライブラリscapyを使って、このバイト列からネットワークパケットオブジェクトを組み立てる。

###[ Ethernet ]###
  dst       = 00:00:00:00:00:00
  src       = 00:00:00:00:00:00
  type      = IPv6
###[ IPv6 ]###
     version   = 6
     nh        = UDP
     src       = ::
     dst       = ::
###[ UDP ]###
        sport     = 0
        dport     = domain  # ポート53

生成されたパケットは、分析者がネットワーク上でバックドアの活性化テストを行う際の入力として、または検出用のシグネチャ作成のベースとして利用できる。

ツール「filterforge」と今後の展望

ツール「filterforge」と今後の展望

Cloudflareはこの研究成果をオープンソースツール「filterforge」として公開している。このツールを使えば、BPFバイトコードを含むファイルを入力とするだけで、マジックパケットの条件を満たすパケットのスケルトンを自動生成できる。

ツールの公開により、セキュリティコミュニティ全体でBPFベースの脅威に対する分析速度が向上することが期待される。特に、以下のような応用が考えられる。

  • マルウェアサンプルの自動分類: 生成されたマジックパケットの特徴から、同一グループによる活動を関連付けられる。
  • ネットワーク監視の強化: 生成されたパケットをプローブとして送信し、感染ホストの検出に利用する。
  • 教育・研究: 複雑なBPFフィルタの動作を、具体的なパケット例とともに理解する教材となる。

LLMとの組み合わせ可能性

Cloudflare Blogの記事では、LLM(大規模言語モデル)を用いてBPF命令の文脈的説明を生成する取り組みにも言及している。シンボリック実行による自動パケット生成とLLMによる自然言語説明を組み合わせれば、分析者の作業負荷はさらに軽減される。

ただし現状では、LLMだけに複雑なBPFフィルタの解析とパケット生成を任せるには限界がある。Z3を用いた形式的な手法は、その正確性と完全性において依然として重要な役割を果たす。

この記事のポイント

  • Linuxマルウェアは、カーネル内で動作するBPFソケットプログラムを利用し、特定の「マジックパケット」が到着するまで休眠する隠密性の高いバックドアを構築する。
  • 手動でのBPFバイトコード逆解析は、数百命令に及ぶ複雑なフィルタの場合、数日を要するボトルネックだった。
  • シンボリック実行によりBPFプログラムの入力を記号化し、定理証明器Z3で制約を解くことで、マジックパケットを数秒で自動生成できる。
  • この手法は、最短経路探索、BPF仮想マシンのシンボリックモデル化、Z3制約ソルバー、scapyによるパケット組み立ての4ステップから構成される。
  • Cloudflareが公開したオープンソースツール「filterforge」は、BPFベース脅威の分析速度をコミュニティ全体で向上させる可能性を秘めている。