【テクニカル・上級編】 メモリ上のカーネルオブジェクト(Mutex, Event)の解析によるマルウェアの特定 – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

プロセスツリーの改ざん、メモリ内インジェクション、親プロセスの偽装(PPID Spoofing)――高度標的型攻撃(APT)や洗練されたランサムウェアオペレーターは、EDRやエンドポイントログを欺く術を熟知しています。しかし、どれほど巧妙にsvchost.exeやRuntimeBroker.exeになりすまそうとも、マルウェアがWindows上で実行される「単一のバイナリ」である以上、回避が極めて困難な低レイヤの痕跡が存在します。

それが、Windowsカーネルのオブジェクトマネージャ(Object Manager)によって管理される同期オブジェクト(Mutex / Event)です。

マルウェア開発者にとって、多重感染によるコマンド&コントロール(C2)セッションの重複や、暗号化処理の衝突、同一ホスト上での競合クラッシュは致命的です。そのため、彼らは初期化ルーチンにおいてCreateMutexWやCreateEventWを呼び出さざるを得ません。本稿では、フォレンジックエンジニアおよびセキュリティアーキテクトに向けて、Windowsカーネルにおける同期オブジェクトの内部構造、プール走査とハンドル走査の深淵、そしてVolatility 3を用いた高精度な特定手法とアンチフォレンジックの突破口を解説します。

—

1. カーネルの深淵:Windows Object ManagerとMutantの低レイヤ構造

Windowsのカーネル内部において、ユーザーモードで呼ばれる「Mutex」は、実際には「Mutant」オブジェクトとして実装されています。すべてのカーネルオブジェクトは、メタデータを保持する共通構造体 OBJECT_HEADER によってラップされ、カーネルメモリ空間(プール)に配置されます。

OBJECT_HEADER と _KMUTANT の関係性

ユーザー空間から CreateMutexExW(NULL, L"Global\\MaliciousMutex", ...) が呼ばれると、カーネル空間では _KMUTANT 構造体が割り当てられ、その直前に _OBJECT_HEADER が配置されます。

+------------------------------------+
| _OBJECT_HEADER                     |  <- オブジェクト名、ハンドル数、参照カウント
+------------------------------------+
| _KMUTANT (または _KEVENT)           |  <- Dispatcher Header, シグナル状態, 所有スレッド
+------------------------------------+

64bitアーキテクチャにおける OBJECT_HEADER の主要フィールドは以下の通りです。

// 簡略化した OBJECT_HEADER 構造(カーネルデバッガ dt nt!_OBJECT_HEADER の出力に基づく)
typedef struct _OBJECT_HEADER {
    LONG_PTR PointerCount;               // 参照カウント
    union {
        LONG_PTR HandleCount;            // ハンドル数
        PVOID SingleHandleEntry;
    };
    POBJECT_TYPE Type;                   // オブジェクト型へのポインタ(Mutant型など)
    UCHAR NameInfoOffset;                // OBJECT_HEADER_NAME_INFO へのオフセット
    UCHAR HandleInfoOffset;
    UCHAR QuotaInfoOffset;
    UCHAR Flags;
    // ... ボディ部(_KMUTANT)へ続く
} OBJECT_HEADER, *POBJECT_HEADER;

重要なのは、NameInfoOffset です。名前付きオブジェクト(Named Object)の場合、OBJECT_HEADER の手前の負のオフセット領域に _OBJECT_HEADER_NAME_INFO が存在します。ここにオブジェクトの完全修飾パス(例: \BaseNamedObjects\MaliciousMutex)が UNICODE_STRING として格納されています。

ハンドルテーブル走査 vs プールスキャニング

メモリフォレンジックにおいて同期オブジェクトを探索するアプローチには、大きく分けて2つの系統が存在します。

1. ハンドルテーブル走査(Handle Walking):
各プロセスの EPROCESS から ObjectTable (_HANDLE_TABLE) を辿り、そのプロセスが明示的に開いているハンドルを列挙する手法。

  • 長所: どのプロセス(PID)がそのMutexを保持しているかが100%確実に紐付く。
  • 短所: 攻撃者が CloseHandle を呼んでハンドルを閉じた後も、他のスレッドが参照を維持していたり、アンリンク(DKOM)されているとハンドルテーブルからは消失する。

2. プールタギング走査(Pool Scanning):
カーネルプール全体をスキャンし、オブジェクト割り当て時に付与される特定のプールタグ(4バイト文字列)をシグネチャとして探索する手法。

  • Mutantのプールタグ: Mutn または Mute
  • Eventのプールタグ: Eve または Even
  • 長所: プロセスが終了していても、メモリが上書き(ゼロクリア)されるまでは残存(デッド)オブジェクトとして回収可能。
  • 短所: 偽陽性(スプーフィングされたタグ)のリスクがあり、所有プロセスの特定に追加のカーネルポインタ解析が必要。

—

2. 実戦:Volatility 3によるオブジェクト抽出の深層

