【テクニカル・上級編】 ファイルレスマルウェアのメモリ内実行検知とペイロード抽出 – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

ディスクは嘘をつくが、メモリは真実を語る――かつてフォレンジックの世界で囁かれたこの格言は、ファイルレスマルウェアが跋扈する現代において、より切実な意味を持っています。

ディスク上に実行ファイルを残さず、正規のプロセスやシステムメモリの隙間に潜伏する攻撃手法は、従来のシグネチャベースのEDR(Endpoint Detection and Response)を容易に無力化します。彼らが好むのは、Reflective DLL InjectionやProcess Hollowingといった、OSの正規のメモリ管理メカニズムを逆手に取ったインメモリ実行技術です。

しかし、いかに精巧に偽装されたペイロードであっても、CPUがそれを実行するためには、Windowsカーネルに対して「メモリ領域の確保」と「実行権限の付与」を要求せねばなりません。このカーネルレベルの動的挙動は隠蔽できません。

本稿では、インメモリで動作するマルウェアの検知において最も強力なアプローチであるVAD(Virtual Address Descriptor)ツリー解析の低レイヤメカニズムを解剖し、不審なメモリ領域からペイロードを抽出し、解析可能なPE(Portable Executable)ファイルとして再構築するプロフェッショナルなDFIR技術を解説します。

—

1. インメモリ偽装の極低レイヤ・メカニズム

メモリフォレンジックで敵を捉えるためには、まず彼らがどのようにメモリを汚染しているのか、その物理的・論理的挙動を理解する必要があります。代表的な2つの手法、Reflective DLL InjectionとProcess Hollowingの挙動を解剖します。

Reflective DLL Injection の内幕

通常のDLLロードは、Windowsのローダー(ntdll.dll 内の LdrLoadDll)がディスク上のPEファイルを解析し、メモリにマップし、インポートテーブル(IAT)を解決し、ベース再配置を適用することで行われます。このプロセスを経ると、プロセスのPEB(Process Environment Block)にあるロード済みモジュールリスト(InLoadOrderModuleList など)にDLLのパスが登録されます。

一方、Reflective DLL Injectionは、これらローダーの一連の挙動をインジェクター自身がメモリ上で再実装(ReflectiveLoader)します。

[攻撃元プロセス]
  │ 1. VirtualAllocEx() でターゲット内に RWX 領域を確保
  │ 2. WriteProcessMemory() で Reflective DLL を書き込み
  │ 3. CreateRemoteThread() で ReflectiveLoader() を起動
  ▼
[ターゲットプロセス]
  │ 4. ReflectiveLoader() が自身のPEヘッダーをセルフ解析
  │ 5. 必要なAPIをローカルのIATから動的に解決 (GetProcAddressの代替)
  │ 6. メモリ上にセクションを適切に展開し、DLLMain() を実行

この結果、DLLはプロセスのメモリ空間に完全に展開されて動作しているにもかかわらず、PEBのモジュールリストには一切登録されません。APIレベルの監視(APIフック)をすり抜ける巧妙なステルス技術です。

Process Hollowing の内幕

Process Hollowing(プロセスの空洞化)は、より大胆です。
攻撃者は、信頼された正規プロセス(例: svchost.exe や mstsc.exe)を CREATE_SUSPENDED フラグ付きで起動します。

1. 空洞化: NtUnmapViewOfSection(または ZwUnmapViewOfSection)を呼び出し、起動した正規プロセスのメインモジュールのメモリマッピングを強制的に解放(アンマップ)します。
2. 注入: VirtualAllocEx を用いて、解放されたアドレス空間(または任意の空き領域)に悪意あるPEイメージを書き込みます。
3. 偽装の完成: スレッドのコンテキスト(GetThreadContext)を取得し、レジスタ(x64環境における RCX や RIP)を書き換えてエントリーポイントを新しく注入したPEのアドレスに設定します。その後、ResumeThread を呼び出すことで、外見は正規プロセスでありながら、内部は完全にマルウェアに入れ替わったプロセスが動き出します。

