【実務・中級編】 メモリフォレンジックにおけるタイムライン分析の自動化 – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

おい、少し手を止めてこっちを向いてくれ。

今朝、クライアントのCISOから真っ青な顔で電話が入った。「全社端末のEDRアラートは静かなのに、ドメインコントローラーへの不審なラテラルムーブメントが検知された」とね。こういうとき、君たちが真っ先にやりがちなのが、ディスクのライブイメージ取得だ。でも、ちょっと待ってほしい。現代の高度な脅威アクターは、ファイルレス攻撃やインメモリでのDLLインジェクションを駆使し、ハードディスクには一文字も悪意ある痕跡を残さない。彼らが息を潜めているのは、RAM(物理メモリ)という名の暗黒世界だけだ。

そして、その混沌としたメモリ空間から、敵の足跡を秒単位で復元するのが「メモリフォレンジックにおけるタイムライン分析」だ。

今日は、教科書には載っていない、現場の泥臭いインシデントレスポンスの真髄と、それを自動化してキルチェーンを丸裸にする方法を叩き込んでやる。心して聞けよ。

—

なぜ「静的なログ」だけでは攻撃者を見逃すのか

システム管理者やWebエンジニアの多くは、SIEMやSyslog、あるいはWindowsのセキュリティイベントログを信じ切っている。だが、現場のインシデントレスポンスを数多くこなしてきた私から言わせれば、ログは「攻撃者に都合よく改ざん・消去されうるもの」だ。

高度なAPTグループやランサムウェアの先兵たちは、侵害に成功すると、まず最初にイベントログサービス(EventLog)を停止するか、プロセスインジェクションによって既存の正当なプロセス(例えば svchost.exe や explorer.exe)のコンテキスト内で悪意あるコードを実行する。ディスク上のファイルを漁っても、タイムスタンプ(MACB)は巧妙に書き換えられている。

ここで武器になるのが揮発性メモリのタイムライン分析だ。メモリ上には、OSが隠そうとしても隠しきれない「生の活動履歴」が残る。

  • プロセスが生成された正確なミリ秒単位のタイムスタンプ
  • ネットワークコネクションが確立され、すでに切断されたソケットの残骸
  • 突如としてアロケートされ、今は解放されたヒープ領域の断片

これらを時系列(タイムライン)で綺麗に一本の線として繋ぎ合わせることこそが、攻撃者のキルチェーン(初期潜入、権限昇格、内部偵察、横展開)を完全に可視化する唯一の手段なのだ。

—

現場で使える!メモリタイムライン自動生成 Pythonスクリプト

Volatility 3などのフォレンジックフレームワークを使えば、メモリダンプからプロセスのリストやネットワーク接続を抽出できる。しかし、数千件におよぶイベントを手作業で突合してExcelで並べ替えるなどという愚行は、夜中の3時には絶対に発狂する原因になる。

ここでは、Volatility 3の出力をパースし、プロセス生成・ネットワーク・ハンドル情報をマージして、一つの時系列JSONおよびCSVタイムラインとして出力する実用的なPythonスクリプトを授けよう。現場の初動調査でそのまま叩き込めるよう、エラーハンドリングと丁寧なコメントを仕込んである。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import csv
import json
from datetime import datetime
import sys