一般的なアナリストは単に windows.handles プラグインを実行してテキスト検索を行いますが、真の脅威ハンティングにおいては、内部構造を直接パースするカスタムアプローチが不可欠です。

以下は、Volatility 3 のフレームワークを直接利用して、プロセス空間からMutantオブジェクトを特定し、その名称と所有プロセス(PID/プロセス名)を正確にマッピングする解析スクリプトの実装例です。

# mutant_hunter.py - Volatility 3 Custom Mutex Extractor
import logging
from typing import Iterable
from volatility3.framework import interfaces, renderers
from volatility3.framework.configuration import requirements
from volatility3.framework.objects import utility
from volatility3.plugins.windows import pslist

vollog = logging.getLogger(__name__)

class MutantHunter(interfaces.plugins.PluginInterface):
    """プロセスに紐づくすべての名前付きMutant(Mutex)をディープ走査するプラグイン"""

    _required_framework_version = (2, 0, 0)
    
    @classmethod
    def get_requirements(cls):
        return [
            requirements.ModuleRequirement(
                name="kernel",
                description="Windows kernel module",
                architectures=["Intel32", "Intel64"],
            ),
            requirements.PluginRequirement(
                name="pslist", plugin=pslist.PsList, version=(2, 0, 0)
            ),
        ]

    def _generator(self, procs):
        kernel = self.context.modules[self.config["kernel"]]
        
        for proc in procs:
            process_name = utility.array_to_string(proc.ImageFileName)
            pid = proc.UniqueProcessId
            
            # オブジェクトテーブルが存在しないプロセス(システムスレッド等)をスキップ
            if not proc.ObjectTable:
                continue
                
            # プロセスのハンドルテーブルを列挙
            for entry in proc.ObjectTable.handles():
                try:
                    obj = entry.dereference()
                    # オブジェクトヘッダを取得
                    obj_header = kernel.object(
                        object_type="_OBJECT_HEADER",
                        offset=obj.vol.offset - kernel.get_type("_OBJECT_HEADER").size
                    )
                    
                    # オブジェクトタイプの名前を確認(Mutantのみをターゲット)
                    type_name = obj_header.get_object_type_name()
                    if type_name != "Mutant":
                        continue
                    
                    # 名前情報の取得
                    mutant_name = "Unnamed"
                    if obj_header.NameInfoOffset != 0:
                        name_info = kernel.object(
                            object_type="_OBJECT_HEADER_NAME_INFO",
                            offset=obj_header.vol.offset - obj_header.NameInfoOffset
                        )
                        mutant_name = name_info.Name.String
                        
                    yield (0, (pid, process_name, hex(entry.HandleValue), mutant_name))
                    
                except Exception:
                    continue

    def run(self):
        kernel = self.context.modules[self.config["kernel"]]
        return renderers.TreeGrid(
            [
                ("PID", int),
                ("Process", str),
                ("Handle", str),
                ("Mutant Name", str),
            ],
            self._generator(pslist.PsList.list_processes(self.context, kernel.layer_name, kernel.symbol_table_name))
        )

このアプローチにより、GUIツールや標準プラグインの出力パースに依存せず、オートメーションパイプラインに低レイヤの正確なMutant情報を直接取り込むことが可能となります。

—

3. 攻撃者のアンチフォレンジック戦術とその突破ロジック

アナリストがMutex名に注目していることは、マルウェア開発者側も理解しています。そのため、近年の攻撃キャンペーンではMutexの追跡を困難にする設計が組み込まれています。

パターンA: ホスト固有値を用いた動的DGA型Mutex

静的なハードコードされたMutex名(例: Global\\MyTrojanMutex)は一瞬で検知されるため、攻撃者はホスト環境のシード値からハッシュを動的に生成します。

  • シード値の例:
  • VolumeSerialNumber (Cドライブのシリアル)
  • MachineGuid (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography)
  • コンピュータ名 + MACアドレス
  • アルゴリズム例:

MD5(ComputerName + VolumeID) の先頭16進数文字列をフォーマットして Global\\{8F4C2A1E-...} とする手法。

【突破のロジック】

メモリダンプからレジストリハイブ(nt!_CMHIVE)を展開し、該当マシンの MachineGuid やボリュームシリアルを抽出した上で、マルウェアの逆コンパイルコードに存在するハッシュ関数(MurmurHash3、MD5、CRC32など)をPythonで再現・事前計算(レインボー化)し、メモリ上のMutantと衝突検証(Collision Verification)を行います。

パターンB: 名前のないMutex(Unnamed Mutex)とハンドルの複製

最もステルス性の高い実装は、名前付きMutantを使わない手法です。

// 名前を持たないMutexを作成
HANDLE hMutex = CreateMutexW(NULL, FALSE, NULL);

// 自身から派生する子プロセスに対してハンドルを継承(bInheritHandles = TRUE)
// あるいは DuplicateHandle API でターゲットプロセスにハンドルを直接挿入する
DuplicateHandle(
    GetCurrentProcess(),
    hMutex,
    hTargetProcess,
    &hTargetMutex,
    0,
    TRUE,
    DUPLICATE_SAME_ACCESS
);

