【テクニカル・上級編】 メモリ上の隠蔽されたプロセスの特定 – インシデントレスポンス & フォレンジック(DFIR)防御ガイド

隠されたプロセスを暴く:DKOMの裏側とメモリフォレンジックの実践

インシデントレスポンスの現場において、タスクマネージャーや tasklist コマンド、さらにはサードパーティのEDRですら「何も見えない」と言っている端末で、不審なネットワークコネクションだけが外部のC2サーバーと確立されている。この瞬間、アナリストの背筋には冷たいものが走る。プロセスリストからの「消去」。これこそが、カーネルの深層を掌握した攻撃者が好んで使う、Direct Kernel Object Manipulation(DKOM)の現実だ。

APIや通常のOSクエリが返すリストをいくら舐め回しても無駄である。なぜなら、OSの正当なAPIそのものが改竄されているか、あるいはリストの根幹であるカーネル構造体のリンクが意図的に切断されているからだ。表面的なログ監視やプロセス監視に頼り切ったセキュリティアーキテクチャは、このレイヤーの前では無力に等しい。

今回は、OSの視覚から完全に姿を消したプロセスを、物理メモリ(あるいは生メモリイメージ)の構造体解析によっていかにして引きずり出すか、その低レイヤのメカニズムと実践的なハンズオンを解説する。

—

1. なぜ「隠蔽」が可能なのか:DKOMのメカニズム

Windowsカーネル(NTOSKRNL)において、実行中のプロセスは EPROCESS と呼ばれる巨大な構造体として管理されている。この EPROCESS の中には、プロセスのトークン、ハンドルテーブル、仮想メモリのルート(DirectoryTableBase)、そしてプロセス間の親子関係やリスト管理を行うための ActiveProcessLinks(LIST_ENTRY 型)が含まれている。

通常、タスクマネージャーなどがプロセスの一覧を取得する際、カーネルはこの ActiveProcessLinks の両方向リンクリスト(Doubly Linked List)を辿り、順次プロセス情報を収集する。

[System EPROCESS] <---> [Smss.exe EPROCESS] <---> [Csrss.exe EPROCESS] ...

悪意あるカーネルドライバやルートキットは、このリスト構造を書き換える。例えば、特定プロセスの ActiveProcessLinks の Flink(Forward Link)と Blink(Backward Link)のポインタを付け替え、自身の EPROCESS をこの環状リストから切り離す(Unlinking)。

切断前: [A] <---> [Target] <---> [B]
切断後: [A] =====================> [B] (Targetはリストから孤立)

OSのAPIはリストの鎖を辿ってプロセスを探すため、切り離された Target は存在しないものとして扱われる。しかし、メモリ上に EPROCESS の実体が残っている限り、あるいはメモリのプール(Pool)領域をくまなくスキャンすれば、その痕跡は必ず発見できる。これがメモリフォレンジックの存在意義である。

—

2. 現場でのアプローチ:Volatility 3 を用いたプロセス抽出

現代のインシデントレスポンスにおいて、メモリイメージからのプロセス再構築は自動化ツールなしには語れない。ここでは、メモリフォレンジックのデファクトスタンダードである Volatility 3 を用い、隠蔽されたプロセスを炙り出すアプローチを検証する。

通常のプロセス一覧を取得するプラグイン windows.pslist は、先ほど解説した ActiveProcessLinks のリンクリストを辿るため、DKOMによって隠蔽されたプロセスを見逃す。そこで、カーネル内の別の構造体を逆引きするプラグインや、メモリ内の特徴的なシグネチャをスキャンする手法が必要になる。

windows.psscan によるプールタグスキャン

windows.psscan プラグインは、リンクリストを一切信用しない。代わりに、物理メモリ上の特定のオフセットや、カーネルプール(Pool Allocation)に刻まれる EPROCESS のプールタグ(Windows 10/11では通常 Proc や Thre など)を総当たりでスキャンし、EPROCESS 構造体のシグネチャに合致するメモリアドレスを強制的に列挙する。

以下のコマンドは、取得したメモリイメージから隠蔽プロセスを含むすべての EPROCESS 構造体をスキャンし、テーブルとして出力する例である。

# ボラティリティ3を使用して、メモリイメージからEPROCESS構造体をプールスキャンする
python3 vol.py -f memdump.raw windows.psscan

このコマンドの出力結果において、windows.pslist には現れなかったものの、windows.psscan にのみ存在するプロセスID(PID)や名前が見つかった場合、それは高確率でDKOMやその他の手法によって隠蔽されたプロセス、あるいは終了済みのプロセスの残骸(ゾンビ状態のプール領域)である。

—

3. 実践:Pythonによるカーネルメモリ構造体(EPROCESS)のオフセット解析と検証

商用EDRや特殊なカスタムフォレンジックツールを自製する場合、あるいはVolaitilityのカスタムプラグインを記述する際には、対象とするOSバージョンの EPROCESS 内のオフセット(Offset)を正確に把握しておく必要がある。