—

2. VAD(Virtual Address Descriptor)ツリーの深淵

これらのインメモリ攻撃を検知する鍵が、Windowsカーネルが各プロセスの仮想メモリ空間を管理するために保持しているVAD(Virtual Address Descriptor)ツリーです。

VADツリーの構造

Windowsカーネルは、プロセスごとに割り当てられた仮想メモリの範囲(ページ)を、バランス二分探索木(AVL木)の一種であるVADツリーで管理しています。このツリーのルートポインタは、プロセスを表現するカーネルオブジェクト _EPROCESS の VadRoot メンバに格納されています。

プロセスのメモリ空間に VirtualAlloc などで新たなページがコミットされるたび、カーネルは対応する _MMVAD 構造体(またはその軽量版である _MMVAD_SHORT)を生成し、VADツリーに挿入します。

_MMVAD 構造体には、以下の極めて重要なメタデータが含まれています。

  • StartingVpn / EndingVpn: 仮想ページ番号(Virtual Page Number)。このVADがカバーするアドレス範囲。
  • Protection: メモリの保護属性(PAGE_EXECUTE_READWRITE、PAGE_READWRITE、PAGE_NOACCESS など)。
  • PrivateMemory / CommitCharge: このメモリがこのプロセス専用のプライベートメモリか、それともファイル共有されているか。
  • ControlArea: ファイルバッキング(ディスク上の物理ファイルと紐付いているか)を示す構造体へのポインタ。

メモリフォレンジックにおける「不審なVAD」の識別ロジック

正規のDLLがロードされた場合、そのメモリ領域はディスク上のファイルに対応するため、VADノードは SEC_IMAGE(Image)としてマップされ、ControlArea は該当するDLLファイル(例: C:\Windows\System32\kernel32.dll)を指します。

しかし、Reflective DLL Injectionやシェルコードのインジェクションによって確保されたメモリ領域は、ディスク上のファイルと紐付いていません。したがって、VADノードは MEM_PRIVATE(Private)としてマークされます。

ここに、決定的な検知ロジックが成立します。

$$\text{不審なVAD領域} \iff (\text{PrivateMemory} = \text{True}) \land (\text{Protection} \in \{\text{PAGE\_EXECUTE\_READWRITE}, \text{PAGE\_EXECUTE\_READ}\})$$

「ディスク上のファイルに紐付いていない(Private)にもかかわらず、実行権限(Executable)が付与されているメモリ領域」は、JITコンパイラ(ブラウザや.NETランタイムなど)のような一部の例外を除き、通常のアプリケーション開発では極めて不自然な存在です。これこそが、ファイルレスマルウェアが残す不可避のフットプリントです。

—

3. 実践:Volatility 3による検知と詳細パース

それでは、実際のインシデントレスポンスにおいて、メモリダンプからこの不審なVAD領域を特定し、ペイロードを抽出するプロセスを解説します。今回は、業界標準のメモリフォレンジックフレームワークである Volatility 3 を使用します。

基本的な検知:windows.malfind の限界と本質

Volatility 3の windows.malfind プラグインは、まさに前述の「Private かつ Executable」なVAD領域を自動で走査し、さらにその領域の先頭数バイトにPEヘッダー(MZ シグネチャ)やアセンブリコードが存在するかをスキャンする非常に便利なツールです。

# malfindプラグインを実行し、不審なメモリインジェクションの兆候を走査
python3 vol.py -f target_memdump.raw windows.malfind

出力結果の例:

PID   Process    Start VPN    End VPN      Tag    OutOfOrder  Information
---   --------   ----------   ----------   ----   ----------  -----------
4216  svchost.e  0x1f3c0000   0x1f3c1fff   VadS   False       PAGE_EXECUTE_READWRITE  CommitCharge: 2
      0x1f3c0000  4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00   MZ..............
      0x1f3c0010  b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00   ........@.......

