OpenAICloudflare2026/04/08 13:00

From bytecode to bytes: automated magic packet generation

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

バイトコードからバイトへ:自動化されたマジックパケット生成

Key Points

  • BPFを自動逆解析
  • Z3でシンボリック実行
  • scapyでパケット生成

Summary

Linuxのclassic BPFフィルタを解析して、特定の「ACCEPT」経路を満たすパケットを自動生成する手法の要約。記事はBPFDoorのようなカーネル内BPFバックドアを対象に、命令列から最短の許可パスを抽出し、Z3を使ったシンボリック実行で該当バイト列を求め、scapyで実際のパケットに組み立てる流れを示す。

Key Points

  • 問題分解

    • BPFは簡潔なVM(A,Xレジスタ・Mスロット)で動作し、パケットの特定バイトを条件判定することで動作経路が決定される。
    • 解析は「最短のACCEPT経路」を見つける幅優先探索(BFS)でパスを列挙し、条件数の少ないパスを優先する。
  • シンボリック実行とZ3適用

    • パケットバイトをZ3のBitVecとしてモデル化し、命令ごとに制約を追加(例:Concat(pkt_12,pkt_13)==0x86dd や pkt_14 & 0xF0 == 0x40 等)。
    • ジャンプや比較は条件分岐としてZ3に与え、check()で満たすモデルを取得する。
    • 実運用上はタイムアウトやパス剪定が必要(命令数が100超のケースで爆発的に増えるため)。
  • パケット生成(scapy)

    • Z3モデルから各Byteの値を抽出し、scapyでEther/IP/UDP等のヘッダに配置して実パケットを構築する。
    • リンクタイプ(ethernet/raw)や最小パケット長、IHLやIPv6/IPv4バージョンのマスク処理に注意する。
  • 実践的注意点

    • フラグメントや可変IHL、エンディアン(Concatによる上位下位バイト)を正確に扱う必要がある。
    • BPF命令セットは限定的だが、条件の組み合わせ数が増えると解探索が重くなるため、事前に受け入れ経路を短く絞るのが重要。

Practical recipe for engineers

  • BPFバイトコードを命令列にデコードする。
  • BFSでACCEPTに到達する候補パスを列挙し、短いパスを選択する。
  • 選択したパスを基にシンボリック実行機(A,X,M,packet[])を構築し、命令をZ3制約へマッピングする。
  • Z3で解を取得し、BitVec値をバイト列へ変換する。
  • scapyで対応するレイヤを構築し、生成パケットを検証する(実ネットワークでの投下は運用ルールに従う)。

Use case

  • stealthなBPFバックドア(BPFDoor等)のトリガーパケット復元・侵入検知ルール作成・サンドボックス検証に有効。

Recommended safeguards

  • Z3のsolveタイムアウト設定、探索深度制限、パス重複除去を設けて解析時間を制御する。
  • 生成パケットのネットワーク投下は検証環境でのみ行う。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

バイトコードからバイトへ: 自動化されたマジックパケット生成

バイトコードからバイトへ: 自動化されたマジックパケット生成

LinuxマルウェアはしばしばBerkeley Packet Filter (BPF) のソケットプログラムに潜伏します。これらはカーネル内に埋め込まれる小さな実行可能ロジックで、ネットワークトラフィックの処理方法をカスタマイズできます。インターネット上の最も持続的な脅威のいくつかは、これらのフィルタを使って特定の“マジック”パケットを受信するまで休眠し、検出を回避します。これらのフィルタは数百命令に及び、複雑な論理ジャンプを含むことがあるため、手作業でリバースエンジニアリングするのは非常に時間がかかり、セキュリティ研究者のボトルネックになります。

より良い方法を模索するために、我々はシンボリック実行を検討しました。これはコードを単なる命令列としてではなく、制約の集合として扱う手法です。Z3 theorem prover を用いることで、マルウェアなフィルタから逆算し、それをトリガーするパケットを自動生成できます。本記事では、手作業でのアセンブリ解析に数時間を要していた作業を数秒に短縮するツールをどのように構築したかを説明します。

