【テクニカル・上級編】 メモリフォレンジックにおけるアンチフォレンジック技術の検知 – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

メモリ空間の戦場:アンチフォレンジックを破る低レイヤDFIRの極意

現代のサイバーインシデントレスポンスにおいて、ディスク調査だけで攻撃の全容を解明することは不可能に近い。高度持続的脅威(APT)アクターや、洗練されたランサムウェアグループは、痕跡をディスクに残さない「ファイルレスマルウェア」や、メモリ上でのみ展開される「ビーコン」を常用する。

しかし、攻撃者がどれほどディスク上の足跡を消し去ろうとも、コードを実行するためには必ず「メモリ(RAM)」という物理的な物理媒体を経由せざるを得ない。メモリは嘘をつかない――これが、私たちデジタルフォレンジック・インシデントレスポンス(DFIR)アナリストの絶対的な信念である。

だが、攻撃者側もこの事実を熟知している。彼らはメモリダンプの取得を妨害し、解析ツール(VolatilityやRekallなど)のシグネチャやパーサーを欺く「アンチフォレンジック技術」を高度化させている。

本稿では、メモリダンプの妨害工作や、OSのページング機構・カーネル構造体の隙を突く高度なメモリ回避技術のメカニズムを解剖する。その上で、それらを検知・無効化するための低レイヤの防御的解析アプローチと検知ロジックを解説する。

—

1. メモリを欺く:アンチフォレンジックの極悪なアプローチ

攻撃者がメモリフォレンジックを回避するために用いるアプローチは、主に「構造の隠蔽(DKOM)」「ページテーブルの操作(PTE改ざん)」「ダンププロセスの妨害」の3つに大別される。これらは、Windowsカーネルの核心的な仕様を逆手に取ったものである。

1.1. DKOM(Direct Kernel Object Manipulation)によるプロセス隠蔽

Windowsカーネルは、実行中のすべてのプロセスを _EPROCESS という二重リンクリスト(ActiveProcessLinks)で管理している。標準的なフォレンジックツールの多く(Volatilityの pslist など)は、このリンクを辿ることでプロセスの一覧を出力する。

攻撃者は、カーネル権限(LPE脆弱性の悪用や悪意あるドライバのロードによる)を行使し、この ActiveProcessLinks から自らのプロセス(例えば、ビーコンがインジェクションされたプロセス)を「切り離す(Unlinking)」。

[正常なプロセスリスト]
Process A (FLINK) ---> Process B (FLINK) ---> Process C (FLINK)
          <--- (BLINK)           <--- (BLINK)

[DKOMによるプロセスBの隠蔽]
Process A (FLINK) --------------------------> Process C (FLINK)
          <---------------------------------- (BLINK)
               
               Process B (孤立化・実行は継続)

スレッドスケジューラは ActiveProcessLinks ではなく、スレッド構造体(_ETHREAD)のディスパッチャオブジェクトを直接参照してCPU時間を割り当てるため、リストから外されたプロセスBは、タスクマネージャーや通常のAPI、そして単純なフォレンジックツールからは完全に不可視のまま実行を続けることができる。

1.2. PTE(Page Table Entry)の操作とシャドウマッピング

x64アーキテクチャにおいて、仮想アドレスから物理アドレスへの変換は、ページテーブル(PML4 -> PDPT -> PD -> PT)を介して行われる。各ページテーブルのエントリ(PTE)には、物理アドレス(PFN)だけでなく、アクセス権限(Read/Write、User/Supervisor、No-Execute(NX))を制御するビットが含まれている。

+-------------------------------------------------------------------+
| PFN (Page Frame Number) | ... | NX | D | A | U/S | R/W | Present |
+-------------------------------------------------------------------+
                                   |                     |
                                   +-- 実行制御           +-- 読み書き制御

高度なアンチフォレンジック技術(いわゆる「Shadow Attack」や「PTE Subversion」)では、このPTEを意図的に操作する。
例えば、フォレンジックツールが読み取るための「ダミーの正常ページ(RW許可、実行不可)」と、CPUが実際にコードを実行するための「悪意あるページ(Read制限、実行許可)」を同一の仮想アドレスに対して二重にマッピング、あるいは動的にPTEのPresentビットやPFNを書き換えて解析を欺く。これにより、アナリストがメモリダンプからシグネチャスキャンをかけても、検知対象のメモリ領域には無害なデータしか見えないという状況を作り出す。