上記の結果は、PID: 4216(svchost.exe)の 0x1f3c0000 から始まるメモリ領域が PAGE_EXECUTE_READWRITE(ERW)であり、ダンプの先頭が 4d 5a(ASCIIで MZ、すなわちPEヘッダーの開始シグネチャ)であることを示しています。これは典型的なReflective DLL Injectionの痕跡です。

高度な偽装への対処:windows.vadinfo を用いたマニュアルスキャン

洗練された攻撃者は、malfind のようなナイーブなスキャナーを欺くため、PEヘッダーをメモリ上で意図的に破壊(ゼロクリアなど)します。また、APIフックを避けるためにメモリの保護属性を一時的に PAGE_NOACCESS や PAGE_READWRITE に戻し、実行時のみ PAGE_EXECUTE_READ に変更するスレッドローテーション(Thread Ordering)技術を用いることもあります。

このような場合、windows.vadinfo プラグインを用いて、プロセスの全VADツリーを精査する必要があります。

# 特定のプロセス(PID 4216)の全VAD詳細情報を出力
python3 vol.py -f target_memdump.raw windows.vadinfo --pid 4216 > vad_dump.txt

出力された vad_dump.txt から、以下の条件に合致するVADノードを手動、またはスクリプトでフィルタリングします。

# 注目すべき不審なVADノードの例
Start Address: 0x1f3c0000
End Address: 0x1f4bffff
Tag: VadS
Control Area: None             <-- [重要] Noneはファイルバッキングがない(Private)ことを示す
Protection: PAGE_EXECUTE_READ  <-- [重要] 実行権限がある
File path: N/A                 <-- [重要] 紐づくディスク上のファイルが存在しない

—

4. ペイロードの抽出とPEヘッダーの再構築

不審なVAD領域(例: 0x1f3c0000)を特定したら、次はその領域の生メモリ(Raw Memory)をダンプし、リバースエンジニアリング可能な状態に「再構築」します。

メモリのダンプ

Volatility 3を使用して、該当プロセスの特定アドレス範囲をバイナリとして書き出します。

# PID 4216 の不審な仮想アドレス空間(0x1f3c0000)からメモリをダンプ
python3 vol.py -f target_memdump.raw -o ./dump_output windows.vaddump --pid 4216 --address 0x1f3c0000

これにより、pid4216.vad.0x1f3c0000.dmp のような名前のファイルが生成されます。

PE再構築(PE Reconstruction)の技術的障壁

ダンプされたファイルは、「メモリ上に展開された状態(Virtual Layout)」のPEファイルです。これをそのままIDA ProやGhidraなどの静的解析ツールに読み込ませても、正しく解析できません。なぜなら、ディスク上のPEファイル(Raw Layout)とメモリ上のPEファイル(Virtual Layout)では、アライメント(配置単位)が異なるからです。

  • File Alignment(ファイルアライメント): 通常 0x200 (512) バイト単位。ディスク上の物理サイズを節約するため。
  • Section Alignment(セクションアライメント): 通常 0x1000 (4096) バイト単位。仮想メモリの最小単位(ページサイズ)に合わせるため。

メモリからダンプしたファイルは Section Alignment に従って展開され、セクション間に「穴(ヌルバイトのパディング)」が空いた状態になっています。これをディスク上で動作可能な、あるいは静的解析ツールが正しくセクションテーブルを解釈できる「ファイル構造(Raw)」に戻す必要があります。

実用的なPE再構築(PE Alignment Fix)スクリプト

以下は、メモリからダンプした raw PE イメージのセクションヘッダーを解析し、セクションの「バーチャルアドレス(RVA)」を「ファイルオフセット(PointerToRawData)」に再マッピングして、解析可能なPEファイルとして再構築するPythonスクリプトです。

import sys
import struct