複雑性の上限

マルチプルな悪意あるフィルタを分解する前に、その実行エンジンを理解する必要があります。Berkeley Packet Filter (BPF) は、バイトコード命令の集合に基づいてカーネルがネットワークスタックから特定のパケットを高速に取り出すことを可能にする高効率技術です。多くの開発者が慣れ親しんでいるeBPF (Extended BPF) の強力な進化版とは別に、本稿では「クラシック」BPFに焦点を当てます。元々はtcpdumpのようなツール用に設計されたクラシックBPFは、わずか2つのレジスタしか持たないシンプルな仮想マシンで、ネットワークトラフィックを高速に評価します。カーネル深部で動作し、ユーザースペースツールからトラフィックを“隠す”ことができるため、ステルスなバックドアを構築するマルウェア作者に好まれています。

LLMを使ってBPF命令の文脈的表現を作ることは、アナリストの手作業の負担を既に軽減していますが、検証条件に対応するネットワークパケットを作成する作業は、LLMからの追加コンテキストがあっても依然として手間がかかります。BPFプログラムが約20命令程度であれば問題になることは少ないですが、一部サンプルで観察されたように100命令を超えると指数的に複雑化し、時間がかかります。

問題を分解すると、バッファを読み取り制約をチェックし、その結果に応じて実行パスを続行するか停止して結果を判定する、という構造に還元できます。このような決定性のある問題は、与えられた制約集合を解ける Z3 によって解決可能です。

Exhibit A: BPFDoor

BPFDoor は高度な受動型 Linux バックドアで、Red Menshen(別名 Earth Bluecrow)を含む中国ベースの脅威アクターによるサイバースパイ活動で主に利用されています。少なくとも2021年から活動が確認されており、侵害したネットワーク内でステルスな足場を維持するよう設計されています。通信、教育、政府分野を狙い、アジアおよび中東での活動に注力しています。

BPFDoor は特定のネットワークポートを開くことなく、受信トラフィック全体を監視するために BPF を使用します。

BPFDoor の例命令

Fortinet による解析で共有されたサンプル (82ed617816453eba2d755642e3efebfcbd19705ac626f6bc8ed238f4fc111bb0) に注目します。BPF 命令を分解して注釈を付けると、次のように書けます。

(000) ldh [0xc] ; Read halfword at offset 12 (EtherType)
(001) jeq #0x86dd, jt 2, jf 6 ; 0x86DD (IPv6) -> ins 002 else ins 006
(002) ldb [0x14] ; Read byte at offset 20 (Protocol)
(003) jeq #0x11, jt 4, jf 15 ; 0x11 (UDP) -> ins 004 else DROP
(004) ldh [0x38] ; Read halfword at offset 56 (Dst Port)
(005) jeq #0x35, jt 14, jf 15 ; 0x35 (DNS) -> ACCEPT else DROP
(006) jeq #0x800, jt 7, jf 15 ; 0x800 (IPv4) -> ins 007 else DROP
(007) ldb [23] ; Read byte at offset 23 (Protocol)
(008) jeq #0x11, jt 9, jf 15 ; 0x11 (UDP) -> ins 009 else DROP
(009) ldh [20] ; Read halfword at offset 20 (fragment)
(010) jset #0x1fff, jt 15, jf 11 ; fragmented -> DROP else ins 011
(011) ldxb 4*([14]&0xf) ; Load index (x) register ihl & 0xf
(012) ldh [x + 16] ; Read halfword at offset x+16 (Dst Port)
(013) jeq #0x35, jt 14, jf 15 ; 0x35 (DNS) -> ACCEPT else DROP
(014) ret #0x40000 (ACCEPT)
(015) ret #0 (DROP)

