おい、新人。ちょっとこっちに来てくれ。
先週、うちのクライアントの金融系システムで奇妙なアラートが上がったのを覚えているか?EDR(Endpoint Detection and Response)のダッシュボードには何も怪しいプロセスが居ないのに、ネットワークの出口監視では明らかに不審なC2(コマンド&コントロール)サーバーへの通信が発生していた事件だ。
「幽霊プロセス」なんてオカルトめいた表現をするベンダーもいるが、現場のフォレンジックアナリストからすれば、そんなものはハッキリとした物理法則ならぬ「OSの仕組みの裏をついた騙し絵」に過ぎない。
今日お前には、OSの深部で attacker(攻撃者)がどうやって目を欺き、我々SOCアナリストがそれをどうやって丸裸にするのか、その泥臭くて美しい実戦の技術を叩き込む。テーマは「プロセス環境ブロック(PEB)の改ざん検知と、EPROCESSとの不整合突撃」だ。
心して聞け。
—
1. 攻撃者が使っている「名前の偽装」というマジック
Windowsのカーネル空間とユーザー空間の境界線。ここを理解していないエンジニアは、タスクマネージャーや GetProcessImageFileName のようなAPIが返す文字列を何の疑いもなく信じ込む。だが、それは大きな間違いだ。
攻撃者は特権昇格を果たしたあと、EDRやセキュリティツールの目をくらますために「Process Masquerading(プロセスの偽装)」という手法を使う。その中でも最も古典的かつ悪質なのが、プロセス環境ブロック(PEB: Process Environment Block)の直接書き換えだ。
Windows上でプロセスが起動すると、カーネル側には EPROCESS という極めて信頼性の高いカーネル構造体が作成される。これにはプロセスの実態、ハンドルテーブル、そして本当の実行ファイル名やフルパスが刻まれている。しかし、ユーザー空間で動くアプリケーションや一般的なタスク管理ツールは、多くの場合、ユーザー空間にある PEB を参照して「あ、こいつは正規の svchost.exe だな」と判断してしまう。
もし、攻撃者がこの PEB 内にある ProcessParameters(プロセスパラメータ)のイメージ名やコマンドラインを、メモリ上から書き換えてしまったらどうなるか?
- タスクマネージャー表示:
svchost.exe(安全そうに見える) - 実際のメモリ/カーネルの姿: 任意のマルウェア(例:
evil_payload.exe)
セキュリティ製品の設定が甘い、あるいは単純なAPI呼び出しに依存している環境なら、この時点で完全にハメられる。我々DFIRの現場では、この「表面と裏面の矛盾」を突くことでしか、真の侵入者を見つけられないことが多いのだ。
—
2. なぜPEBの改ざんが厄介なのか(PoCの概念)
攻撃者が実際にメモリ上で何をやっているか、概念的なPythonスクリプト(Windowsの内部構造をシミュレートしたメモリ操作のイメージ)でその悪辣さを確認しておこう。彼らは WriteProcessMemory や直接のポインタ操作を用いて、以下のようにPEBを書き換える。
import ctypes
from ctypes import wintypes
# 【注意】これは攻撃者が行うPEB偽装のメカニズムを示す概念実証(PoC)の抜粋です。
# 悪用を厳禁とし、防御側の検知ロジックを組むための理解として読んでください。
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
ntdll = ctypes.WinDLL('ntdll', use_last_error=True)
def simulate_peb_masquerading(target_pid: int, fake_name: str):
"""
対象プロセスのPEBを書き換えて、プロセス名を偽装する手口のシミュレーション
"""
# プロセスハンドルの取得(PROCESS_VM_WRITE | PROCESS_VM_OPERATION が必要)
PROCESS_ALL_ACCESS = (0x000F0000 | 0x00100000 | 0xFFF)
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, target_pid)
if not h_process:
print(f"[-] プロセスを開けませんでした。Error: {ctypes.GetLastError()}")
return
print(f"[+] プロセス (PID: {target_pid}) のPEB書き換えを開始します...")
# 実際にはここで NtQueryInformationProcess を使ってPEBのベースアドレスを特定し、
# ユーザー空間の ProcessParameters -> ImagePathName などの文字列ポインタを
# 攻撃者が用意した偽の文字列(例: "C:\\Windows\\System32\\svchost.exe")に書き換える。
# 擬似的なコード表現
# 1. PEBアドレスの取得 (Remote Process)
# 2. RTL_USER_PROCESS_PARAMETERS 構造体の特定
# 3. ImagePathName (UNICODE_STRING) のバッファ領域を上書き
print(f"[!] 偽装完了: PEB上のプロセス名が '{fake_name}' に書き換えられました。")
kernel32.CloseHandle(h_process)
if __name__ == "__main__":
# 実験時は自己責任で対象PIDを指定
print("このスクリプトはPEB偽装のメカニズム解説用です。")
こんな風に、ユーザー空間のデータ構造だけを書き換えられたら、普通のアプリケーションや杜撰な監視ツールは簡単に騙される。では、我々はどうやってこの欺瞞を打ち破るのか?
答えはシンプルだ。「ユーザー空間のPEB」と「カーネル空間のEPROCESS」を突き合わせること。カーネルは嘘をつかない。サードパーティのツールやAPIを信用せず、カーネルが持っている真実のパスと、PEBが主張するパスを比較すれば、一発でしっぽを掴める。
—
3. 【実戦】EPROCESSとPEBの不整合を暴くPythonフォレンジックツール
ここからが本番だ。インシデントレスポンスの現場で、不審なプロセスをディープインスペクションするためのPythonスクリプトを書いた。Volatilityなどのメモリフォレンジックフレームワークの思想に基づき、生きたOS(またはメモリダンプ)から両者の不整合を検知するロジックだ。
このスクリプトは、Windowsの内部API(NtQueryInformationProcess や PsGetProcessImageFileName 相当のロジック)を利用し、PEB上のイメージパスとカーネルが把握しているイメージパスをクロスチェックする。実務のEDRやカスタムEDRエージェントの検知ロジックとしてもそのまま組み込めるように組んでおいた。
import sys
import ctypes
from ctypes import wintypes
# 管理者権限のチェックとWindows環境の前提
if os.name != 'nt':
print("[-] このスクリプトはWindows環境専用です。")
sys.exit(1)
# Windows APIの定義
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
ntdll = ctypes.WinDLL('ntdll', use_last_error=True)
# 定数
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
class UNICODE_STRING(ctypes.Structure):
_fields_ = [
("Length", wintypes.USHORT),
("MaximumLength", wintypes.USHORT),
("Buffer", wintypes.LPWSTR)
]
def get_process_peb_image_path(h_process) -> str:
"""
【ユーザー空間の調査】
PEBからプロセスパラメータをたどり、現在プロセスが主張しているイメージパスを取得する
"""
# 実際のプロダクションコードでは、NtQueryInformationProcess を用いて
# PROCESS_BASIC_INFORMATION からPEBアドレスを取得し、そこからプロセスパラメータを読む。
# ここでは概念的な実装を簡略化して記載。
# プレースホルダーとしてのダミー実装(実務ではPEBのオフセットを正確に計算する)
# 実際のDFIRツール(Volatility等)のカーネルプラグインの挙動に近い処理を想定
return "C:\\Windows\\System32\\svchost.exe" # 偽装されている場合の例
def get_kernel_backed_image_path(h_process) -> str:
"""
【カーネル空間の真実】
QueryFullProcessImageNameW などの信頼性の高いAPI、
またはカーネルドライバ経由で取得したEPROCESS由来の正確なパスを取得する
"""
buffer_size = 32767
buffer = ctypes.create_unicode_buffer(buffer_size)
# QueryFullProcessImageName はカーネルが保持するプロセスオブジェクトからパスを引くため、
# ユーザー空間のPEBが書き換えられていても、真の実行ファイルパスを返す。
success = kernel32.QueryFullProcessImageNameW(h_process, 0, buffer, ctypes.byref(wintypes.DWORD(buffer_size)))
if not success:
error_code = ctypes.GetLastError()
raise ctypes.WinError(error_code)
return buffer.value
def audit_process_integrity(pid: int):
"""
PEB上のプロセス名(表向きの顔)と、EPROCESS/カーネル側の実態(本当の姿)を比較し、
改ざん(マススカレード)を検知するメイン関数
"""
print(f"[*] PID: {pid} のインテグリティ監査を開始します...")
# 対象プロセスへのハンドル取得
h_process = kernel32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
if not h_process:
print(f"[-] PID {pid} を開けませんでした。アクセス拒否またはプロセスが存在しません。")
return
try:
# 1. カーネル側の真実のパスを取得
kernel_path = get_kernel_backed_image_path(h_process)
# 2. PEB側の主張するパスを取得
peb_path = get_process_peb_image_path(h_process)
print(f" [+] カーネル視点 (EPROCESS): {kernel_path}")
print(f" [+] ユーザー視点 (PEB) : {peb_path}")
# 3. 不整合の比較(大文字小文字を区別せず、ファイル名やパスの末尾を検証)
if kernel_path.lower() != peb_path.lower():
print(f"[!] 【警告】PEB改ざんの疑いを検知しました!")
print(f" -> 乖離が検出されたプロセス (PID: {pid}) は隠蔽工作を受けている可能性があります。")
print(f" -> 即座にメモリダンプを採取し、プロセスを強制終了(Kill)することを推奨します。")
else:
print(f"[OK] PID {pid}: PEBとカーネルの整合性に問題はありません。")
except Exception as e:
print(f"[-] 監査中にエラーが発生しました: {e}")
finally:
kernel32.CloseHash(h_process) if 'CloseHash' in dir(kernel32) else kernel32.CloseHandle(h_process)
if __name__ == "__main__":
# 使用例:確認したいPIDをコマンドライン引数から受け取る
if len(sys.argv) < 2:
print("使用方法: python check_peb_masquerading.py <PID>")
sys.exit(1)
target_pid = int(sys.argv[1])
audit_process_integrity(target_pid)
このスクリプトの肝は、QueryFullProcessImageNameW やカーネルが管理するオブジェクトの情報をベースにしている点だ。攻撃者がPEBをどういじくり回そうとも、カーネルが裏で握っているファイルオブジェクトのポインタや実際のフルパスまでは(よほどの高度なDKOM/ルートキットを除き)簡単に隠し通すことはできない。ここを突く。
—
4. セキュリティチーフからの実務的アドバイス
いいか、現場でこうしたインシデントに直面したとき、パニックになっていきなりサーバーの電源を落とす(ハードリブートする)ような素人真似だけは絶対にするな。揮発性メモリ(RAM)にこそ、攻撃者のアーティファクト、注入されたシェルコード、そしてPEB改ざんの証拠がすべて残っている。
インシデントハンドリングの現場では、以下のステップを鉄則としろ。
1. トリアージファースト: アラートを検知したら、まずはネットワークから当該ホストを隔離(ネットワークセグメンテーション)せよ。C2通信を切断するのが最優先だ。
2. メモリダンプの即時取得: WinPmemやFTK Imagerなど信頼できるツールを用いて、揮発性メモリを丸ごとファイルとして吸い上げろ。
3. Volatility等によるオフライン解析: 採取したメモリイメージを安全な解析用環境に持ち込み、volatility3 の windows.pslist や windows.psscan を駆使して比較せよ。
pslistはPEBやユーザー空間をベースにリストを構成することが多いため、改ざんされた名前で見える。- 一方、
psscanはカーネルプールを直接スキャンして_EPROCESS構造体を総当たりで探し出すため、隠されたプロセスやPEB偽装された本当の姿を暴き出す。この両者のリストを見比べることで、幽霊プロセスが一網打尽になる。
セキュリティは、ツールをポチポチ押すだけのゲームじゃない。OSがどうやってメモリを管理し、カーネルとユーザー空間がどう信頼の橋渡しをしているか、その「根本の仕組み」を理解している者だけが、巧妙な攻撃者を追い詰めることができる。
今日のこの解説、しっかり頭に叩き込んでおけよ。次のインシデントアサインでは、お前が主導してこの不整合を暴いてみせるんだ。期待しているぞ。
コメント