GDI/USER系カーネル関数フック #2

前回の続報。時間が作れそうもないので、暫定版をあげます(sdt_shadow.cab)。次のような制約がありますが、参考になる部分があればどうぞー。

  1. XP限定です(構造体定義がXPのものしかない)
  2. GUIとか「使う」ためのコードは入ってません
  3. 構造体定義は必要最小限です(PAD長 Paddingされています)
  4. XP-SP2のVM上でのみテストしています


応用すれば SetWindowsHook 系を実行するプロセスの制限・監視とかができます。GDI++カーネル空間で完結させたりとかもできるかな? 10中8,9やらないけど。以下コードの簡単な説明。

続報 08/02/20

ここで紹介している手順は冗長です。KeServiceDescriptorTableShadow の操作は、ユーザモードプロセスから DeviceIoControl を叩きドライバの IRP_MJ_DEVICE_CONTROL ハンドラ関数の中で処理することで青画面にならずに操作できるようです。



winlogon.exe が起動していたらETHREADを取得して、即座にフック実施。そうでないなら起動を PsSetCreateProcessNotifyRoutine で監視する。

  // winlogon.exe が起動しているか否か
  if (EPROCESS* winlogon = GetProcessByName("winlogon.exe"))
  { // 起動しているのでフック開始
    DeviceExtension->bHooked = SetHook(CONTAINING_RECORD(winlogon->ThreadListHead.Flink,
                                                          ETHREAD, ThreadListEntry),
                                       DeviceExtension);
  }
  else
  { // 起動していないので起動を監視
    status = PsSetCreateProcessNotifyRoutine(CreateProcessNotifyRoutine, FALSE);
    if (NT_ERROR(status)) return status;
    DeviceExtension->ProcessNotifyRoutine = CreateProcessNotifyRoutine;
  }


起動を監視するルーチン CreateProcessNotifyRoutine では、プロセスID→EPROCESS取得→イメージ名比較→(winlogon.exeなら)→ETHREAD取得→フック実施という流れ。ここでは省略してるけど、一度winlogon.exeが起動したことを確認したら、監視を解除すること。誤動作しないようにね。

// winlogon.exe の起動を監視し、起動したらAPC登録を行う
// ドライバロード時に winlogon.exe が起動していないときだけ実行される
static void CreateProcessNotifyRoutine(IN HANDLE ParentId,
                                       IN HANDLE ProcessId,
                                       IN BOOLEAN Create)
{
  //...
  if (!Create) return;

  // PIDからプロセスを取得
  EPROCESS* Process = NULL;
  status = PsLookupProcessByProcessId(ProcessId, &Process);
  if (NT_ERROR(status)) return;
  util::scoped_resource<EPROCESS**> process(&Process, ObfDereferenceObject);

  // winlogon.exe というイメージ名であるか
  if (strcmp((char const*)Process->ImageFileName, "winlogon.exe") != 0) return;

  //...
  // winlogon.exe のスレッドを取得する
  if (ETHREAD* Thread = CONTAINING_RECORD(Process->ThreadListHead.Flink, ETHREAD, ThreadListEntry))
  {
    //...
    // フックAPCを実行
    DeviceExtension->bHooked = SetHook(Thread, DeviceExtension);
  }


SetHookでは KeServiceDescriptorTableShadow 大域変数を初期化し、受け取ったETHREADを使ってAPCを登録する。つまり、winlogon.exeの第一スレッドに特定の処理を実施させるため、その処理を登録しているわけ。

// フックAPCを実行
static bool SetHook(ETHREAD* Thread, DEVICE_EXTENSION* DeviceExtension)
{
  //...
  // SDTShadow の初期化
  if (!InitServiceDescriptorTableShadow()) return false;

  // フック・アンフックAPCの初期化
  KeInitializeApc(&DeviceExtension->HookApc,
                  &Thread->Tcb, OriginalApcEnvironment, HookRoutine, NULL, NULL, KernelMode, NULL);
  //...
  // フックAPCの登録
  BOOLEAN bInsert = KeInsertQueueApc(&DeviceExtension->HookApc, NULL, NULL, 0);

APC操作にはKAPCという構造体変数に対して、初期化と、キューへの登録のための非公開関数を使う。その際に対象とするスレッド(KTHREAD構造体)変数が必要になる。また、KAPC構造体変数はヒープに作るか大域変数に作るかして、ローカルスタックで破壊されないようにすること。今回はDEVICE_EXTENSION構造体に含めて、コーディングが楽な大域変数にしてます。


InitServiceDescriptorTableShadow はバージョンごとにハードコードされた値を使って、KeServiceDescriptorTableShadow を探し出す(これを動的にするための決定的なロジックは見つけられなかった)。

// KeServiceDescriptorTableShadow の初期化と取得
inline SERVICE_DESCRIPTOR_TABLE* InitServiceDescriptorTableShadow()
{
  if (!KeServiceDescriptorTableShadow)
  {
    ULONG majorVersion = 0;
    ULONG minorVersion = 0;
    ULONG buildNumber  = 0;
    PsGetVersion(&majorVersion, &minorVersion, &buildNumber, NULL);
    if      (majorVersion == 5 && minorVersion == 0) 
              KeServiceDescriptorTableShadow = CALC_OFFSET_BYTE(KeServiceDescriptorTable, -0xE0);
    else if (majorVersion == 5 && minorVersion == 1) 
              KeServiceDescriptorTableShadow = CALC_OFFSET_BYTE(KeServiceDescriptorTable, -0x40);

具体的な値の調査はWinDbgでどうぞ。

kd> x nt!keservice*
805634c0 nt!KeServiceDescriptorTableShadow = <no type information>
80563500 nt!KeServiceDescriptorTable = <no type information>


で、HookRoutineでは KeServiceDescriptorTableShadow[1].ServiceTable[i] を対象として関数交換を行う。このとき i はシステムコール番号 - 0x1000 の値を使うというのは前回説明した通り。各関数のインデックス値は Windows XP Build 2600 System Services vs Windows 2000 Build 2195 System Services を参考にどうぞ。

  OldNtUserSetWindowsHookEx = HOOK_SYSCALL_INDEX_WIN32K(INDEX_NtUserSetWindowsHookEx, 
                                                        NewNtUserSetWindowsHookEx);
template <typename T>
inline T HOOK_SYSCALL_INDEX_WIN32K(ULONG target_i, T new_f)
{
  return static_cast<T>((void*)InterlockedExchange((long*)(&KeServiceDescriptorTableShadow[1].ServiceTable[target_i]), (long)new_f));
}


どんなもんでしょうか。ひどく駆け足なので、あまり参考にならないかもですね。あと実装についてのアドバイスなどがいただければとても嬉しいです。