ラック・セキュリティごった煮ブログ

セキュリティエンジニアがエンジニアの方に向けて、 セキュリティやIT技術に関する情報を発信していくアカウントです。

株式会社ラックのセキュリティエンジニアが、 エンジニアの方向けにセキュリティやIT技術に関する情報を発信するブログです。(編集:株式会社ラック・デジタルペンテスト部)
/ セキュリティブログ / セキュリティエンジニア /
当ウェブサイトをご利用の際には、こちらの「サイトのご利用条件」をご確認ください。

デジタルペンテスト部提供サービス:ペネトレーションテスト

カーネルモードから追跡するWindows OSのプロセス情報

デジタルペンテスト部の北原です。 今回の記事では、Windowsカーネルに実装されているプロセス追跡機能と、その用途について解説します。

Windowsカーネルには、プロセスを追跡するためのいくつかの機能が実装されています。 これらの機能はデバッグやトラブルシュートなどのために開発されましたが、近年はサイバー攻撃やゲームでのチート行為などの検知に活用されています。 本記事では、Windowsに実装されているいくつかのプロセス通知機能のうち、カーネルモードで動作する機能を中心に解説します。

Process Notification Callback

コールバック関数とカーネルAPI

Windows OSには、新たなプロセスが生成または終了された際に、そのプロセスの情報を収集するために実行するコールバック関数を登録する機能が実装されています。 登録したコールバック関数は、プロセスの生成直後、プロセスの初期化処理が始まる前に実行されます。 コールバック関数を活用すれば、生成または終了されたプロセス情報を収集するのみではなく、生成されるプロセスの状態を開始前に変更できます。

この機能はWindowsカーネルに実装されており、独自に定義したコールバック関数を登録するにはカーネルドライバを作成する必要があります。 コールバック関数を登録するためのカーネルAPIとして、以下の3つがWindowsカーネルに実装されています。

登録したコールバック関数の登録解除にも、登録に用いたものと同じカーネルAPIが用いられます。 例えば、PsSetCreateProcessNotifyRoutine APIの関数シグネチャは以下の定義に従います。

NTSTATUS PsSetCreateProcessNotifyRoutine(
  [in] PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine,
  [in] BOOLEAN                        Remove
);

第1パラメータのNotifyRoutineには登録または登録解除したいコールバック関数の関数ポインタを指定し、第2パラメータのRemoveには登録するか登録解除するかを示すブール値を設定します。 RemoveFALSEに設定すればコールバック関数が登録され、TRUEに設定すればコールバック関数の登録が解除されます。 NotifyRoutineに指定するコールバック関数ポインタは、以下のPCREATE_PROCESS_NOTIFY_ROUTINEという関数シグネチャに一致させる必要があります。

PCREATE_PROCESS_NOTIFY_ROUTINE PcreateProcessNotifyRoutine;

void PcreateProcessNotifyRoutine(
  [in] HANDLE ParentId,
  [in] HANDLE ProcessId,
  [in] BOOLEAN Create
);

このPcreateProcessNotifyRoutine関数の内容は、カーネルドライバの開発者が自身の手で実装する必要があります。 PsSetCreateProcessNotifyRoutine APIでコールバック関数を登録すると、プロセスの作成または終了の際に、Windowsカーネルから以下の情報が設定された状態でコールバック関数が呼び出されます。

  • ParentId - 作成または終了されたプロセスの親プロセスID

  • ProcessId - 作成または終了されたプロセス自身のプロセスID

  • Create - プロセスが作成された(TRUE)のか終了された(FALSE)のかを示すブール値

Createパラメータの値がTRUEであるかFALSEであるかを検証すれば、プロセスの作成時と終了時のどちらで処理を実行するのかを定義できます。 親プロセスの情報を収集するにはParentIdパラメータに渡される親プロセスIDに、コールバック関数の実行原因となったプロセスの情報を収集するにはProcessIdパラメータに渡されるプロセスIDに対して、カーネルAPIシステムコールを実行して情報を収集する必要があります。 コールバック関数の実行が開始された時刻情報は、KeQuerySystemTimePrecise APIなどで取得できます。