def parse_volatility_pslist(csv_path):
    """
    Volatility 3 の pslist プラグインの出力をパースし、
    プロセス生成イベントとしてタイムライン用リストに正規化する。
    """
    events = []
    try:
        with open(csv_path, mode='r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                # Volatility 3 の出力カラム名を想定
                pid = row.get('PID', 'N/A')
                ppid = row.get('PPID', 'N/A')
                image_filename = row.get('ImageFileName', 'N/A')
                create_time = row.get('CreateTime', None)
                
                if create_time and create_time != 'N/A':
                    events.append({
                        'timestamp': create_time,
                        'source': 'Process Creation (pslist)',
                        'actor': f"PPID: {ppid}",
                        'target': f"PID: {pid} ({image_filename})",
                        'details': f"Process spawned: {image_filename}"
                    })
    except Exception as e:
        print(f"[!] Error parsing pslist CSV: {e}", file=sys.stderr)
        
    return events

def parse_volatility_netscan(csv_path):
    """
    Volatility 3 の netscan プラグインの出力をパースし、
    ネットワーク接続イベントとしてタイムライン用リストに正規化する。
    """
    events = []
    try:
        with open(csv_path, mode='r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                proto = row.get('Proto', 'N/A')
                local_addr = row.get('LocalAddr', 'N/A')
                foreign_addr = row.get('ForeignAddr', 'N/A')
                state = row.get('State', 'N/A')
                owner = row.get('Owner', 'N/A')
                created = row.get('Created', None)
                
                # 接続確立時刻が存在する場合のみタイムラインに組み込む
                if created and created != 'N/A':
                    events.append({
                        'timestamp': created,
                        'source': f"Network Connection ({proto})",
                        'actor': f"Process: {owner}",
                        'target': f"{local_addr} -> {foreign_addr}",
                        'details': f"State: {state}"
                    })
    except Exception as e:
        print(f"[!] Error parsing netscan CSV: {e}", file=sys.stderr)
        
    return events

def generate_unified_timeline(pslist_csv, netscan_csv, output_json):
    """
    複数の異なるフォレンジック成果物を統合し、
    タイムスタンプ順にソートされたキルチェーン可視化用JSONを生成する。
    """
    print("[*] メモリフォレンジック・タイムラインの統合処理を開始します...")
    
    timeline_events = []
    timeline_events.extend(parse_volatility_pslist(pslist_csv))
    timeline_events.extend(parse_volatility_netscan(netscan_csv))
    
    # タイムスタンプを基準に昇順ソート(攻撃の前後関係を明確にするため)
    # ※実際の運用ではパース時の時刻フォーマット統一処理が必要ですが、ここでは簡略化しています
    timeline_events.sort(key=lambda x: x['timestamp'])
    
    # 統合結果をJSONとして書き出し
    try:
        with open(output_json, mode='w', encoding='utf-8') as f:
            json.dump(timeline_events, f, indent=4, ensure_ascii=False)
        print(f"[+] タイムラインの生成に成功しました: {output_json}")
    except Exception as e:
        print(f"[!] JSONの書き込みに失敗しました: {e}", file=sys.stderr)

if __name__ == '__main__':
    # 実行例:
    # python memory_timeline_parser.py pslist.csv netscan.csv unified_timeline.json
    if len(sys.argv) < 4:
        print("Usage: python memory_timeline_parser.py <pslist.csv> <netscan.csv> <output.json>")
        sys.exit(1)
        
    pslist_input = sys.argv[1]
    netscan_input = sys.argv[2]
    output_path = sys.argv[3]
    
    generate_unified_timeline(pslist_input, netscan_input, output_path)

このスクリプトが吐き出すJSONを、社内のインシデント管理UIやJupyter Notebookにインポートすれば、どのプロセスがどのタイミングで外部のC2サーバーへ通信を試みたのかが、ミリ単位の誤差すらなく一本のドラマのように浮き彫りになる。これが自動化の威力だ。手作業の泥沼から自分を解放しろ。

—

現場のプロからの訓示:インシデント対応で絶対に忘れるな

最後に、技術的な実装以上に大切なマインドを伝えておく。

インシデントレスポンスの現場において、時間は常に我々の敵だ。攻撃者は痕跡を消そうとし、経営陣は被害の全貌を今すぐ報告しろとせっつく。だからこそ、日頃からこうした自動化スクリプトやフォレンジック環境を整備しておく必要がある。

だが、ツールに依存しすぎるな。ツールが吐き出したタイムラインの「前後の文脈」を読み解き、「なぜこのプロセスがこのタイミングで起動したのか?」という悪意のロジックに気付けるかどうかは、君たち自身のエンジニアとしての総合力、すなわちOSの内部構造(Windows internals や Linux Kernel)への深い理解にかかっている。

メモリは嘘をつかない。だが、それを翻訳する人間の目が曇っていれば、どれほどの自動化も意味をなさない。

さあ、手を動かそう。ログを見るな、メモリを見ろ。次に同じアラートが鳴ったとき、君がチームを救うヒーローになる番だ。期待しているぞ。

コメント

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