def reconstruct_pe(input_path, output_path):
    print(f"[*] Reconstructing PE file from memory dump: {input_path}")
    
    with open(input_path, "rb") as f:
        data = bytearray(f.read())
        
    # MZシグネチャの検証
    if data[0:2] != b'MZ':
        print("[!] Warning: Missing MZ signature. Attempting to force repair...")
        # 攻撃者が消去したヘッダーの修復(仮に標準的なDOSヘッダーを補填する場合)
        data[0:2] = b'MZ'
        
    # PEヘッダー(e_lfanew)のオフセットを取得
    pe_offset = struct.unpack("<I", data[0x3c:0x40])[0]
    if data[pe_offset:pe_offset+4] != b'PE\x00\x00':
        print("[!] Error: Invalid PE signature at offset. Cannot reconstruct.")
        return

    # セクション数の取得
    num_sections = struct.unpack("<H", data[pe_offset+6:pe_offset+8])[0]
    # オプショナルヘッダーのサイズ
    size_of_opt_header = struct.unpack("<H", data[pe_offset+20:pe_offset+22])[0]
    
    # セクションテーブルの開始位置
    section_table_offset = pe_offset + 24 + size_of_opt_header
    
    # 新しいPEファイル用のバッファを用意
    # 仮想サイズ(SizeOfImage)分の十分な領域を確保
    size_of_image = struct.unpack("<I", data[pe_offset+56:pe_offset+60])[0]
    reconstructed_pe = bytearray(size_of_image)
    
    # ヘッダー領域(PEヘッダーの終わりまで)をコピー
    header_size = section_table_offset + (num_sections * 40)
    reconstructed_pe[0:header_size] = data[0:header_size]
    
    # 各セクションをループ処理し、メモリ上のアライメントからファイル上のアライメントへ再配置
    for i in range(num_sections):
        offset = section_table_offset + (i * 40)
        sec_name = data[offset:offset+8].decode('utf-8', errors='ignore').strip('\x00')
        
        # セクション情報をアンパック
        # VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData
        v_size, v_addr, r_size, r_addr = struct.unpack("<IIII", data[offset+8:offset+24])
        
        print(f"    - Section found: {sec_name:<8} | VirtualAddr: 0x{v_addr:08x} | RawAddr: 0x{r_addr:08x} | Size: 0x{r_size:08x}")
        
        # 攻撃者がアンチフォレンジックとして PointerToRawData (r_addr) を 0 にしている場合、
        # 仮想アドレス (v_addr) をファイルオフセットとして代用する
        if r_addr == 0:
            r_addr = v_addr
            # ヘッダー内の PointerToRawData を書き換え
            data[offset+20:offset+24] = struct.pack("<I", r_addr)
            
        # メモリ上のデータからセクションデータを抽出し、ファイル上の位置に配置
        src_start = v_addr
        src_end = v_addr + min(v_size, r_size)
        
        # ダンプファイル内のデータ範囲チェック
        if src_end <= len(data):
            reconstructed_pe[r_addr:r_addr + (src_end - src_start)] = data[src_start:src_end]
            
            # ヘッダー内の SizeOfRawData を確定値に修正
            data[offset+16:offset+20] = struct.pack("<I", src_end - src_start)
            
    # ヘッダー領域の最終更新を書き戻し
    reconstructed_pe[0:header_size] = data[0:header_size]

    with open(output_path, "wb") as f:
        f.write(reconstructed_pe)
        
    print(f"[+] Reconstruction complete! Saved to: {output_path}")

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("Usage: python reconstruct.py <input_dump> <output_pe>")
        sys.exit(1)
    reconstruct_pe(sys.argv[1], sys.argv[2])

このスクリプトは、メモリから抽出した生の不完全なダンプデータを修復し、各セクションの仮想アドレス(RVA)をファイルオフセット(PointerToRawData)に再マッピングすることで、GhidraやIDA、PEviewなどのツールがエラーを起こさずに構造を読み込める状態へと強制変換します。

—

5. 監査とエンタープライズ防衛アーキテクチャ