プロセスID以上の情報が欲しい場合、カーネルAPIシステムコールを用いた手動での処理を簡略化するために、PsSetCreateProcessNotifyRoutineEx APIまたはPsSetCreateProcessNotifyRoutineEx2 APIを用いる方が便利です。 APIの定義はそれぞれ以下に従います。

NTSTATUS PsSetCreateProcessNotifyRoutineEx(
  [in] PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine,
  [in] BOOLEAN                           Remove
);

NTSTATUS PsSetCreateProcessNotifyRoutineEx2(
  [in] PSCREATEPROCESSNOTIFYTYPE NotifyType,
  [in] PVOID                     NotifyInformation,
  [in] BOOLEAN                   Remove
);

先述のPsSetCreateProcessNotifyRoutine APIと同様に、プロセスの作成または終了を示す情報はRemoveパラメータに設定しますが、登録するコールバック関数の関数シグネチャは、以下のPCREATE_PROCESS_NOTIFY_ROUTINE_EXという関数シグネチャに従います。

PCREATE_PROCESS_NOTIFY_ROUTINE_EX PcreateProcessNotifyRoutineEx;

void PcreateProcessNotifyRoutineEx(
  [_Inout_]           PEPROCESS Process,
  [in]                HANDLE ProcessId,
  [in, out, optional] PPS_CREATE_NOTIFY_INFO CreateInfo
);

PsSetCreateProcessNotifyRoutineEx2 APIでは、コールバック関数への関数ポインタはNotifyInformationパラメータに渡します。 NotifyInformationパラメータの変数型はPVOIDと定義されていますが、実際にはPsSetCreateProcessNotifyRoutineEx APIと同じPCREATE_PROCESS_NOTIFY_ROUTINE_EXの関数シグネチャを渡す仕様であるため、どちらのカーネルAPIでも同じ関数シグネチャのコールバック関数が渡されます。 これら2つのAPIの違いは、コールバック関数に渡される情報の詳細度です。 PsSetCreateProcessNotifyRoutineEx2 APIの第1パラメータであるNotifyTypeには、以下のPSCREATEPROCESSNOTIFYTYPE列挙体の値を指定します。

typedef enum _PSCREATEPROCESSNOTIFYTYPE {
  PsCreateProcessNotifySubsystems
} PSCREATEPROCESSNOTIFYTYPE;

Windows 11 Version 24H2の時点では、PSCREATEPROCESSNOTIFYTYPE列挙体の値はPsCreateProcessNotifySubsystems0)しか定義されておらず、NotifyTypeパラメータに他の値を指定してPsSetCreateProcessNotifyRoutineEx2 APIを呼び出してもSTATUS_INVALID_PARAMETERというNTSTATUSコードが返され、コールバック関数の実行は失敗します。

さて、関数シグネチャPCREATE_PROCESS_NOTIFY_ROUTINE_EXについて話を戻すと、Processパラメータには生成されたプロセスの情報が保存されているEPROCESS構造体の情報が格納されているメモリアドレスが、ProcessIdパラメータには生成されたプロセスのプロセスIDが、CreateInfoパラメータには以下のPS_CREATE_NOTIFY_INFO構造体の情報が格納されているメモリアドレスが渡されます。

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG Flags;
    struct {
      ULONG FileOpenNameAvailable : 1;
      ULONG IsSubsystemProcess : 1;
      ULONG Reserved : 30;
    };
  };
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

