【実務・中級編】 メモリフォレンジックにおけるカーネルオブジェクトの不整合解析 – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

隠されたプロセスを暴け:DKOMとカーネルオブジェクト不整合解析の現場

おい、ちょっと手を止めてくれ。
先週、某クライアントのインシデント対応で、俺たちは頭を抱えた。EDR(Endpoint Detection and Response)のコンソールは「緑(正常)」。タスクマネージャーを見ても、怪しいプロセスは一切動いていない。しかし、ネットワークの出口では、夜な夜な特定のC2(コマンド&コントロール)サーバーへ暗号化されたトラフィックが吐き出されている。

「見えない敵」にどう立ち向かうか?
ここで登場するのが、今回のテーマであるDKOM(Direct Kernel Object Manipulation:直接カーネルオブジェクト操作)と、それに伴うカーネルオブジェクトの不整合解析だ。

教科書的なセキュリティガイドラインには、「タスクマネージャーを確認しましょう」「EDRを入れましょう」としか書いてない。だが、プロのインシデントレスポンスの現場では、そんなお花畑の対策はとうの昔に突破されている。攻撃者は、OSのAPIを経由せずにメモリ上のデータ構造を直接書き換え、プロセスを「視覚的」かつ「API的」に消し去るのだ。

今日は、その後輩であるお前たちに、メモリフォレンジックの極限とも言えるこの領域の泥臭い真実と、それを看破するための実務的なアプローチを叩き込む。

—

1. DKOMのメカニズム:なぜプロセスは「消える」のか?

通常のOS(Windowsを例にとろう)では、プロセス管理はカーネル内の二重リンクリスト(Doubly Linked List)、いわゆる ActiveProcessLinks(EPROCESS 構造体の中にある)を辿ることで行われている。タスクマネージャーや tasklist コマンド、さらには一般的な監視ツールの大半も、このリストを順番に参照して「今、何が動いているか」を表示している。

攻撃者はどうするか?
彼らはカーネル空間への書き込み権限(ローカル特権昇格や脆弱性ドライバ経由)を手に入れると、メモリ上の特定の EPROCESS 構造体を指すポインタを書き換え、ターゲットのプロセスをこのリンクリストから「外し」てしまうのだ。

[正常な状態]
プロセスA <---> プロセスB (悪意のプロセス) <---> プロセスC

[DKOM実行後]
プロセスA <-----------------------------------> プロセスC
                      ^
        (プロセスBはリストから外されるが、メモリ上に居座り続ける)

リストから外されたプロセスBは、OSのAPI(EnumProcesses など)からは完全に姿を消す。しかし、メモリ上にはコードもデータも存在し続け、CPU時間を消費し、裏で悪事を働き続ける。これがDKOMによるプロセス隠蔽の正体だ。

—

2. 現場の盲点:「不整合」を暴くメモリフォレンジック

では、APIから見えないプロセスを、どうやって見つけ出すのか?
答えはシンプルだ。「OSが嘘をついても、物理メモリやカーネルの別構造体は真実を知っている」ということ。

DKOMで ActiveProcessLinks から外されたプロセスであっても、カーネル内には別の管理構造体が生き残っていることが多い。例えば以下のような痕跡だ。

1. ハンドルテーブル(Object Table): プロセスがオープンしているファイルやレジストリ、ミューテックスの参照。
2. 句ハンドル(Session ID / UniqueProcessId): プロセスIDそのものは別のテーブルやキャッシュに残っている。
3. プールタグスキャン(Pool Tag Scanning): カーネルメモリの割り当てプールを総当たりし、Proc などのプロセス特有のプールタグ(Pool Tag)を探す。

Volatilityなどのメモリフォレンジックツールを用いた解析では、単一のリストを信じるのではなく、複数のリストや構造体の間で「数詞の不整合(Consistency Check)」を探す。
例えば、pslist(リンクリストベース)では出てこないのに、psscan(プールタグスキャンベース)ではヒットするプロセスがある場合、それは100% DKOMによる隠蔽工作を疑うべき決定的な証拠となる。

—

3. 【実務的アプローチ】カーネル整合性を担保する防御と検知の実装

インシデントレスポンスの現場だけでなく、システムの設計段階からこうしたメモリ改ざんや不正なカーネル空間へのアクセスを防ぐ、あるいは検知する仕組みを組み込む必要がある。

ここでは、単なる監視スクリプトを超え、OSやシステムの整合性をプログラムレベルで担保するためのPythonによる検証スクリプトのサンプルを提示する。これは、本番環境の定期的なヘルスチェックや、EDRの補完としてホストのプロセスリストの整合性を自前で検証するためのロジックだ。

Pythonによるプロセスリスト整合性検証スクリプト

以下のコードは、Windows環境において、標準APIから取得できるプロセスIDのリストと、カーネルレベルの低レイヤー情報を比較し、乖離(不整合)がないかをチェックする概念実証(PoC)に近い実用スクリプトだ。