上記の例では、ACCEPT に至るパスが2つ存在することがわかります(ステップ5とステップ13)。また、チェックされているバイト、オフセット、値も明確に観察できます。これらの検証を取りまとめ、ACCEPT パスに一致する条件を追跡すれば、自動的に該当するパケットを作成できるはずです。

最短パスの計算

BPF 命令が示す条件を満たすパケットへの最短パスを見つけるには、不利な条件で終了しないパスを追跡する必要があります。小さなキューを作成して開始します。このキューは次の重要なデータポイントを保持します:

  • 次に実行する命令へのポインタ(program counter)。
  • 実行済みの命令のパス(現在のパス)+次に実行される命令。

条件をチェックする命令に遭遇するたびに、その結果をブール値で保持してキューに格納します。これにより、ACCEPT に到達するまでの条件の数を比較し、最短パスを計算できます。

擬似コードでは次のように表現できます。

paths = []
queue = dequeue([(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 or accept, stop parsing this instruction
    if instruction.class == jump_instruction:
        if instruction.operation == unconditional_jump:
            next_pc = pc + 1 + instruction_constant
            queue.append((next_pc, path + [next_pc]))
            continue
        # Conditional jump, explore both
        pc_true = pc + 1 + instruction.jump_true
        pc_false = pc + 1 + instruction.jump_false
        if instruction.jump_true <= instruction.jump_false:
            queue.append((pc_true, path + [pc_true]))
            queue.append((pc_false, path + [pc_false]))
        # else: same as above but reverse order of appending
    # else: sequential instruction, append to the queue

このロジックを先の BPFDoor 例に対して実行すると、ACCEPT されるパケットへの最短パスが次のように示されます:

(000) code=0x28 jt=0 jf=0 k=0xc ; Read halfword at offset 12 (EtherType)
(001) code=0x15 jt=0 jf=4 k=0x86dd ; IPv6 packet
(002) code=0x30 jt=0 jf=0 k=0x14 ; Read byte at offset 20 (Protocol)
(003) code=0x15 jt=0 jf=11 k=0x11 ; Protocol number 17 (UDP)
(004) code=0x28 jt=0 jf=0 k=0x38 ; Read word at offset 56 (Destination Port)
(005) code=0x15 jt=8 jf=9 k=0x35 ; Destination port 53
(014) code=0x06 jt=0 jf=0 k=0x40000 ; Accept

これは、BPF 命令を解析してバックドアに対する受け入れパケットがどのように見えるべきかを自動的に解く上で既に有用な自動化です。しかし、ここからさらに一歩進めて、期待されるパケットを自動で返してくれる小さなツールを作れたらどうでしょうか。

Z3 と scapy の利用

与えられた制約集合を解くのに適しているツールの一つが Z3 です。Microsoft が開発したこのツールは theorem prover と呼ばれ、内部で非常に複雑な数学的演算を行う簡便な関数群を公開しています。もう一つのツールは scapy で、これは対話的なパケット操作に人気のある Python ライブラリです。

既に ACCEPT されるパケットへのパスを求める方法があるので、残る課題はそのパスで必要とされる制約を解き、得られた解をネットワークパケットの各オフセットのバイトに変換することです。

シンボリック実行

あるプログラムで取られるパスを探索する一般的な手法にシンボリック実行があります。入力を変数(および制約)として与え、成功するパスの結果を知ることでツールにこれらの成功パスを見つけさせ、コンテキスト化した結果を出力させます。

これを実現するために、定数、レジスタ、条件判定の結果となるブール演算子などの状態を追跡できる小さなマシンを実装する必要があります。例として以下のようなクラスを示します。

class BPFPacketCrafter:
    MIN_PKT_SIZE = 64  # Minimum packet size (Ethernet + IP + UDP headers)
    LINK_ETHERNET = "ethernet"  # DLT_EN10MB - starts with Ethernet header
    LINK_RAW = "raw"  # DLT_RAW - starts with IP header directly
    MEM_SLOTS = 16  # Number of scratch memory slots (M[0] to M[15])

    def __init__(self, ins: list[BPFInsn], pkt_size: int = 128, ltype: str = "ethernet"):
        self.instructions = ins
        self.pkt_size = max(self.MIN_PKT_SIZE, pkt_size)
        self.ltype = ltype
        # Symbolic packet bytes
        self.packet = [BitVec(f"pkt_{i}", 8) for i in range(self.pkt_size)]
        # Symbolic registers (32-bit)
        self.A = BitVecVal(0, 32)  # Accumulator
        self.X = BitVecVal(0, 32)  # Index register
        # Scratch memory M[0-15] (32-bit words)
        self.M = [BitVecVal(0, 32) for _ in range(self.MEM_SLOTS)]

上記でシンボリック実行中の状態管理に必要なマシンの大部分をカバーしました。もちろん他にも追跡すべき条件がありますが、これらはソルバーによる解決プロセスで扱われます。

ADD 命令を扱う際には、マシンは BPF の演算を Z3 の加算にマッピングします。例えば:

def _execute_ins(self, insn: BPFInsn):
    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

幸いにも、BPF 命令セットは比較的小さく実装が容易で、追跡すべきレジスタが2つしかないことは制約として歓迎できます。

このシンボリック実行の全体像は次のように抽象化できます:

  • レジスタ "x"(index)と "a"(accumulator)を初期状態に設定する。
  • 成功パスとして特定された命令列に沿ってループする。
  • 非ジャンプ命令をそのまま実行し、レジスタ状態を追跡する。
  • ジャンプ命令が出現したら、どちらの分岐が取られるかを判定する。
  • Z3 の check() を使用して、与えた制約で条件(ACCEPT)が満たされるかを確認する。
  • Z3 のビットベクタ配列をバイト列に変換する。
  • 変換したバイト列を scapy でパケットとして構築する。

Z3 ソルバーによって構築された制約を見ると、Z3 がパケットバイトを構築するためにたどった実行ステップを追跡できます。例として次のような出力が得られます:

[If(Concat(pkt_12, pkt_13) == 0x800, pkt_14 & 0xF0 == 0x40, True),
 If(Concat(pkt_12, pkt_13) == 0x800, pkt_14 & 0x0F >= 5, True),
 If(Concat(pkt_12, pkt_13) == 0x800, pkt_14 & 0x0F == 5, True),
 If(Concat(pkt_12, pkt_13) == 0x86DD, pkt_14 & 0xF0 == 0x60, True),
 0x86DD == ZeroExt(16, Concat(pkt_12, pkt_13)),
 0x11 == ZeroExt(24, pkt_20),
 0x35 == ZeroExt(16, Concat(pkt_56, pkt_57))]

最初の部分は、リンク層 BPF 命令を扱うときに有効な Ethernet/IP を構築するために追加された制約です。If 文は検出されたプロトコルに基づいて特定の制約を適用します:

  • IPv4 ロジック (0x0800):
    • pkt_14 & 240 == 64: バイト14はIPヘッダの開始。0xF0 マスクで上位ニブル(Version)を分離し、Versionが4 (0x40) かをチェック。
    • pkt_14 & 15 == 5: 0x0F で下位ニブル(IHL)を分離し、IHL が 5(20バイト)であることを要求。オプションなしの標準的なサイズ。
  • IPv6 ロジック (0x86dd):
    • pkt_14 & 240 == 0x60: Versionフィールドが 6 (0x60) であることをチェック。

最後の部分では、異なる値がネットワークパケットのどのバイトに対応しているかを観察できますが、ここで原文は途中で切れており詳細が欠けています。


(注: この記事は原文の 2026-04-08 公開版に基づいて翻訳しています。原文の末尾が途中で切れている箇所があります。)