1.3. メモリダンプ妨害(Anti-Dumping)

攻撃者は、インシデントレスポンスチームがメモリダンプを取得しようとすること自体を妨害する。

  • \Device\PhysicalMemory へのアクセス遮断:

ダンプツールが物理メモリにアクセスするために使用するオブジェクトのハンドルを、セキュリティ記述子(DACL)の改ざんによってクローズまたは拒否する。

  • APIフッキングによるダンププロセスのクラッシュ:

一般的なダンプツール(DumpIt, WinPmem, FTK Imagerなど)が呼び出す NtMapViewOfSection などのシステムコールをフックし、ダンプ処理が走った瞬間にシステムをBSoD(Blue Screen of Death)に陥れ、証拠を破壊する。

—

2. 探知と反撃:隠蔽を暴くDFIRアプローチ

これらのアンチフォレンジックに対抗するために、私たちはツールのボタンを押すだけの「オペレーター」から脱却し、カーネルデータ構造の不整合(Inconsistency)を突くアーキテクチャレベルの監査を行わなければならない。

2.1. プロセスリストの不整合分析(Cross-Reference Scan)

DKOMによって ActiveProcessLinks から隠蔽されたプロセスは、以下の別経路のデータ構造をクロスリファレンスすることで確実に炙り出すことができる。

1. PSP(Pool Scan):
メモリ空間全体から _EPROCESS 構造体の「プールタグ(Proc)」と呼ばれるシグネチャを直接スキャンする(Volatilityの psscan)。
2. スレッド構造体の走査:
すべての _ETHREAD 構造体をスキャンし、それらが属する親プロセス(ThreadsProcess ポインタ)を逆引きする。もし、スレッドが実在するにもかかわらず、その親プロセスが ActiveProcessLinks に存在しない場合、それは100%隠蔽されたプロセス(DKOM)である。
3. ハンドルの走査:
各プロセスのオブジェクトテーブル(_HANDLE_TABLE)から、プロセスやスレッドに対するオープンハンドルをスキャンして逆引きする。

2.2. VAD(Virtual Address Descriptor)とPTEの整合性検証

Windowsは、プロセスごとの仮想メモリマップを「VADツリー」と呼ばれる自己バランス二分探索木(_MMVAD)で管理している。
一方、ハードウェア(MMU)が実際に使用するのは前述の「PTE(ページテーブル)」である。

  • VAD上の権限: PAGE_READONLY
  • PTE上の実際の権限: Writable かつ Executable(Dirty BitやNXの操作による)

この二者の間に矛盾がある場合、それはカーネル内に潜むルートキットやエクスプロイトコードがPTEを直接改ざんして、VADの監視(セキュリティ製品のAPI監視など)を回避している決定的な証拠となる。

—

3. 実践:Volatility 3による不整合検知とカスタムスクリプト

ここからは、実際のインシデント解析現場で役立つ具体的な解析手法を示す。

3.1. Volatility 3 を用いたDKOMの自動検知

Volatility 3では、従来の pslist と psscan の結果を比較することで、DKOMによる隠蔽プロセスを容易に特定できる。

# 1. アクティブなプロセスリスト(リンク情報ベース)の取得
python3 vol.py -f suspicious_mem.raw windows.pslist > pslist.txt

# 2. メモリプールスキャン(構造体直接検索)の実行
python3 vol.py -f suspicious_mem.raw windows.psscan > psscan.txt

# 3. 差分の抽出(psscanにのみ存在するPIDを検出する)
# 以下は簡易的な差分確認の例(pslistとpsscanのPID列を比較)
diff -u <(awk '{print $1}' pslist.txt | sort) <(awk '{print $1}' psscan.txt | sort)

3.2. PTEとVADの権限不整合を検出するカスタム検知スクリプト(概念実証)

以下に示すPythonコードは、Volatility 3のAPI設計思想に基づき、特定のプロセス領域においてVADで定義されたメモリ属性(保護フラグ)と、実際のページテーブルエントリ(PTE)の属性の乖離(不整合)をスキャンして、ステルス化されたインジェクションコードを暴くための論理ロジックである。

import logging
from typing import Iterable
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.interfaces import plugins
from volatility3.plugins.windows import pslist