名前空間(\BaseNamedObjects)に登録されないため、文字列ベースのIOC(Indicators of Compromise)スキャンを完全に回避します。

【突破のロジック】

名前が存在しない場合、Mutantオブジェクトのカーネルアドレス(Pointer)そのものが識別子になります。同一の _KMUTANT アドレスを複数のプロセスがハンドルテーブルで共有している関係性をグラフ理論に基づきクラスタリングします。

[svchost.exe (PID: 1420)] --Handle: 0x48--> \
                                            [KMUTANT: 0xFFFFB80A2C901080]
[unknown.exe (PID: 5892)] --Handle: 0x24--> /

正規のプロセス間通信(IPC)で利用されるシステム標準のMutexを除外(ベースライン化)すれば、無名Mutexを共有するインジェクションプロセス群のツリーが白日の下に晒されます。

—

4. マルウェアファミリー同定のシグネチャ設計

抽出したMutex名は、単なる侵害の痕跡(IOC)にとどまらず、マルウェアファミリーや脅威アクター(Threat Actor)を特定する決定打となります。

以下の表は、実際のインシデントレスポンスで観測される特徴的なMutex命名パターンのマッピングです。

| マルウェア / 脅威グループ | Mutex / Event命名規則の特徴 | 正規表現 / 例 |
| :— | :— | :— |
| QakBot (QBot) | ランダムに見える数字列、または特定の計算ロジックによる英数字 | ^[A-Fa-f0-9]{8,12}$ |
| Cobalt Strike | デフォルトMalleable C2プロファイルに基づくEvent/Mutex | MSSE-[0-9]{4}-server 等(変更可能だがオペレーターの失念が頻発) |
| Emotet | ユーザープロファイルやシステム情報から派生するGUID形式 | Global\\{[A-F0-9]{8}-[A-F0-9]{4}-...} |
| LockBit 3.0 | コマンドライン引数 -k で渡されるパスワードベースのハッシュ | アクター固有のハッシュ生成ロジックに依存 |
| PlugX / Korplug | ハードコードされた特徴的な識別文字列 | Global\\X... や Global\\G... で始まる特定の固定文字列 |

メモリ空間に対するYARAハンティングの最適化

抽出したMutexのパターンを元に、rawメモリダンプあるいはページファイル(pagefile.sys)から、該当オブジェクトを生成したコード断片をハンティングするためのYARAルール設計です。

rule Hunt_Suspicious_Kernel_Named_Mutex {
    meta:
        author = "Senior Incident Handler"
        description = "Detects in-memory constructs of BaseNamedObjects Mutex creation commonly abused by adversaries"
        tier = "Deep Memory Forensics"
    
    strings:
        // "\BaseNamedObjects\" のUTF-16LE表現
        $bno_wide = "\\BaseNamedObjects\\" wide
        
        // Cobalt Strikeのデフォルトプロファイルで頻繁に見落とされる名前パターンの検知
        $cs_default_1 = "MSSE-" wide
        $cs_default_2 = "status_" wide
        
        // DGA型Mutexの一般的なプレフィックス(Global\ または Local\)
        $prefix_global = "Global\\" wide
        $prefix_local  = "Local\\" wide

    condition:
        // メモリダンプ全体の走査を想定
        any of ($cs_default_*) and ($prefix_global or $prefix_local)
        or (
            $bno_wide and (
                // 疑わしいフォーマット文字列(printfスタイルのGUID生成など)
                pe.imports("kernel32.dll", "CreateMutexW") or 
                pe.imports("kernel32.dll", "OpenMutexW")
            )
        )
}

—

5. まとめ:カーネルオブジェクトが語るインシデントの真実

ファイルは消去され、イベントログはクリアされ、プロセスメモリのテキスト領域は巧妙に上書き(Trampoline / Module Stomping)される時代です。しかし、攻撃者がOSのAPIを借りてリソースを制御し、競合を避けて活動を継続しようとする限り、Windowsカーネルのオブジェクトマネージャを騙し切ることはできません。

1. プロセスツリーの盲信を捨てる: プロセス名や親子関係が正常に見えても、そのハンドルテーブルが指し示すカーネルオブジェクトの「名前」と「リンク構造」を暴く。
2. ハンドルとプールの両面走査: ハンドルテーブルが偽装・改ざんされている可能性を考慮し、プールタグ(Mutn / Eve)によるスキャニングを併用する。
3. 動的生成Mutexの逆算: ホスト情報のハッシュから生成されるMutexは、逆アセンブルしたロジックとメモリ内のホスト情報を組み合わせることで、確定的なマルウェア同定ロジックへと昇華させる。

これら低レイヤのメモリ挙動を体系的に理解し、インシデント対応の現場でスクリプトレベルで自動化・深層解析できる能力こそが、洗練されたサイバー脅威に対抗するための防衛線となります。

コメント

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