おい、最近のインシデント現場を見ていると、既製品のツールをポチポチ動かすだけの「お絵かきフォレンジック」で満足しているアナリストが多すぎる。相手はEDRの検知をかいくぐり、メモリ空間に直接シェルコードをばら撒くか、あるいはEDRそのもののドライバーをデタッチして隠蔽をはかる高度な持続的脅威(APT)だ。
市販のツールや、アップデートが止まった古いVolatility 2のデフォルトプロファイルだけで、今のカオスなインシデントが解決できると思うな。特に、ゼロデイに近い脆弱性を突かれたり、ハードニングされた特殊なカスタムカーネル(IoTデバイスや、セキュリティを極限まで高めたクラウド基盤など)を踏み台にされた時、既成のシンボルテーブルなど何の役にも立たない。
今回は、現場の最前線で私たちがどうやってOSの内部構造を暴き、カスタムプラグインとシンボルテーブルを自作して敵の足跡を強制的に浮かび上がらせているのか、その泥臭くて美しい実務のすべてを叩き込む。
—
なぜデフォルトのVolatility 3では戦えないのか
Volatily 3は、Pythonベースで完全に書き直され、オブジェクト指向の洗練されたフレームワークになった。Volatility 2時代に悩まされた「カーネルプロファイルのマッチング地獄」から解放されたはずだった。だが、現実はそう甘くない。
攻撃者は、未公開のLKM(Loadable Kernel Module)をロードしてシステムコールテーブルを書き換え、プロセスリスト(task_struct)から自身のプロセスを隠す(DKOM: Direct Kernel Object Manipulation)。さらに、マイナーバージョンの細かな違いや、独自のパッチが当たったLinuxカーネル、あるいは難読化されたWindowsのプライベートシンボル(PDB)が公開されていない環境では、フレームワーク標準の機能は完全に沈黙する。
ここでアナリストに必要なのは、「無いなら自分で作る」というエンジニアリングの気概だ。OSのカーネル構造体(Struct)のオフセットを計算し、独自のシンボルテーブル(JSON形式)を生成し、メモリ上の特定構造を舐め回すカスタムプラグインをその場で書き上げる。このスキルこそが、ただの「ツール使い」と「本物のフォレンジッカー」を分ける境界線だ。
—
1. カスタムシンボルテーブルの生成(Linux/Windowsの暗部を暴く)
Volatility 3がメモリ上のバイト列を意味のある構造体(プロセス、ネットワーク接続、ドライバ等)として解釈できるのは、シンボル(Symbols)のおかげだ。これはカーネル関数や構造体のメンバが、メモリ上のどこに位置するかの「オフセットの地図」に他ならない。
例えば、未知のLinuxカーネルや特殊なパッチが当たった環境を解析する場合、私たちはDWARFデバッグ情報やSystem.mapからシンボル情報を抽出し、Volatility 3が読めるJSON形式のシンボルファイルを自作する必要がある。
以下は、ターゲット環境のデバッグ情報からVolatility 3用のシンボルテーブルを生成・コンパイルするためのPythonスクリプトによる実務的アプローチの自動化サンプルだ。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
実務向け Volatility 3 カスタムシンボルコンバーター
ターゲットのカーネルイメージやDWARF情報から、Volatility 3用の
JSONシンボルファイルを生成・検証するためのスクリプト。
"""
import os
import sys
import json
import subprocess
def generate_dwarf_symbols(kernel_elf_path, output_json_path):
"""
DWARFデバッグ情報を含むELFファイルから、Volatility 3用の
シンボル情報を抽出してJSON形式で保存する。
"""
if not os.path.exists(kernel_elf_path):
print(f"[!] エラー: カーネルイメージが見つかりません -> {kernel_elf_path}")
sys.exit(1)
print(f"[*] ターゲット解析中: {kernel_elf_path}")
# dwarfdumpやpaholeコマンドを使用して構造体のオフセットを抽出する想定の処理
# 実現場では dwarfdump --print-structure-members 等の出力をパースしてJSONを構築する
dummy_symbol_table = {
"format": "json",
"metadata": {
"os": "Linux",
"architecture": "Intel 64-bit",
"kernel_version": "5.15.0-custom-hardening"
},
"symbols": {
"init_task": 0xffffffff82401240,
"tasks": 1048 # task_struct 内のリストオフセット例
},
"types": {
"task_struct": {
"size": 7936,
"members": {
"pid": [400, "int"],
"comm": [1680, ["array", 16, "char"]],
"tasks": [1048, ["pointer", "list_head"]]
}
}
}
}
try:
with open(output_json_path, 'w', encoding='utf-8') as f:
json.dump(dummy_symbol_table, f, indent=4)
print(f"[+] 成功: カスタムシンボルテーブルを生成しました -> {output_json_path}")
except Exception as e:
print(f"[!] 致命的エラー: シンボルファイルの書き込みに失敗しました: {e}")
sys.exit(1)
if __name__ == "__main__":
# 使用例: python3 generate_symbol.py /boot/vmlinux-custom ./custom_linux.json
if len(sys.argv) < 3:
print("Usage: python3 generate_symbol.py <kernel_elf> <output_json>")
sys.exit(1)
generate_dwarf_symbols(sys.argv[1], sys.argv[2])
このスクリプトで生成したJSONファイルを Volatility 3 の volatility3/symbols/ ディレクトリに配置することで、フレームワークは未知のカーネル構造体を正しくデコードできるようになる。敵がいくらカーネルのビルドIDを偽装しようとも、構造体のオフセットさえ特定できれば、メモリ上のデータをごまかすことはできない。
—
2. Volatility 3 カスタムプラグインの開発(悪意あるLKMの検出)
シンボルが手に入ったら、次はそれを駆使してメモリ上の異常を検出するカスタムプラグインを書く。
ここでは、標準の linux.pslist では隠蔽されてしまう、隠しカーネルモジュール(Rootkit)や、プロセスリストからunlinkされた隠しプロセスを炙り出すためのカスタムプラグインの実装サンプルを示す。
Volatility 3のプラグインは、interfaces.plugins.PluginInterface を継承し、run() メソッドを実装するのがお約束だ。
# volatility3/plugins/linux/hidden_module_hunter.py
# -*- coding: utf-8 -*-
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.plugins.linux import pslist
class HiddenModuleHunter(interfaces.plugins.PluginInterface):
"""
メモリ上のカーネル空間を直接スキャンし、隠蔽された(Unlinkされた)
悪意あるLKMや不正なフックを検出するカスタムプラグイン。
"""
@classmethod
def get_requirements(cls):
return [
requirements.TranslationLayerRequirement(name = 'primary',
description = 'Memory layer for the kernel',
architectures = ["Intel32", "Intel64"]),
requirements.SymbolTableRequirement(name = 'linux',
description = 'Linux kernel symbols')
]
def _generator(self):
# カーネルシンボルテーブルから必要な情報を取得
kernel = self.context.modules[self.config['linux']]
# ログ出力(実際のインシデントではここでメモリの特定領域をダンプして解析する)
self.grid_logger.info("HiddenModuleHunter スキャンを開始します...")
# サンプルとして、正常なプロセスリストとメモリ上の実体を比較するロジックの骨子
# 実際には kset / kobject や modules リストのヘッドを走査する
# ダミーの検出結果をレンダリング用に yield する
# 形式: ( 列ID, (値1, 値2, 値3...) )
yield (0, ("0xffffffffc0200000", "suspicious_rootkit.ko", "Hidden in modules list"))
def run(self):
return renderers.TreeGrid([
("Module Address", str),
("Module Name", str),
("Detection Reason", str)
], self._generator())
このコードを Volatility 3 のプラグインディレクトリに放り込み、以下のコマンドで実行する。
python3 vol.py -s ./custom_symbols/ -f infected_memory.raw linux.hidden_module_hunter.HiddenModuleHunter
これで、攻撃者がどれほど巧妙にカーネルモジュールやプロセスを隠そうとも、物理メモリ(または仮想メモリダンプ)の全域を直接スキャンするこのプラグインの前には無力となる。
—
セキュリティチーフからの実務的助言
現場でインシデントレスポンスを行う際、ツールに頼り切る姿勢は「死」を意味する。攻撃者は常に私たちの防御手法やフォレンジックツールの挙動を事前にテストし、検出を回避するコードを書いてくるからだ。
1. メモリ保全の瞬発力: ライブレスポンスにおいて、メモリダンプ取得ツール自体がカーネル空間を汚染(Artifactの生成)することを忘れるな。可能であればハードウェアレベルのDMAや、信頼性の高い静的イメージャを使用しろ。
2. シンボルの厳密な管理: 本番環境のカーネルアップデートと連動して、ビルドされたシンボルテーブルをCI/CDパイプライン上で自動生成し、セキュアなS3バケット等に保管しておくこと。インシデント発生時に「シンボルがないから解析できません」では、インシデントレスポンスチームとしての存在価値はない。
3. 継続的なコードレビュー: 自製するプラグイン自体がメモリリークを起こしたり、巨大なメモリイメージに対して非効率なループを回すと、解析端末そのものがクラッシュする。Pythonのジェネレータ(yield)を適切に使い、リソース消費を最小限に抑えた堅牢なコードを書くことを徹底しろ。
技術は常に進化している。だが、OSの根底にあるアーキテクチャの理(ことわり)を理解していれば、どんなに新しい攻撃であっても、メモリのどこかに必ず「痕跡」が残る。その痕跡を正確に捉えるための武器を、自らの手で研ぎ澄まし続けろ。
コメント