メモリフォレンジックは事後解析(ポストモルテム)において最強の武器ですが、エンタープライズ環境においては「そもそもメモリインジェクションを動的に阻止・検知する」ための、プロアクティブな防衛アーキテクチャ設計が不可欠です。

1. カーネルコールバックを活用したプロセスクリエーションの監視

攻撃者が CreateProcess や SetThreadContext を呼び出す瞬間を捉えるため、カーネルドライバレベルでの監視(EDRの核心機能)を実装します。Windowsカーネルが提供する以下の通知ルーチンを使用します。

  • PsSetCreateProcessNotifyRoutineEx: 新しいプロセスが作成される際にコールバックを受け取ります。ここで CREATE_SUSPENDED などの不審な作成フラグを監視します。
  • ObRegisterCallbacks: プロセスやスレッドのハンドル作成(OpenProcess、VirtualAllocEx 実行前のハンドル取得など)をインターセプトします。攻撃プロセスがターゲットのシステムプロセス(lsass.exe や explorer.exe)に対して PROCESS_VM_WRITE や PROCESS_VM_OPERATION などの過剰な権限を要求した段階で、そのハンドル要求を拒否、または警告を発します。

2. ハードウェア支援型セキュリティ(HVCI / VBS)

ソフトウェアレベルの検知には限界があります。カーネル自体が汚染された場合、VADツリーの改ざんすら可能になるからです。これに対抗するため、近年のエンタープライズ設計ではVBS(Virtualization-Based Security:仮想化ベースのセキュリティ)およびHVCI(Hypervisor-Protected Code Integrity)の有効化が必須です。

HVCI環境下では、ハイパーバイザ(Hyper-V)がメモリページの属性(W^X:Write XOR Execute)をハードウェアレベルで強制します。
これにより、「書き込み可能(Writable)かつ実行可能(Executable)」なメモリページ(RWX)の生成そのものがシステム全体で禁止され、Reflective DLL Injectionなどの攻撃はインジェクションの第一段階(VirtualAlloc での RWX 確保)でハードウェアによってブロックされます。

3. メモリ監視用YARAルールのシグネチャデプロイ

EDRやセキュリティエージェントが定期的にメモリ空間をスキャンするための、VAD構造を意識したYARAルールをデプロイします。以下は、メモリ空間内に潜むインメモリPEヘッダーを検知するためのYARAルール例です。

rule Detect_InMemory_PE_Header {
    meta:
        description = "Detects unmapped PE files in memory (Private Executable memory)"
        author = "SOC Lead Analyst"
        severity = "Critical"

    strings:
        // MZヘッダー
        $mz = { 4D 5A }
        // PEシグネチャの標準的な位置オフセット
        $pe = { 50 45 00 00 }

    condition:
        // メモリの先頭(RVA 0)付近にMZがあり、かつPEヘッダーが正しく存在すること
        $mz at 0 and $pe at (uint32(0x3C))
}

これを、システムの「Private / Executable」属性を持つメモリ領域のみに限定して実行(スキャン対象を絞り込むことでパフォーマンスへの影響を最小限に抑える)するようにスキャナーを設計します。

—

結言

ファイルレスマルウェアは、OSの正当な機能の影に隠れて活動するため、一見すると不可視に見えます。しかし、彼らが「CPUを動作させる」という物理的な制約に従う限り、WindowsカーネルのVADツリーには必ずその歪みが現れます。

VADツリーの解析は、攻撃者がどれほど巧妙にIATを難読化し、PEヘッダーを破壊しようとも、その本質的な「実行権限の不整合」を暴き出すことができる決定的な技術です。

インシデントハンドラーおよびセキュリティアーキテクトは、ツールを叩くこと(Volatilityの実行)に満足せず、その裏で動作するWindowsのメモリ管理メカニズムと、ダンプされたバイナリを再構築する低レイヤのPE構造を深く理解してください。それこそが、高度な国家型攻撃グループ(APT)の痕跡を捉え、その真のペイロードを暴くための唯一の道なのです。

コメント

タイトルとURLをコピーしました