# ログ設定
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class VadPteMismatchScanner(plugins.PluginInterface):
    """VAD(仮想アドレス記述子)とPTE(ページテーブル)の属性不整合を検知するプラグイン"""

    _required_framework_version = (2, 0, 0)

    @classmethod
    def get_requirements(cls):
        return [
            requirements.ModuleRequirement(
                name = 'kernel', 
                description = '対象OSのカーネル構造体を解決するためのモジュール', 
                architectures = ["Intel32", "Intel64"]
            ),
            requirements.PluginRequirement(
                name = 'pslist', 
                plugin = pslist.PsList, 
                version = (2, 0, 0)
            ),
        ]

    def _generator(self, procs: Iterable[interfaces.objects.ObjectInterface]):
        for proc in procs:
            proc_name = proc.ImageFileName.cast("string")
            pid = proc.UniqueProcessId
            
            # プロセスのVADツリーを取得
            try:
                vad_root = proc.VadRoot
                if not vad_root:
                    continue
            except Exception as e:
                logger.debug(f"PID {pid} のVADルート取得に失敗: {e}")
                continue

            # VADノードをトラバース(再帰的またはイテレータによる走査)
            for vad in vad_root.traverse():
                start_vpn = vad.StartVpn
                end_vpn = vad.EndVpn
                
                # VADが定義する保護属性(例: PAGE_EXECUTE_READWRITE等)を取得
                vad_protection = vad.get_protection()
                
                # このVAD領域に対応する物理ページテーブル(PTE)を検査
                # ※ 簡易表現としてプロセスのページディレクトリベース(CR3)からアドレス変換をシミュレート
                for vpn in range(start_vpn, end_vpn + 1):
                    virt_addr = vpn << 12 # 4KBページサイズ
                    
                    # 仮想アドレスからPTE情報を抽出
                    pte_info = proc.environment.get_pte_for_address(proc, virt_addr)
                    if not pte_info or not pte_info.is_present:
                        continue
                    
                    # 不整合の検知ロジック
                    # 例: VAD上は「実行不可(No-Execute)」かつ「書き込み不可」のはずが、
                    # PTE上で「Writable」かつ「Executable(NXビットが0)」になっている場合
                    if "READONLY" in vad_protection and pte_info.is_writable:
                        yield (0, (
                            pid,
                            proc_name,
                            hex(virt_addr),
                            vad_protection,
                            "PTE_WRITABLE_MISMATCH",
                            f"VADではReadOnlyと定義されていますが、PTEでは書き込み可能(Writable)になっています。"
                        ))
                    
                    if "NO_ACCESS" in vad_protection and pte_info.is_present:
                        yield (0, (
                            pid,
                            proc_name,
                            hex(virt_addr),
                            vad_protection,
                            "PTE_SHADOW_PAGE",
                            f"VADではアクセス不可(NO_ACCESS)ですが、物理ページが存在(Present)しています。"
                        ))

    def run(self):
        # 実行中の全プロセスを取得
        kernel = self.context.modules[self.config['kernel']]
        procs = pslist.PsList.list_processes(self.context, kernel.layer_name, kernel.symbol_table_name)
        
        return renderers.TreeGrid([
            ("PID", int),
            ("ProcessName", str),
            ("VirtualAddress", str),
            ("VadProtection", str),
            ("AnomalyType", str),
            ("Description", str)
        ], self._generator(procs))

—

4. アーキテクチャレベルでの防衛:メモリ整合性を担保するガードレイル

攻撃者によるカーネルメモリの改ざんやアンチフォレンジック技術に対抗するためには、OSが起動した「後」の検知だけでなく、システム設計段階でメモリ空間をハードウェアレベルで保護する「ガードレイル」を構築しておく必要がある。

+-------------------------------------------------------------+
|                     User Mode (Ring 3)                      |
|  [フォレンジックツール]          [不審なプロセス/マルウェア]     |
+-------------------------------------------------------------+
|                    Kernel Mode (Ring 0)                     |
|  [Windows Kernel] <------------------+                      |
|                                      | (DKOM/PTE改ざんを防御)  |
+-------------------------------------------------------------+
|                   Secure Kernel (VTL 1)                     |
|  [HVCI / Code Integrity]             |                      |
+-------------------------------------------------------------+
|                    Hypervisor (Ring -1)                     |
|  [Hyper-V] --------------------------+                      |
|  (SLAT / EPT による第2レベルアドレス変換で物理ページを保護)      |
+-------------------------------------------------------------+