import ctypes
import sys
from typing import Set

# Windows APIの定数や構造体を定義( ctypes を使用)
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010

def get_api_process_ids() -> Set[int]:
    """
    標準的なWindows API (EnumProcesses) を使用して、
    OSから「見える」プロセスIDのセットを取得する。
    """
    psapi = ctypes.WinDLL('psapi.dll')
    cb = 1024 * 4
    while True:
        pids = ctypes.c_ulong * (cb // 4)
        bytes_returned = ctypes.c_ulong(0)
        if psapi.EnumProcesses(pids, cb, ctypes.byref(bytes_returned)):
            if bytes_returned.value < cb:
                break
            cb *= 2
        else:
            raise RuntimeError("EnumProcesses の実行に失敗しました。")
        
    return {pids[i] for i in range(bytes_returned.value // 4)}

def get_alternative_process_ids() -> Set[int]:
    """
    [セキュリティチーフの解説]
    本来、真のカーネル不整合検知にはWMIや未公開のカーネルAPI、
    あるいはサードパーティ製ドライバ経由のプールスキャン結果を使用する。
    ここではプレースホルダーとして、システム上の別レイヤー(例: 監査ログや別手段の列挙)
    との比較ロジックの枠組みを示す。
    """
    # 実運用ではここでNTDLLの未公開API(NtQuerySystemInformationなど)や
    # 独自開発のカーネルドライバからのデータを取得して比較する。
    # 攻撃者はNtQuerySystemInformationすらフックすることがあるため、
    # 最終的には物理メモリの直接読み込み(直叩き)が必要になる。
    
    # 模擬的な代替ソースからの取得データ(例として空セットを返す)
    kernel_or_raw_pids: Set[int] = set() 
    return kernel_or_raw_pids

def audit_process_integrity() -> None:
    """
    標準API側のリストと、カーネル/ロウレベル側のリストを突合し、
    DKOMによる隠蔽(APIに現れないプロセス)を検知する。
    """
    try:
        visible_pids = get_api_process_ids()
        print(f"[*] API経由で検出されたプロセス数: {len(visible_pids)}")
        
        # 代替手段(低レイヤー)で取得したPIDリスト
        # ※実際のEDRやフォレンジックツールはここを生メモリ解析結果と比較する
        raw_pids = get_alternative_process_ids()
        
        # 乖離の検知ロジック
        # 隠蔽されている場合、raw_pids(またはメモリプールから直接引いたID)に存在し、
        # visible_pidsに存在しないIDが現れる。
        hidden_pids = raw_pids - visible_pids
        
        if hidden_pids:
            print(f"[!] 警告: DKOMによるプロセスの隠蔽(不整合)を検知しました! PID: {hidden_pids}")
            # ここでインシデントアラートを発報し、自動的にメモリダンプを取得する処理を記述する
        else:
            print("[+] プロセスリストの大きな不整合は検出されませんでした。")
            
    except Exception as e:
        print(f"[-] エラーが発生しました: {e}", file=sys.stderr)

if __name__ == "__main__":
    if sys.platform != "win32":
        print("[-] このスクリプトはWindows環境専用です。", file=sys.stderr)
        sys.exit(1)
        
    print("--- プロセス整合性監査モジュールを開始します ---")
    audit_process_integrity()

—

4. チーフエンジニアからの実践的な教訓

お前たちが今後、インフラの設計やセキュアなアプリケーションの基盤構築に関わる上で、このDKOMの事例から何を学ぶべきか。

1. 単一の監視レイヤーに依存するな
「ログが出ているから安全」「EDRが検知していないから大丈夫」という考え方は今すぐ捨てろ。攻撃者はセキュリティ製品が監視しているレイヤー(ユーザーモード、あるいは一般的なAPI)をいかにバイパスするかを常に考えている。重要なシステムでは、カーネルの整合性保護機能(HVCI: ハイパーバイザー保護コード整合性など)を確実に有効化し、不正なドライバのロードを防ぐこと。
2. 「見えないこと」を前提にアーキテクチャを組め
Webアプリケーションの脆弱性から侵入され、カーネル権限まで奪われた時点で、OS上の情報は完全に信用できなくなる。重要なデータ処理や監査ログの送信は、ホストOSの信頼性に依存しない、外部のセキュアなSIEMやイミュータブル(変更不可)なストレージへリアルタイムにストリーミングする設計にしろ。

セキュリティは、ツールを導入して終わりではない。「システムはいつか破られる。その時、嘘をついているレイヤーはどこか?」を疑い続ける執念こそが、お前たちを本物のエンジニアにする。

さて、理論はここまでだ。次はラボ環境で実際にメモリイメージをダンプし、Volatilityを使って pslist と psscan の差分を自分の目で確認してみろ。手を動かした人間だけが、真のフォレンジックを語る資格を持つ。

コメント

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