プロセスツリーの歪みを見抜け:Volatility 3とメモリフォレンジックによる親プロセス偽装の暴き方
インシデントレスポンスの現場において、最初に目撃する光景の多くは「静まり返ったデジタル空間」だ。しかし、その背後では、巧妙に偽装されたプロセスがメモリの深層でひそかに蠢いている。EDR(Endpoint Detection and Response)がアラートを上げるのは、往々にして攻撃者がすでに初期侵入を果たし、権限昇格やラテラルムーブメントに移行した後のことだ。
特に、攻撃者がOSの仕様の隙間を縫って仕掛ける「親プロセス偽装(Parent Process Spoofing)」は、従来の静的なログ解析やタスクマネージャーの目視レベルでは完全にスルーされる。explorer.exe のような正当なシステムプロセスが、実は悪意ある powershell.exe や cmd.exe を生み出しているという不自然な因果関係——これを暴く唯一にして最果ての手段が、揮発性メモリの深部を直接覗き込むメモリフォレンジックである。
本稿では、次世代メモリフォレンジックフレームワークである Volatility 3 を用い、プロセスツリーの再構築から親プロセス偽装の深層検知に至るまでの全プロセスを、実戦的なコマンドとカーネルの挙動を交えて徹底的に解説する。
—
1. なぜ「親プロセス偽装」は検知をかいくぐれるのか?
Windows OSにおいて、プロセスが生成される際(CreateProcess APIの呼び出し時)、通常は自身のプロセスID(PID)が子プロセスの親として記録される。しかし、攻撃者はAPIの隠しパラメータや、プロセス生成構造体である STARTUPINFOEX の中に PROC_THREAD_ATTRIBUTE_PARENT_PROCESS 属性を明示的に指定することで、「親プロセスのPIDを任意のプロセス(例えば explorer.exe や svchost.exe)に偽って子プロセスを起動する」ことが可能になる。
これにより、セキュリティ監査の現場でアナリストが pstree を確認した際、不審なスクリプト実行環境あたかも正当なシステム管理活動の一環であるかのように誤認させることができる。
しかし、いかにAPIレベルやユーザランドの構造体で親PIDを偽装しようとも、OSカーネルが管理するデータ構造のすべてを欺くことは極めて困難である。ここにメモリフォレンジックの勝機がある。
—
2. Volatility 3によるプロセスツリーの再構築 (windows.pstree)
まずは、取得したメモリダンプ(RAW形式やVMwareの.vmemなど)に対し、Volatility 3の windows.pstree プラグインを実行し、プロセスの親子関係を俯瞰する。
# Volatility 3を用いたプロセスツリーの出力例
python3 vol.py -f memdump.raw windows.pstree
このコマンドを実行すると、以下のようなツリー構造が標準出力に得られる。
PID PPID ImageFileName Offset(V)
--------------------------------------------------------------
624 544 smss.exe 0xfa80012b3040
712 624 csrss.exe 0xfa80012d4060
784 732 wininit.exe 0xfa80012f5080
832 784 services.exe 0xfa8001321090
1240 832 svchost.exe 0xfa80014a20b0
4120 1240 explorer.exe 0xfa80018b60c0
5892 4120 powershell.exe 0xfa80019f10e0
一見すると、PID 4120 の explorer.exe が、PID 5892 の powershell.exe を正当に起動しているように見える。だが、ここからがアナリストの腕の見せ所だ。この「見せかけのツリー」の裏に隠された矛盾を暴いていく。
—
3. カーネルの深層へ:プロセスの二重構造と偽装の痕跡
Volatility 3の優れている点は、単にAPIが返す情報を鵜呑みにするのではなく、カーネル内の未公開構造体や、プロセスが持つEPROCESS(Executive Process)ブロックを直接走査する点にある。
真の親プロセス関係を暴くためには、windows.pslist や windows.psscan を併用し、プロセス構造体が保持する本来のポインタを精査する必要がある。
EPROCESS構造体における不整合の特定
Windowsカーネル内において、各プロセスは _EPROCESS 構造体として管理されている。ここには、以下の2つの重要な情報が存在する。
1. InheritedFromUniqueProcessId: プロセス生成時に記録された親プロセスのPID(ここに偽装された値が入る)。
2. ActiveProcessLinks: プロセス間の双方向連結リスト。
3. Token: プロセスが保持するセキュリティコンテキスト。
親プロセス偽装が行われた場合、InheritedFromUniqueProcessId は書き換えられるが、プロセス生成のコンテキストや、親プロセス側が持つ子プロセス管理のリンケージ、あるいはプロセス作成時のタイムスタンプ(CreateTime)やハンドルテーブルの整合性には矛盾が生じることが多い。
特に、explorer.exe(通常はユーザーセッションごとに1つ、あるいは特定の特権で動作)が、権限の異なる別のユーザーセッションで動作するプロセスを突然生み出している場合などは、明確な異常値となる。
—
4. 自動化とディープインスペクション:Pythonスクリプトによる検証ロジック
実際のインシデントレスポンス現場では、数千のプロセスを目視で確認することは不可能である。そのため、Volatilityの出力やPython APIを活用し、不審な親子関係を自動でフィルタリングする仕組みが求められる。
以下は、Volatility 3のフレームワークをインポート、あるいはログを解析し、「正当な親プロセスと子プロセスの組み合わせ」のホワイトリストから外れたものを検出するための概念的な解析ロジックのサンプルである。
# インシデントレスポンスにおける親プロセス偽装検出ロジックの概念実装
import sys
# 組織内の標準的なプロセス起動関係(ホワイトリストの例)
VALID_PARENT_CHILD_MAPPING = {
"explorer.exe": ["cmd.exe", "powershell.exe", "notepad.exe", "chrome.exe"],
"services.exe": ["svchost.exe", "msmpeng.exe", "WmiPrvSE.exe"],
"wininit.exe": ["services.exe", "lsass.exe", "lsaiso.exe"]
}
def detect_process_spoofing(process_list):
"""
プロセスリスト(PID, PPID, ImageFileName, RealParentPID)を受け取り、
親プロセス偽装の疑いがあるエントリを検知する関数
"""
anomalies = []
for proc in process_list:
pid = proc['pid']
ppid = proc['ppid']
image_name = proc['image_name'].lower()
real_ppid = proc['real_ppid'] # カーネル構造体から直接取得した本来の親PID
# 1. PPIDとReal_PPIDの不一致チェック(最も直接的な偽装の証拠)
if ppid != real_ppid:
anomalies.append({
"type": "Direct PPID Mismatch",
"pid": pid,
"image": image_name,
"reported_ppid": ppid,
"actual_ppid": real_ppid,
"description": "APIレベルの親PIDとカーネル構造体の値が一致しません。親プロセス偽装の強力な兆候です。"
})
# 2. コンテキスト異常チェック(例:本来あり得ない親からの子プロセス生成)
# ※ ここでは簡易的に親の画像名と子の関係性を検証
parent_image = get_image_name_by_pid(process_list, ppid)
if parent_image in VALID_PARENT_CHILD_MAPPING:
if image_name not in [c.lower() for c in VALID_PARENT_CHILD_MAPPING[parent_image]]:
anomalies.append({
"type": "Unexpected Process Lineage",
"pid": pid,
"image": image_name,
"parent_image": parent_image,
"description": "許可されていない親プロセスから子プロセスが生成されています。"
})
return anomalies
def get_image_name_by_pid(process_list, target_pid):
"""指定されたPIDのイメージ名を取得するヘルパー関数"""
for proc in process_list:
if proc['pid'] == target_pid:
return proc['image_name'].lower()
return "unknown"
# 実行例(モックデータを用いた検証)
if __name__ == "__main__":
# 模擬的なプロセスフォレンジックデータ
mock_memory_processes = [
{"pid": 544, "ppid": 400, "real_ppid": 400, "image_name": "smss.exe"},
{"pid": 4120, "ppid": 624, "real_ppid": 624, "image_name": "explorer.exe"},
# 攻撃者が explorer.exe (PID 4120) を親に見せかけたが、実際の親は lsass.exe (PID 680) または不審なマルウェア (PID 9999) であるケース
{"pid": 5892, "ppid": 4120, "real_ppid": 9999, "image_name": "powershell.exe"}
]
detected_threats = detect_process_spoofing(mock_memory_processes)
for threat in detected_threats:
print(f"[!] 検出アラート: {threat['type']}")
print(f" 対象PID: {threat['pid']} ({threat['image']})")
print(f" 詳細: {threat['description']}\n")
このコードが示す通り、表面上のPID(ppid)と、低レイヤのカーネル構造体や文脈から導き出される本来の親PID(real_ppid)の乖離を突くことこそが、高度な偽装手法を無力化する鍵となる。
—
5. チーフホワイトハッカーの視点:フォレンジックから次世代防御への昇華
メモリフォレンジックによって親プロセス偽装を暴いた後、セキュリティアーキテクトがなすべき真の仕事は、単なる「インシデントの収束」ではない。なぜその偽装が許容されてしまったのか、という防御側のアーキテクチャの欠陥を是正することだ。
1. EDRの挙動監視の強化: プロセス生成時のAPI呼び出し(NtCreateUserProcess)だけでなく、カーネルコールバック(ObRegisterCallbacks)を利用したハンドル操作の監視を徹底し、ユーザーランドレベルでのプロセス属性の書き換えをドライバー層でブロックする。
2. ASR(Attack Surface Reduction)ルールの導入: 「WindowsのスクリプトHostやPowerShellが、正当な親プロセス以外から実行されることをブロックする」といったポリシーを組織全体のエンドポイントに適用する。
3. メモリ保全プロトポリスの自動化: 異常なプロセスツリーを検知した瞬間に、ライブレスポンスツールが自動的にメモリダンプをトリガーし、Volatility 3を用いた自動解析パイプラインに流し込む仕組みを構築する。
攻撃者は常にOSの「信頼の連鎖(Chain of Trust)」の隙間を狙っている。しかし、メモリという一切の嘘をつかない物理的実体を前にしては、いかなる巧妙な偽装も必ず綻びを見せる。Volatility 3を駆使したメモリフォレンジックは、現代の高度なサイバー攻撃に対峙するすべてのエンジニアにとって、真実を暴くための最も鋭利なメスである。
コメント