以下に、Pythonを用いてダンプされたメモリ領域から特定のシグネチャや EPROCESS の主要メンバー(PID、ImageFileName、ActiveProcessLinks)をパースするための概念的な解析スクリプトを示す。

import struct
import sys

def parse_eprocess_structure(memory_dump_path, suspected_offset):
    """
    指定されたオフセットからEPROCESS構造体の主要フィールドを読み取る関数
    ※注意: 実際の解析ではWindowsのバージョンごとの構造体オフセットの差異を考慮する必要があります。
    """
    # Windows 10 x64 における一般的なオフセットの例 (バージョンにより変動します)
    OFFSET_UNIQUE_PROCESS_ID = 0x440  # UniqueProcessId (PID) のオフセット
    OFFSET_IMAGE_FILE_NAME   = 0x450  # ImageFileName (プロセス名) のオフセット
    OFFSET_ACTIVE_LINKS      = 0x448  # ActiveProcessLinks のオフセット
    
    try:
        with open(memory_dump_path, "rb") as f:
        # 指定されたカーネルメモリアドレス(オフセット)までシーク
            f.seek(suspected_offset)
            # EPROCESSの妥当性を確認するため、数バイトを読み込む
            raw_data = f.read(1024)
            
            if len(raw_data) < 1024:
                print("[-] エラー: 指定されたオフセットから十分なデータを読み込めませんでした。")
                return

            # PIDの抽出 (8バイト整数)
            pid = struct.unpack_from("<Q", raw_data, OFFSET_UNIQUE_PROCESS_ID)[0]
            
            # プロセス名の抽出 (15バイトのヌル終端文字列)
            image_file_name = raw_data[OFFSET_IMAGE_FILE_NAME : OFFSET_IMAGE_FILE_NAME + 15]
            # デコード処理(バイナリから文字列へ)
            process_name = image_file_name.split(b'\x00')[0].decode('utf-8', errors='ignore')
            
            # ActiveProcessLinks の Flink と Blink を取得
            flink = struct.unpack_from("<Q", raw_data, OFFSET_ACTIVE_LINKS)[0]
            blink = struct.unpack_from("<Q", raw_data, OFFSET_ACTIVE_LINKS + 8)[0]

            print(f"[+] 構造体解析結果 @ Offset: 0x{suspected_offset:X}")
            print(f"    - プロセス名 (ImageFileName) : {process_name}")
            print(f"    - プロセスID (PID)           : {pid}")
            print(f"    - ActiveProcessLinks (Flink) : 0x{flink:X}")
            print(f"    - ActiveProcessLinks (Blink) : 0x{blink:X}")
            
    except IOError as e:
        print(f"[-] ファイルの読み込みに失敗しました: {e}")

if __name__ == "__main__":
    # 使用例のシミュレーション(実運用では正確なカーネルオフセットを指定)
    print("=== EPROCESS カーネル構造体解析シミュレータ ===")
    # parse_eprocess_structure("memdump.raw", 0xfffffa8001234000)

このコードが示すように、メモリ上の生データから直接オフセットを計算して値を引き抜くアプローチをとれば、OSが提供するAPIや管理テーブルがどれほど改ざんされていようとも、物理的なデータそのものを偽装することは極めて困難であるため、真実を暴くことができる。

—

4. チーフホワイトハッカーの視点:防御側が取るべき次の一手

DKOMをはじめとするメモリ上の隠蔽手法に対抗するためには、従来の「OSからの情報を受け取るだけ」のセキュリティ対策から脱却しなければならない。セキュリティアーキテクトやテックリードが設計すべき防衛層の要諦は以下の通りである。

1. ハイパーバイザーベースの仮想化(VBS / HVCI)の強制
カーネルメモリの整合性をハイパーバイザー層から常時監視し、不正なドライバロードやカーネル領域の改ざん(PatchGuard / Kernel Patch Protectionのバイパス試行)をハードウェアレベルで検知・ブロックする。
2. EDRにおけるクロスチェックの義務化
EDRエージェントは、psapi や通常のWin32 API依存ではなく、カーネルコールバック(ObRegisterCallbacks や ZwQuerySystemInformation の独自のフック、さらには定期的なプールスキャン)を実装し、pslist と psscan の結果の差異を常に監視・アラート化する仕組みを持たなければならない。
3. メモリインテグリティの継続的監査
クラウドワークロードや重要インフラにおいては、ライブメモリのインスペクション(Live Memory Forensics)を定期的に自動実行し、認知されていない EPROCESS や不審なハンドルテーブルの割り当てを検知するパイプラインを構築することが、高度な持続的脅威(APT)を早期に無力化する唯一の現実解となる。

「見えないもの」を「見えない」と諦めた瞬間から、インシデントレスポンスの敗北が始まる。OSの表層に惑わされず、メモリの深層に刻まれたビットの真実を読み解く能力こそが、現代のセキュリティエンジニアに求められている。

コメント

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