構造体に格納されているフィールドの情報は、それぞれ以下の通りです。

  • Size - PS_CREATE_NOTIFY_INFO構造体自体のバイト数

  • Flags - プロセス情報を示すフラグ値。FileOpenNameAvailableフラグ(0x00000001)が設定されている場合はプロセスの生成元ファイル名が取得できる状態であり、IsSubsystemProcessフラグ(0x00000002)が設定されている場合はWin32ではないサブシステムのプロセスであるという意味です。IsSubsystemProcessフラグはPsSetCreateProcessNotifyRoutineEx2 APIを用いてコールバック関数を設定した場合にのみ用いられ、このフラグが設定されているプロセスのサブシステム情報を収集するには、情報クラスProcessSubsystemInformationを指定してシステムコールNtQueryInformationProcessを用いる必要があります

  • ParentProcessId - プロセスの親プロセスID。Windows OSでは、作成元プロセスが作成されたプロセスの親プロセスIDを変更できるため、このフィールド情報は必ずしも作成元プロセスのプロセスIDとは一致しません。作成元プロセスのプロセスIDは、この後に続くCreatingThreadIdフィールドから取得できます

  • CreatingThreadId - 以下のCLIENT_ID構造体として、作成元プロセスのID(UniqueProcess)と作成元スレッドのID(UniqueThread)の情報が設定されています

      typedef struct _CLIENT_ID {
         HANDLE UniqueProcess;
         HANDLE UniqueThread;
       } CLIENT_ID;
    
  • FileObject - プロセスの実行ファイル情報を示す、FILE_OBJECT構造体の情報が格納されているメモリアドレス。本題から逸れるため、本記事では詳しくは解説しません

  • ImageFileName ファイル名の文字列を示す、UNICODE_STRING構造体の情報が格納されているメモリアドレス。カーネル空間では、文字列情報は以下に示すUNICODE_STRING構造体のデータとして保存されます。

      typedef struct _UNICODE_STRING {
        USHORT Length;
        USHORT MaximumLength;
        PWSTR  Buffer;
      } UNICODE_STRING, *PUNICODE_STRING;
    

    Windows OSではUnicode(UTF16 Little Endian)が既定の文字列書式として採用されています。Lengthフィールドには文字列のバイト数(長さではありません。例えばAの場合は\x41\x00なのでLength2)、MaximumLengthフィールドには格納できる文字列情報の最大バイト数、Bufferフィールドには文字列情報が格納されているメモリアドレスを設定します。このデータ構造体を文字列情報の保存に用いれば、文字列の終端を示すNULLバイトが必要なくなるだけではなく、NULLバイトを含む文字列データを定義できるため、Windowsカーネルで標準的な文字列データ構造体として採用されています

  • CommandLine プロセスの生成に用いられたコマンドライン文字列を示す、UNICODE_STRING構造体の情報が格納されているメモリアドレス

  • CreationStatus - プロセスの生成ステータスを示すNTSTATUSコード

プロセスの終了時にコールバック関数が呼び出される場合、CreateInfoパラメータにはNULLポインタが渡されるため、CreateInfoパラメータの値を検証すれば、コールバック関数がプロセスの生成時と終了時のどちらの時点で呼び出されたのかが判断できます。

コールバック機能で収集した情報は、コールバック関数内部で処理してもよいですが、ユーザ空間で処理したい場合が多いでしょう。 一般的なカーネルドライバでは、IOCTL命令を通じてユーザ空間にデータを連携できます。

以下の図では、この機能を活用したカーネルドライバと、その情報を問い合わせるユーザ空間のプログラムを独自に作成して実行している様子を示しています。 カーネルドライバには、カーネルモードのコールバック関数で収集した情報をカーネル空間に保存し、IOCTL命令を通じてユーザ空間に送信する機能を実装しています。

カーネルモードのコールバック関数を利用した生成プロセスの追跡例

図の例では、カーネルドライバを起動した後にnotepad.exeを起動して専用のクライアントプログラムからカーネルドライバにIOCTL命令を送り、起動されたプロセスの時刻、コマンドライン、プロセスID、親プロセスID、EPROCESS構造体のアドレスを取得しています。2025/08/18 21:27:46に"C:\Windows\System32\notepad.exe"というコマンドで、プロセスIDが384のプロセスからnotepad.exeが起動されたということが確認できています。

登録されたコールバック関数の列挙

WinDbgカーネルデバッグすれば、デバッグ対象のWindows OSに登録されているコールバック関数がnt!PspCreateProcessNotifyRoutineというシンボルが割り当てられているメモリアドレスから確認できます。 nt!PspCreateProcessNotifyRoutineというシンボル名はデバッグ用のプライベートシンボルであり、デバッグシンボルが無い状態でntoskrnl.exeを解析しても、そのメモリアドレスを直接的には特定できないようになっています。 nt!PspCreateProcessNotifyRoutineは、コールバック関数の定義情報が格納されているメモリアドレス情報の配列であり、以下のように確認できます。

