プロセスの中身をくり抜く影:Process HollowingのVAD不整合を暴くメモリフォレンジックの極意
夜中にSOCのSIEMがけたたましくアラートを吐き出す。EDRは「無害な正規プロセスが不審な子プロセスを生成しました」と告げているが、ダッシュボードの向こう側にいる攻撃者は、すでにその一枚板の防御を笑い飛ばしているかもしれない。
現代の高度な脅威アクターやランサムウェアのオペレーターたちが好んで使う手口の一つに、Process Hollowing(プロセス・ハロウィング)がある。
彼らはsvchost.exeやexplorer.exeといった信頼された正規プロセスをサスペンド状態で立ち上げ、その正当なメモリ空間(Image Base)をきれいさっぱりくり抜き、中身を自分たちの悪意あるシェルコードやリバースシェルにすり替えてしまう。EDRの静的なプロセスツリー監視や、表層的なファイルパスのチェックだけを信じ込んでいるアナリストにとって、これは完璧な隠れ蓑となる。
しかし、どれほど巧妙に皮を被ろうとも、OSの低レイヤ――特にWindowsメモリ管理の心臓部であるVAD(Virtual Address Descriptor)ツリーの整合性を騙し通すことはできない。
今回は、プロセス・ハロウィングの根本的なメカニズムと、カーネル構造体の矛盾を突いてその隠蔽工作を暴く、実践的なメモリフォレンジックの深層を解説しよう。
—
1. 根本原因:Windowsメモリ管理の盲点とProcess Hollowingのメカニズム
Process Hollowingの本質は、OSの正当なプロセス生成プロセスにおける「設計上のタイムラグ」と「メモリの柔軟性」を悪用した欺瞞工作にある。
攻撃者がこの手法を実行する際、内部では次のような一連のAPIコールが厳密な順序で実行される。
1. プロセスの生成 (CreateProcessW): CREATE_SUSPENDEDフラグを指定し、実行スレッドが1つも動かない状態で正規のプロセス(例: notepad.exe)をメモリ上にロードする。
2. メモリのアンマップ (ZwUnmapViewOfSection): ここが最大の急所だ。攻撃者はロードされた正規バイナリのイメージ領域(Image Base)を、カーネルAPIを直接叩いて強制的に解放(アンマップ)する。この瞬間、プロセスのアドレス空間の該当領域は「何も割り当てられていない空洞」になる。
3. 悪意あるコードの書き込み (VirtualAllocEx / WriteProcessMemory): 空いた領域、あるいは別の適切なアドレスに、攻撃者が用意したペイロード(Cobalt Strikeのビーコンなど)のPEヘッダおよびセクションを書き込む。
4. コンテキストの書き換え (SetThreadContext): サスペンド状態のメインスレッドのRIP/EIPレジスタを、新しく書き込んだペイロードのエントリポイント(EntryPoint)に強制的に書き換える。
5. 実行再開 (ResumeThread): スレッドを再開させると、OSは「正規のプロセスが動いている」と錯覚したまま、中身がすり替わったコードを実行し始める。
この一連の動きの中で、攻撃者はOSのプロセス構造体(EPROCESS)やPEB(Process Environment Block)の一部を偽装し、親プロセスとの関係やイメージ名を正当なものに見せかける。だが、ここでメモリ管理サブシステム(Memory Manager)との間に致命的な矛盾が生じる。それが「VADツリー」の不整合である。
—
2. 攻防の急所:VAD(Virtual Address Descriptor)属性の不整合
Windowsカーネルは、各プロセスがどの仮想メモリ領域をどのように使用しているかを管理するために、VADツリーと呼ばれるAVLツリー構造を維持している。
通常、ディスク上の実行ファイル(PEファイル)からマッピングされたメモリ領域は、VADノードにおいて「ファイルマップ(File-backed)」の属性を持ち、対応するファイルオブジェクト(例えば \Windows\System32\notepad.exe)へのポインタが保持される。
しかし、Process HollowingによってZwUnmapViewOfSectionが実行された後、そこに VirtualAllocEx で新しいメモリ領域が割り当てられると、何が起きるか?
- 本来の状態:
Image Baseからの領域はファイルマッピング(File-backed)としてVADに登録されるべき。 - ハロウィング後の状態: 割り当てられた領域は、ファイルに紐づかないプライベートメモリ(Private Working Set / Private Anonymous Memory)としてVADに登録されてしまう。
さらに悪いことに、PEB内の ImageBaseAddress は新しいアドレスを指しているにもかかわらず、カーネル側のVADツリーが指し示す実体や属性と、プロセス空間内のPEヘッダ情報が完全に矛盾する状態が生まれる。この「ガワ(名前や親)と中身(メモリのマッピング属性やPE構造)のミスマッチ」こそが、フォレンジック調査において最も信頼性の高い検出シグネチャとなる。
—
3. 実践:Volatility 3によるVAD不整合の検出アプローチ
理論を理解したところで、現場のインシデントレスポンスでこれをどう暴くかを見ていこう。メモリダンプ(RAWイメージやCrash Dump)からVolatility 3を用いて不審なプロセスを炙り出すためのアプローチだ。
Volatility 3の標準プラグインである windows.vads や windows.malfind を組み合わせることで、この矛盾を効率的に検出できる。
以下のPythonスクリプトは、Volatility 3のフレームワークを利用し、プロセスのアドレス空間内におけるVADの属性と、実際のPEヘッダの存在をプログラム的に検証するカスタム解析ロジックの概念実証(PoC)である。
# Volatility 3フレームワークを想定したVAD不整合検出しの概念コード
# 実際の現場ではVolatilityのAPIコンテキストに合わせてインポートを調整してください。
import volatility3.symbols.windows as win_symbols
def check_vad_hollowing_anomaly(context, layer_name, symbol_table, proc_el):
"""
指定されたプロセスのVADツリーを走査し、Image Base周辺のメモリ属性の不整合を検証する。
"""
process_name = proc_el.ImageFileName.cast("string", max_length=15, errors="replace")
# プロセスのPEBを取得
peb = proc_el.Peb
if not peb:
return
image_base_address = peb.ImageBaseAddress
print(f"[*] 解析中プロセス: {process_name} (PID: {proc_el.UniqueProcessId}), ImageBase: {hex(image_base_address)}")
# VADツリーの走査(擬似的な走査ロジック)
# 実際のVolatility 3では proc_el.VadRoot を通じてAVLツリーをトラバースする
vad_root = proc_el.VadRoot
for vad in vad_root.traverse():
start = vad.get_start()
end = vad.get_end()
# ImageBaseが含まれるVADノードを特定
if start <= image_base_address <= end:
vad_type = vad.get_tag() # 例: VadNone, VadFile, VadDeviceなど
prot = vad.get_protection()
print(f" -> ヒットしたVAD領域: {hex(start)} - {hex(end)}")
print(f" VADタイプ/属性: {vad_type}, 保護モード: {prot}")
# 【検出ロジック】
# 本来、Image Baseが含まれる領域はファイルマッピング(例: VadFile)であるべきだが、
# Process Hollowingを受けている場合、これがプライベートメモリ(Private)にすり替わっている。
if "Private" in str(vad_type) or vad_type == "VadNone":
print(f"[!] 警告: 潜在的なProcess Hollowingを検知しました!")
print(f" プロセス '{process_name}' のイメージ領域がプライベートメモリとして割り当てられています。")
# さらに、メモリ上のPEヘッダ(MZシグネチャ)の有無や、
# ディスク上のファイルとのバイト比較を行うことで精度を高める。
現場で確認すべきパラメータとIOC(Indicators of Compromise)
メモリフォレンジックを実施する際、以下のポイントをチェックリストとして手元に置いておくべきだ。
1. windows.malfind の出力確認:
プロセス空間内の実行可能かつ書き込み可能(PAGE_EXECUTE_READWRITE / PAGE_EXECUTE_READ)なプライベートメモリ領域に、MZ(0x5A4D)ヘッダが直接書き込まれていないか。正規のモジュールであれば、通常プライベートメモリに突然PEヘッダが出現することはない。
2. VADのプロテクションと内容の乖離:
vadinfo プラグインを使い、該当プロセスのメモリマップを出力する。セクション名(.text, .data など)が不自然に欠落している、あるいはマッピング元ファイルのパスが存在しない(あるいは別のシステムファイルにすり替わっている)ケースは極めて怪しい。
3. スレッドコンテキストとエントリポイントの検証:
スレッドの開始アドレス(Win32StartAddress)が、プロセスのメインモジュールのエントリポイントや既知の正当なローダー関数(例: LdrInitializeThunk)を指しているか。これが全く異なるプライベートメモリ領域を指している場合、ハロウィングの確率は跳ね上がる。
—
4. チーフホワイトハッカーが語る:検知の先にある次世代の防衛アーキテクチャ
メモリ上のコードインジェクションやProcess Hollowingを完全に防ぐことは、従来のOSアーキテクチャの上では非常に困難だ。APIフッキングやEDRのセンサーは常に「攻撃者がAPIを呼んだ後、または呼び出す瞬間」を監視しているにすぎず、カーネルモードでの直接的な操作(Direct Kernel Object Manipulation: DKOM)や未公開APIの利用に対しては後手に回りがちだからだ。
真の意味でこの種の脅威に対抗するためには、以下のレイヤで多層防御を構築する必要がある。
- ハードウェア支援型仮想化とHVCI(Hypervisor-Protected Code Integrity)の強制:
VBS(Virtualization-Based Security)を活用し、メモリページの一貫性をハイパーバイザー側から強制する。これにより、カーネルレベルであっても不正なメモリの書き換えや実行権限の付与がハードウェアレベルでブロックされる。
- EDRの挙動検知の高度化(静的から動的・文脈的解析へのシフト単体):
単に ZwUnmapViewOfSection が呼ばれたことだけでなく、プロセス生成直後の不自然なメモリレイアウトの変更、スレッドのサスペンド・レジスタ書き換え・再開という「一連のコンテキストの連鎖(Kill Chainの断片)」をリアルタイムで相関分析するルールをSIEM/XDRに実装する。
- 次世代エンドポイントセキュリティの監査:
定期的なメモリインテグリティのスキャンを自動化し、プロセス群のVADツリーとディスク上のバイナリハッシュの不整合をバックグラウンドで継続的に監査する体制を整えること。
Process Hollowingは、OSの善意(柔軟なメモリ管理機構)を逆用したエレガントだが卑劣な偽装工作である。しかし、OSの内部構造(VADの法則)を深く理解したアナリストの手にかかれば、その虚構は一瞬で剥ぎ取られる。
セキュリティの泥臭い現場において、最後に勝つのは「仕組みの裏側まで見通す深い技術的知見」を持つ者だ。常にカーネルの視点を持ち続け、不審な影を徹底的に追い詰めてほしい。
コメント