【実務・中級編】 コンテナ環境でのメモリダンプ取得時の整合性確保 – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

コンテナの「死」を看取るな:現場で戦うためのメモリフォレンジック術

コンテナ環境でのインシデントレスポンス(IR)は、いわば「時限爆弾を抱えながら外科手術をする」ようなものだ。従来の物理サーバーやVMであれば、メモリダンプを採取する時間は十分にある。しかし、コンテナにおいて「調査のためにコンテナを停止します」などと言おうものなら、攻撃者が仕掛けたLiveness Probeや自動スケーリングのトリガーにより、次の瞬間にはコンテナが消滅し、証拠もろともフォレンジックの機会が霧散する。

今回は、我々が現場で直面する「コンテナの消失」という悪夢を回避し、メモリの整合性を保ちつつ「生きた証拠」を確保するための実践的なアプローチを解説する。

—

攻撃者が狙う「フォレンジックの盲点」

攻撃者は、侵害後にメモリ上で難読化されたペイロードを展開し、ファイルシステムには痕跡を残さない「ファイルレス攻撃」を好む。彼らは自身のプロセスを監視し、コンテナの再起動やリソース枯渇を検知すると、証拠を焼き切るスクリプトを仕込んでいることさえある。

ここで重要なのは、「コンテナを止めるのではなく、静止させる」という考え方だ。コンテナランタイムを一時停止(Pause)させ、外部からメモリをダンプする手法が、最もインテグリティ(整合性)を維持できる。

—

現場で使える:コンテナ静止とダンプの自動化

DockerやKubernetes環境で、コンテナを殺さずにメモリを吸い出すには、docker pause を活用し、ホスト側から gcore や LiME を叩くのが定石だ。以下に、インシデント発生時に即座に叩くべき、Pythonによる自動取得スクリプトのひな形を示す。

コンテナメモリ抽出用 Python スクリプト

このスクリプトは、対象コンテナを凍結(Pause)し、メモリダンプを取得後、即座に解凍(Unpause)する。

import subprocess
import time

def dump_container_memory(container_id, output_path):
    try:
        # 1. コンテナを凍結させ、実行状態を完全に固定する
        print(f"[*] コンテナ {container_id} を凍結中...")
        subprocess.run(["docker", "pause", container_id], check=True)
        
        # 2. プロセスID(PID)を取得
        pid = subprocess.check_output(
            ["docker", "inspect", "--format", "{{.State.Pid}}", container_id]
        ).decode().strip()
        
        # 3. gcoreを使用してメモリダンプを取得(対象プロセスの全メモリをダンプ)
        print(f"[*] PID: {pid} のメモリをダンプ中...")
        subprocess.run(["gcore", "-o", output_path, pid], check=True)
        
        print(f"[+] ダンプ完了: {output_path}.{pid}")

    except subprocess.CalledProcessError as e:
        print(f"[!] エラー発生: {e}")
    finally:
        # 4. 調査後は必ず解除(これを忘れるとサービスダウンに直結する)
        subprocess.run(["docker", "unpause", container_id])
        print("[*] コンテナの凍結を解除しました。")

# 実行例
# dump_container_memory("target_container_name", "/tmp/forensics/dump")

—

コンテナを「守る」ためのインフラ設定:ヘルスチェックの罠

フォレンジック中に最も恐ろしいのは、取得作業中にKubernetesの livenessProbe が「応答なし」と判断してコンテナを強制再起動(Kill)してしまうことだ。これを防ぐためには、フォレンジック専用の実行権限をIAMで制限しつつ、一時的にプローブを無効化する手順を準備しておく必要がある。

K8sにおける「証拠保全モード」の設定例

緊急時、対象コンテナのPod定義から一時的に livenessProbe を外す、あるいは initialDelaySeconds を極端に長くするパッチを当てるのが現場の知恵だ。

# パッチ適用用のJSONサンプル(kubectl patchで使用)
spec:
  containers:
  - name: app-container
    livenessProbe:
      # 調査中、一時的にタイムアウトを延長してKillを防ぐ
      failureThreshold: 10
      periodSeconds: 60
      initialDelaySeconds: 300

—

エンジニアへの教訓:ログだけでは真実は見えない

最後に、心に留めておいてほしいことがある。ログは「攻撃者があなたに見せたいもの」であり、メモリは「攻撃者が実際に実行したすべて」だ。

1. タイムスタンプの整合性: メモリダンプ取得時は、ホストの時計とNTPのズレを確認すること。
2. サイドカーの活用: 本番環境では、フォレンジック専用のサイドカーコンテナをデプロイできるよう、Dockerfileに gdb や gcore を含める計画を立てておけ(ただし、本番イメージに含めるべきか否かはセキュリティポリシーと相談だ)。
3. 自動化の罠: 自動取得スクリプトは、必ずテスト環境で「コンテナが確実に再開するか」を検証してから運用に回すこと。

インシデントレスポンスは、準備が9割。今、君たちの目の前にあるコンテナが、次の瞬間に「空っぽの器」にならないよう、このコードを武器として備えておいてほしい。現場からは以上だ。次回の講義では、メモリダンプ内の難読化されたシェルコードをどうやって特定するか、その手法を紐解くとしよう。

コメント

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