0: kd> dps nt!PspCreateProcessNotifyRoutine
fffff802`50f0bbe0  ffffa405`624f581f
fffff802`50f0bbe8  ffffa405`62afe49f
fffff802`50f0bbf0  ffffa405`62afe4cf
fffff802`50f0bbf8  ffffa405`62afe37f
fffff802`50f0bc00  ffffa405`641875ff
fffff802`50f0bc08  ffffa405`64187d7f
fffff802`50f0bc10  ffffa405`6418858f
fffff802`50f0bc18  ffffa405`646514af
fffff802`50f0bc20  ffffa405`64654b3f
fffff802`50f0bc28  ffffa405`64659fcf
fffff802`50f0bc30  ffffa405`664e423f
fffff802`50f0bc38  ffffa405`680ac11f
fffff802`50f0bc40  ffffa405`680ae1ef
fffff802`50f0bc48  ffffa405`680ae36f
fffff802`50f0bc50  00000000`00000000
fffff802`50f0bc58  00000000`00000000

ffffa405`624f581fのように、すべての配列要素の値はすべて0xfに設定されています。 この配列の値の下位3ビットには追加情報(詳細については不明)が含まれており、コールバック関数の情報が保存されているメモリアドレスを計算するにはffffffff`fffffff8をAND演算します(コールバック情報は必ず8の倍数のメモリアドレスが開始地点となるように整列されるため下位3ビットは用いられず、追加情報を示すビットを設定しても問題ありません)。 例えば、最初のffffa405`624f581fからコールバック関数の情報が保存されているメモリアドレスを算出して情報を確認すると、以下のようにコールバック関数としてcng!CngCreateProcessNotifyRoutineが登録されていると確認できます。

0: kd> dps (ffffa405`624f581f & ffffffff`fffffff8) L2
ffffa405`624f5818  fffff802`556e5500 cng!CngCreateProcessNotifyRoutine
ffffa405`624f5820  00000000`00000000

関数アドレスに続く値は、コールバック関数を登録したカーネルAPIの情報を示しています。 よって、カーネルはその値を確認して、どの情報をコールバック関数に渡すか判別できるという仕組みです。 コールバック関数がPsSetCreateProcessNotifyRoutine APIで登録された場合は00000000PsSetCreateProcessNotifyRoutineEx APIで登録された場合は00000002PsSetCreateProcessNotifyRoutineEx2 APIで登録された場合は00000006が設定されます。 試しに、それぞれのAPIを用いてコールバック関数を登録するカーネルドライバを実装すると、以下のように確認できます。

# PsSetCreateProcessNotifyRoutine APIで登録したコールバック関数の情報
0: kd> dps (ffffa405`680ac11f & ffffffff`fffffff8) L2
ffffa405`680ac118  fffff802`75a71ec0 CallbackTest!ProcessNotifyRoutine
ffffa405`680ac120  00000000`00000000

# PsSetCreateProcessNotifyRoutineEx APIで登録したコールバック関数の情報
0: kd> dps (ffffa405`680ae1ef & ffffffff`fffffff8) L2
ffffa405`680ae1e8  fffff802`75a71ed0 CallbackTest!ProcessNotifyRoutineEx
ffffa405`680ae1f0  00000000`00000002

# PsSetCreateProcessNotifyRoutineEx2 APIで登録したコールバック関数の情報
0: kd> dps (ffffa405`680ae36f & ffffffff`fffffff8) L2
ffffa405`680ae368  fffff802`75a71ed0 CallbackTest!ProcessNotifyRoutineEx2
ffffa405`680ae370  00000000`00000006

応用例

ProcMon

カーネルモードのコールバック機能は、EDRをはじめとするセキュリティ製品やゲームのチート行為を検出するソフトウェアなどで活用されていますが、最も身近な例はSysinternals SuiteProcMonでしょう。 ProcMonでは、プロセス情報の収集にカーネルモードのコールバック機能を活用しています。 ProcMonの実行ファイルの.rsrcセクションには、カーネルモードのコールバック機能を利用するためのカーネルドライバが埋め込まれており、ProcMonの初回起動時にC:\Windows\System32\driversディレクトリに展開されます。 カーネルドライバはPROCMONXX.SYSXXの部分は数字)という命名であり、隠しファイルとして設置されるため、以下のように隠しファイルを閲覧するオプションを有効化しないと存在を確認できません。

PS C:\> Get-ChildItem -Path C:\Windows\System32\drivers\ | ?{ $_.Name -imatch "proc" }


    Directory: C:\Windows\System32\drivers


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         8/13/2025  11:32 AM         271744 processr.sys
-a----         7/30/2025   7:56 PM          37288 PROCEXP152.SYS
-a----         4/11/2025   3:09 PM          79880 ProcLaunchMon.sys


PS C:\> Get-ChildItem -Force -Path C:\Windows\System32\drivers\ | ?{ $_.Name -imatch "proc" }


    Directory: C:\Windows\System32\drivers


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         8/13/2025  11:32 AM         271744 processr.sys
-a----         7/30/2025   7:56 PM          37288 PROCEXP152.SYS
-a----         4/11/2025   3:09 PM          79880 ProcLaunchMon.sys
-a-h--         7/25/2025   2:08 PM          82344 PROCMON24.SYS

ProcMonのカーネルドライバは一般的なカーネルドライバとは異なり、ファイルシステムドライバとして実装されています。 sc.exeコマンドなどではサービス情報が確認できない設定でシステムに登録されるため、ファイルシステムドライバを制御するためのfltmc.exeコマンドでシステムへの登録が確認できます。 管理者権限でfltmc.exeコマンドを実行すると、以下のようにProcMonのカーネルドライバであるPROCMON24の存在が確認できます。

PS C:\> fltmc

Filter Name                     Num Instances    Altitude    Frame
------------------------------  -------------  ------------  -----
bindflt                                 1       409800         0
UCPD                                    6       385250.5       0
PROCMON24                               6       385200         0
WdFilter                                6       328010         0
storqosflt                              0       244000         0
wcifs                                   0       189900         0
PrjFlt                                  0       189800         0
CldFlt                                  0       180451         0
bfs                                     8       150000         0
FileCrypt                               0       141100         0
luafv                                   1       135000         0
UnionFS                                 0       130850         0
npsvctrig                               1        46000         0
Wof                                     3        40700         0
FileInfo                                6        40500         0

ProcMonのカーネルドライバは一般的なカーネルドライバではなく、ファイルシステム用のカーネルドライバであるため、本記事の執筆時点で最新版のProcMonではユーザ空間への情報連携にはIOCTL命令は使われていません。 代わりに、ファイルシステムの通信ポート機能を用いて、カーネル空間で取得した状態をユーザ空間に連携しています。 ProcMonのカーネルドライバが作成した通信用ポートは、ProcMonのドライバが稼働している間は、オブジェクトディレクトリのルートにFilterConnectionPort型のオブジェクトとして作成されます。 Sysinternals SuiteのWinObjを用いると、以下のようにProcessMonitor24PortというProcMonの通信用ポートのオブジェクトが確認できます。

ProcMonの通信用ポート

Kernel APC Injection

冒頭で言及した通り、コールバック関数ではプロセス情報の収集のみではなく、開始されるプロセスの状態を操作できます。 セキュリティ監視製品には、コールバック機能にAPC Injectionという手法を組み合わせて、Windows OS上のプロセスに監視用のDLLを読み込ませるものが存在します。

APC(Asynchronous Procedure Calls: 非同期プロシージャコール)を用いてカーネルモードからDLL Injectionする手法は、元々は攻撃者が発見したものであり、それがセキュリティ製品に応用されています。 Kernel APC Injectionは、2009年頃に攻撃者に悪用されたZeroAccessというカーネルモードRootkitの解析により発見されました。 現在、初出の解析レポートは消失しておりアクセスできませんが、詳細についてはQueen's Universityの修士論文が参考になるでしょう。

あまり詳しくは解説しませんが、この手法ではWindowsが実装しているAPIの仕様を上手く活用しています。 APCを用いると、プロセスのスレッドに対して、実行して欲しい命令を登録できます。 カーネルでは、KeInitializeApc APIKeInsertQueueApc APIを用いてAPCの命令を登録します。 これらのAPIMicrosoft公式には文書が公開されていないので詳細については割愛しますが、KeInitializeApc APIを用いてスレッドに実行して欲しい関数の関数ポインタを指定し、KeInsertQueueApc APIを用いてスレッドに命令を登録します。 重要なのは、スレッドに実行して欲しい関数の第3パラメータまで制御できるという点です。 近年、Kernel APC Injectionでは関数ポインタにLdrLoadDll APIという非公開APIを用いる手法が一般的なようですが、Rootkitから発見された初出のKernel APC Injectionでは以下のLoadLibraryExA APIという公開APIが用いられていたようです。

HMODULE LoadLibraryExA(
  [in] LPCSTR lpLibFileName,
       HANDLE hFile,
  [in] DWORD  dwFlags
);

カーネルからはスレッドに実行して欲しい関数の第3パラメータまで制御できるため、LoadLibraryExA APIのすべてのパラメータを制御した状態でAPCの命令をスレッドに登録できます。 よって、lpLibFileNameパラメータに読み込んで欲しいDLLの絶対パスを指定し、APCの命令をスレッドに登録すれば、指定したDLLを読み込むようにカーネルからプロセスのスレッドに指示できるという原理です。

ただし、すべてのスレッドがAPCにより登録された命令を実行するわけではありません。 詳細について省略しますが、APCにより登録された命令を実行するスレッドは、命令が登録された後にAlertableという状態に移行するものである必要があります。 Alertableという状態にならないスレッドは、APCにより登録された命令を実行しません。 通常のプロセスでは複数のスレッドが稼働しており、Alertableな状態に移行するスレッドを識別するのは困難ですが、プロセスの生成時にカーネルモードで実行されるコールバック関数では、確実にAlertableな状態に移行するスレッドにAPCの命令を登録できます。 先述の通り、プロセスの生成時にカーネルモードで実行されるコールバック関数は、プロセスの初期化スレッドが開始する前に実行され、この段階では初期化スレッド以外のスレッドが存在しない状態です。 プロセスの初期化スレッドは、必ずAlertableな状態に移行します。 つまり、プロセスの生成時にカーネルモードで実行されるコールバック関数から、プロセスの初期化スレッドにAPCの命令を登録すれば、確実にDLL Injectionが実現できます。

こうして、現代のセキュリティ監視製品やゲームのチート対策製品では、カーネルドライバの起動後に実行されたすべてのプロセスに対して、自身の監視用DLLを読み込ませられます。 以下の図は、この手法を実装したカーネルドライバの動作例を示しています。

Kernel APC Injectionの動作例

カーネルドライバは、powershell.exeのプロセスが起動したら、カーネルモードで動作するコールバック関数からC:\Works\TestLib_x64.dllというDLLをpowershell.exeのプロセスに読み込ませるように実装しています。 C:\Works\TestLib_x64.dllは、読み込まれたプロセスの名前とプロセスIDをメッセージボックスとして出力するだけのDLLです。 例示している図では、powershell.exeが起動して初期化動作が完了し、プロンプトが表示される前にDLLが読み込まれてメッセージボックスが出現していることが確認できます。 また、Sysinternals SuiteのProcess Explorerから、powershell.exeC:\Works\TestLib_x64.dllが読み込まれていると分かります。

まとめ

今回の記事では、デバッグからセキュリティ監視製品にまで幅広く活用されている、Windows OSに実装されているカーネルモードのコールバック機能の一部について解説しました。 また、現実のソフトウェアでの機能の応用例についても触れました。 これらの手法の理解により、Windowsカーネルやセキュリティ監視製品などへの理解が深まり、ツールの開発やインシデント調査などに役立つでしょう。