4.1. VBS(Virtualization-Based Security)とHVCIの有効化

モダンなWindows環境において、カーネル空間(Ring 0)のメモリ保護における最大の盾となるのがVBSおよびHVCI(Hypervisor-Protected Code Integrity)である。

VBSは、ハードウェアの仮想化支援機能(Intel VT-xやAMD-V)を利用して、通常のOSカーネルよりもさらに特権レベルの高い「仮想化信頼レベル 1(VTL 1: Secure Kernel)」を構築する。

  • W^X(Write or Execute)の強制:

HVCIが有効な環境では、メモリページが同時に「書き込み可能」かつ「実行可能」になることをハードウェアレベルで禁止する。これにより、攻撃者がカーネル空間のPTEを改ざんしてコードをインジェクションしようとしても、ハイパーバイザがメモリの実行を即座にブロックする。

  • カーネル・コントロール・フロー・ガード(kCFG):

間接コール元の正当性をハードウェアレベルで検証し、ROP(Return-Oriented Programming)などのコード再利用攻撃によるカーネルメモリ乗っ取りを防ぐ。

グループポリシーによるVBS/HVCIの強制設定(ADMX/GPO)

エンタープライズ環境において、これらのメモリ保護機構を強制的に有効化するためには、以下のレジストリ設定をGPOで配備する。

Windows Registry Editor Version 5.00

; 仮想化ベースのセキュリティ(VBS)を有効にする
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceGuard]
"EnableVirtualizationBasedSecurity"=dword:00000001

; 安全な起動(Secure Boot)とDMA保護の構成
"RequirePlatformSecurityFeatures"=dword:00000003

; メモリ整合性(HVCI)を有効にする
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity]
"Enabled"=dword:00000001

; 資格情報ガード(Credential Guard)の有効化(追加セキュリティ)
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa]
"LsaCfgFlags"=dword:00000001

4.2. ハイパーバイザレベルでのメモリキャプチャ(アウトオブバンド・フォレンジック)

攻撃者がカーネルドライバを悪用してOS内部からのダンプ取得を妨害する場合、OS(ゲスト)に依存しない「アウトオブバンド(帯域外)」でのメモリキャプチャを行う必要がある。

ハイパーバイザ(VMware ESXi, Hyper-V, KVMなど)を運用している場合、OS内部で動作するエージェント型ダンプツールは一切使用せず、ホスト側から非侵入的(Non-intrusive)にメモリ状態を丸ごとダンプする。

KVM(virsh)による非侵入型メモリダンプ

KVMホスト環境では、ゲストOSに気づかれることなく、ハイパーバイザ側で仮想マシンの全物理メモリをファイルに書き出すことが可能である。

# 稼働中のゲスト仮想マシン(VM_NAME)のメモリを瞬時に一時停止し、ダンプファイルとして出力する
# --bypass-cache を使用してI/Oのオーバーヘッドを最小化
virsh dump --memory-only VM_NAME /var/forensics/dumps/VM_NAME_mem.elf --bypass-cache

この手法で取得されたELF形式のメモリダンプは、ゲストOS内のフックや改ざんの影響を一切受けていない「真実の物理メモリ」であり、Volatility 3等で完璧にパースすることができる。

—

5. 結論:泥臭いパズルを解く覚悟

アンチフォレンジック技術は、一見すると魔法のように思えるかもしれない。しかし、その正体は「OS仕様の隙を突いた、辻褄の合わないデータのつぎはぎ」に過ぎない。

プロセスを隠すためにリンクリストを切り離せば、スレッドスケジューラのキューやハンドルテーブルとの間に不整合が生じる。
PTEを書き換えて解析ツールのスキャンを回避しようとすれば、VADツリーの定義とハードウェアのMMUが参照するページ属性の間に矛盾が生じる。

私たちDFIRアナリストが持つべきなのは、ツールを実行して吐き出された結果を鵜呑みにする姿勢ではない。提示されたデータの裏にある「システム構造の不整合」を執拗に追い求める、泥臭くも知的な執念である。

攻撃者がメモリの闇に潜もうとするならば、私たちはその闇を照らす低レイヤの光(ハードウェア、ハイパーバイザ、そしてカーネル構造の真の理解)を持ち、常にその一歩先で待ち構えていなければならない。

コメント

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