PE(Portable Executable)ファイルフォーマット その3 ~ IAT 書き換えによるインポートシンボル(API)のフック ~

サンプルソースのダウンロード(HookImp.zip)

概要

 今回はインポートアドレステーブルに格納されているインポートシンボルのアドレスを書き換えることでフックを行います。単にインポートシンボルのフックと書かないのは他にも方法があるからですが、それに関しては別の機会に。

PEFile クラスへの機能追加

 前回実装した PEFile クラスでは HMODULE を引数にとるコンストラクタと、インポートシンボルをフックする HookImportSymbol() メソッドが追加されています。

PEFile.h
//! PE ファイルから情報を取得するためのクラス。
class PEFile
{
public:
  //! コンストラクタ
  explicit PEFile(const wchar_t* i_file_name);
  //! モジュールハンドルからのコンストラクタ。
  explicit PEFile(HMODULE i_module_handle);
  //! デストラクタ
  virtual ~PEFile();
  
  //! ロードアドレスを取得する。
  BYTE* GetLoadBase();
  
  //! DOS ヘッダへのポインタを取得。
  IMAGE_DOS_HEADER* GetDosHeaderP();
  //! NT ヘッダへのポインタを取得。
  IMAGE_NT_HEADERS32* GetNtHeadersP();
  //! ファイルヘッダへのポインタを取得。
  IMAGE_FILE_HEADER* GetFileHeaderP();
  //! オプショナルヘッダへのポインタを取得。
  IMAGE_OPTIONAL_HEADER32* GetOptionalHeaderP();
  //! i_index 番目のセクションヘッダへのポインタを取得。
  IMAGE_SECTION_HEADER* GetSectionHeader(int i_index);
  
  //! i_index 番目のデータディレクトリエントリが指すデータへのポインタを取得する。
  BYTE* GetDirEntryDataP(int i_index);
  //! i_index 番目のデータディレクトリエントリが指すデータへのサイズを取得する。
  DWORD GetDirEntryDataSize(int i_index);
  
  //! インポートディレクトリへのポインタを取得する。
  IMAGE_IMPORT_DESCRIPTOR* GetImportDirP();
  //! インポートしている DLL 名を取得する。
  std::vector<const char*> GetImportDllNames();
  //! 指定した DLL からインポートしているシンボルを取得する。
  std::vector<ImportSymbol> GetImportSymbols(const char* i_dll_name);
  //! インポートシンボルをフックする。成功した場合 IAT エントリへのポインタを返す。
  void** HookImportSymbol(void* i_old_p, void* i_new_p);
protected:
  //! イメージファイルをメモリにマッピングする。
  static BYTE* MapImage(const wchar_t* i_file_name);

protected:
  bool                   m_need_to_free;    //!< m_load_base を解放する必要があるか?
  BYTE*                  m_load_base;       //!< イメージのロードされたアドレス
  IMAGE_DOS_HEADER*      m_dos_header_p;    //!< DOS ヘッダへのポインタ
  IMAGE_NT_HEADERS*      m_nt_headers_p;    //!< NT ヘッダへのポインタ
  IMAGE_SECTION_HEADER*  m_section_table_p; //!< セクションテーブルへのポインタ
};

 では、コンストラクタのほうから。
PEFile.cpp
//! モジュールハンドルからのコンストラクタ。
PEFile::PEFile(HMODULE i_module_handle)
: m_need_to_free(false),  // VirtualFree() の必要なし
  m_load_base(NULL),
  m_dos_header_p(NULL),
  m_nt_headers_p(NULL),
  m_section_table_p(NULL)
{
  do
  {
    // DOS ヘッダの取得と確認。
    m_dos_header_p = reinterpret_cast<IMAGE_DOS_HEADER*>(i_module_handle);
    if( IsBadReadPtr(m_dos_header_p, sizeof(IMAGE_DOS_HEADER))
    ||  m_dos_header_p->e_magic != *reinterpret_cast<WORD*>("MZ") )
    {
      _RPTF0(_CRT_WARN, "有効なモジュールハンドルではありません。");
      break;
    }
    
    // NT ヘッダの取得と確認
    m_nt_headers_p  = reinterpret_cast<IMAGE_NT_HEADERS*>(
                          reinterpret_cast<BYTE*>(i_module_handle)
                        + m_dos_header_p->e_lfanew
                      );
    if( m_nt_headers_p->Signature != *reinterpret_cast<DWORD*>("PE\0\0")
    ||  m_nt_headers_p->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR32_MAGIC )
    {
      _RPTF0(_CRT_WARN, "PE ファイルではありません。\n");
      break;
    }
    
    // セクションテーブルへのポインタを取得。
    m_section_table_p = reinterpret_cast<IMAGE_SECTION_HEADER*>(m_nt_headers_p + 1);
    // m_load_base に i_module_handle を入れておく。
    m_load_base = reinterpret_cast<BYTE*>(i_module_handle);
  }
  while( 0 );
}
 おもむろに引数で渡された i_module_handle を IMAGE_DOS_HEADER* にキャストしています。実は LoadLibrary() API や GetModuleHandle() が返す HMODULE, WinMain() に渡される HINSTANCE の値はイメージファイルがロードされたアドレスなのです。ですので、HMODULE から各ヘッダを取得して値をチェックすれば良いということになります。
 というわけで、前回のメンバ m_need_to_free は PEFile がマップしたイメージファイルなのか、ローダによってマップされたイメージファイル(つまりモジュール)なのかを区別するための変数でした。

 次は HookImportSymbol() メソッド。
PEFile.cpp
//! インポートシンボルをフックする。成功した場合 IAT エントリへのポインタを返す。
void** PEFile::HookImportSymbol(void* i_old_p, void* i_new_p)
{
  void**                    retval      = NULL;
  IMAGE_IMPORT_DESCRIPTOR*  imp_descs_p = GetImportDirP();
  
  if( imp_descs_p )
  {
    for(int i = 0; imp_descs_p[i].FirstThunk != 0; i++)
    {
      // インポートアドレステーブルへのポインタを取得。
      void**  iat_p = GetDataP<void**>(imp_descs_p[i].FirstThunk);

      for(int j = 0; iat_p[j] != 0; j++)
      {
        if( iat_p[j] == reinterpret_cast<FARPROC>(i_old_p) )
        {
          DWORD old_protect = 0;
          VirtualProtect(&iat_p[j], sizeof(FARPROC), PAGE_READWRITE, &old_protect);

          retval   = &iat_p[j];
          iat_p[j] = reinterpret_cast<FARPROC>(i_new_p);

          VirtualProtect(&iat_p[j], sizeof(FARPROC), old_protect, &old_protect);
          // break; // break しない。全ての IAT を走査する。
        }
      }

      break;
    }
  }
  
  return retval;
}
 全ての IAT を走査して、フックしたいインポートシンボルの場合だったら新しいもので置き換える、ということをやっています。エクスポートの転送を使ったりすることで IAT には同じインポートシンボルが複数存在する可能性があるので、書き換えたとしてもループは抜けません。
 IAT を書き換えるときの注意点ですが、IAT は読取専用領域におかれることがあるので、VirtualProtect() を使って読み書きできるようにページ属性を変更しています。

 では、実際にフックを行うサンプル。MessageBoxA() をフックして別のメッセージを表示するサンプルです。GetProcAddress() で MessageBoxA() のアドレスを取得し、MyMessageBoxA() 関数で置き換えています。
main.cpp
#include <windows.h>
#include <stdio.h>
#include <string>
#include <vector>
#include "PEFile.h"


typedef int (WINAPI* MessageBoxAP)(HWND, const char*, const char*, int);


//! フック前のメッセージボックス
static MessageBoxAP s_prev_MessageBoxA = NULL;


//! メッセージボックスを置き換える関数
static int WINAPI MyMessageBoxA(
  HWND          i_window_handle,
  const char*   i_message,
  const char*   i_title,
  int           i_type)
{
  return s_prev_MessageBoxA(
           i_window_handle,
           "All your base are belong to us.",
           i_title,
           i_type
         );
}


//! メインルーチン
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
//int wmain()
{
  MessageBox(NULL, "Hello, world!", "フック前", MB_OK);

  s_prev_MessageBoxA = reinterpret_cast<MessageBoxAP>(
                         GetProcAddress(
                           GetModuleHandle("user32.dll"),
                           "MessageBoxA"
                         )
                       );

  PEFile  pe_file(GetModuleHandle(NULL));
  pe_file.HookImportSymbol(
    s_prev_MessageBoxA,
    &MyMessageBoxA
  );

  MessageBox(NULL, "Hello, world!", "フック後", MB_OK);
  
  return 0;
}

  
実行結果


 さて、自プロセスでインポートシンボルのフックを行うのはテスト以外に意味がないので、他プロセスに対してインポートシンボルをフックしてみましょう。

他プロセスに対してインポートシンボルのフックを行う

 通常は置き換え後の関数とフック処理を DLL 内において、他プロセスにその DLL を注入するという形をとります。フック処理は DllMain() の DLL_PROCESS_ATTACH 時に行えば良いでしょう。
 では、DLL を注入するにはどうすれば良いか?という話になります。DLL を他プロセスに注入するには SetWindowsHookEx() API を使う方法、CreateRemoteThread() API を使う方法、LoadLibrary() を呼び出すルーチンをフック対象プロセスに割り当てて既存スレッドをそちらにジャンプさせる方法の 3 種類があります。

 SetWindowsHookEx() API を使うと良くも悪くも全てのプロセスに DLL が注入されるので、フック対象となるプロセスを絞らなければいけません。また、グローバルフックしたイベントがフック対象プロセスで発生するまで DLL がロードされません。例えば WH_GETMESSAGE とともに SetWindowsHookEx() を呼び出した場合は、フック対象プロセスのスレッドで GetMessage() や PeekMessage() が呼び出されないとフック用の DLL がロードされないのです。そのため、ウィンドウを持たないようなアプリケーションでは DLL がロードされず、フックに失敗してしまいます。さらにフック対象プロセスが生存している間、SetWindowsHookEx() を呼び出したプロセスも生存していなければいけません(GetProcAddress() でフック用 DLL 内の関数を返すため、フック対象プロセスが生存している間はフック用DLL がアンロードされてはいけない)。

 CreateRemoteThread() API を使う方法ではプロセスを指定して DLL を注入することになりますが、CreateRemoteThread() API は Windows 2000 から追加された API で 98/Me 以前の OS では使用できないデメリットがあります。また、マルウェアやウィルスが自身を隠蔽するために CreateRemoteThread() を使うことが多く、アンチウィルスにひっかかってしまう場合があります。でも SetWindowsHookEx() を使うよりは簡単に実装できます。

 3番目の LoadLibrary() を呼び出すルーチンをフック対象プログラムに割り当てて、既存スレッドをそちらにジャンプさせる方法に関しては、色々と厄介なので今回は扱いません。

  • SetWindowsHookEx() を使う方法
    • メリット
      • 全プロセスを対象にインポートシンボルをフックする場合にはこちらの方が簡単。
      • Windows98/ME などでも使用できる。
    • デメリット
      • プロセスを限定する場合はその処理を記述しなければいけない。
      • フック対象プロセスが生存している間は SetWindowsHookEx() を呼び出したプログラムも生存していなければいけない。
      • ウィンドウを持たないようなアプリケーションに対してはフックを行いづらい。
  • CreateRemoteThread() を使う方法
    • メリット
      • プロセスを指定して DLL を注入するので、対象を簡単に絞れる。
      • DLL の注入を担当するプログラムが終了しても、インポートシンボルのフックは継続される。
      • ウィンドウを持たないようなアプリケーションに対してもフックを行える。
    • デメリット
      • Windows98/ME では使用できない。
      • アンチウィルスにひっかかる可能性がある。

SetWindowsHookEx() を使う方法

 まず、DLL の MyMessageBoxA(), MyMessageBoxW(), MyLoadLibraryA(), MyLoadLibraryW(), MyLoadLibraryExA(), MyLoadLibraryExW(), MyGetProcAddress() から。これらは SetWindowsHookEx() を使う場合も CreateRemoteThread() を使う場合も同じ処理で、MyXXX の XXX にとって代わる関数です。
 LoadLibrary() 系では新たにロードされた DLL をフックしてから返します。LoadLibraryEx() 系では LOAD_LIBRARY_AS_DATAFILE フラグがセットされている場合にだけフック処理を行っています。これは LOAD_LIBRARY_AS_DATAFILE が指定されているとイメージファイルのマッピング処理が行われない(ただのファイルとして読み込まれる)上に、ロードアドレスに + 1 された値が返されるからです。まぁ、こんなことをしなくても DOS ヘッダのマジックナンバー確認時に蹴られるんですが。あとは Windows 2000 以降では DLL をロードするのに NTDLL.DLL に含まれる LdrLoadDll() を使うこともできますがそこまでしなくて良いでしょう。
 MyGetProcAddress() はフックの対象となるインポートシンボルが GetProcAddress() されたときにフックで置き換える関数を返します。

DllMain.cpp
//! LoadLibraryA() に代わる関数。
static HMODULE WINAPI MyLoadLibraryA(const char* i_dll_name)
{
  HMODULE module_handle = LoadLibraryA(i_dll_name);

  if( module_handle )
    HookImports(PEFile(module_handle), true);
  
  return module_handle;
}


//=============================================================================


//! LoadLibraryW() に代わる関数。
static HMODULE WINAPI MyLoadLibraryW(const wchar_t* i_dll_name)
{
  HMODULE module_handle = LoadLibraryW(i_dll_name);

  if( module_handle )
    HookImports(PEFile(module_handle), true);
  
  return module_handle;
}


//=============================================================================


//! LoadLibraryExA() に代わる関数。
static HINSTANCE WINAPI MyLoadLibraryExA(
  const char* i_dll_name,
  HANDLE      i_reserved,
  DWORD       i_flags
)
{
  HMODULE module_handle = LoadLibraryExA(i_dll_name, i_reserved, i_flags);

  // LOAD_LIBRARY_AS_DATAFILE フラグが指定されていない場合だけフック処理を行う
  if( module_handle && (i_flags & LOAD_LIBRARY_AS_DATAFILE) == 0 )
    HookImports(PEFile(module_handle), true);

  return module_handle;
}


//=============================================================================


//! LoadLibraryExW() に代わる関数。
static HINSTANCE WINAPI MyLoadLibraryExW(
  const wchar_t* i_dll_name,
  HANDLE         i_reserved,
  DWORD          i_flags
)
{
  HMODULE module_handle = LoadLibraryExW(i_dll_name, i_reserved, i_flags);

  // LOAD_LIBRARY_AS_DATAFILE フラグが指定されていない場合だけフック処理を行う
  if( module_handle && (i_flags & LOAD_LIBRARY_AS_DATAFILE) == 0 )
    HookImports(PEFile(module_handle), true);

  return module_handle;
}


//=============================================================================


//! GetProcAddress() に代わる関数。
static FARPROC WINAPI MyGetProcAddress(
  HMODULE       i_module_handle,
  const char*   i_func_name
)
{
  FARPROC retval = GetProcAddress(i_module_handle, i_func_name);

  for(size_t i = 0; i < numberof(s_hook_info); i++)
  {
    if( s_hook_info[i].old_func_p == retval )
    {
      retval = reinterpret_cast<FARPROC>(s_hook_info[i].new_func_p);
      break;
    }
  }

  return retval;
}

 グローバル変数として共有メモリに配置される静的グローバル変数と、共有メモリには置かれない静的グローバル変数が定義されています。
DllMain.cpp
//! DLL で共有されるセクション
#pragma data_seg(".shared")
static HHOOK    s_hook_handle       = NULL;
static DWORD    s_caller_process_id = 0;
static DWORD    s_target_process_id = 0;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.shared,rws")

//! 自 DLL のハンドル。DllMain() でセットする。
static HMODULE  s_my_dll_handle = NULL;

//! フックするインポートシンボルの情報
static struct {
  const char* dll_name;   // インポートシンボルを含む DLL の名前
  const char* func_name;  // インポートシンボルの名前。
  void*       old_func_p; // インポートシンボルのポインタ。DllMain() で初期化される。
  void*       new_func_p; // 置き換え後のポインタ。
} s_hook_info[] = {
  { "kernel32.dll", "LoadLibraryA",   NULL, &MyLoadLibraryA },
  { "kernel32.dll", "LoadLibraryW",   NULL, &MyLoadLibraryW },
  { "kernel32.dll", "LoadLibraryExA", NULL, &MyLoadLibraryExA },
  { "kernel32.dll", "LoadLibraryExW", NULL, &MyLoadLibraryExW },
  { "kernel32.dll", "GetProcAddress", NULL, &MyGetProcAddress },
  { "user32.dll",   "MessageBoxA",    NULL, &MyMessageBoxA },
  { "user32.dll",   "MessageBoxW",    NULL, &MyMessageBoxW },
};
 s_hook_handle は SetWindowsHookEx() の戻り値を格納しておく先です。
 s_caller_process_id は SetWindowsHookEx() を呼び出したプロセスID を格納しておく先で、そのプロセスに対してはインポートシンボルのフックを行わないために使用されます。
 s_target_process_id はインポートシンボルのフック対象となるプロセスの ID を格納しておく先で、0 だと全プロセスを対象とします。
 s_my_dll_handle は自 DLL のハンドルです。DllMain() でセットされ、SetWindowsHookEx() のパラメータとしてや、フック対象となるモジュールかを判断するために使用されます。
 s_hook_info はフックしたいインポートシンボルを含む DLL 名、関数名、置き換える関数のアドレスをセットにしたものです。他にフックしたいインポートシンボルがあればこのリストに追加します。

 次は ImpHookStart() と ImpHookStop() です。インポートシンボルのフック開始と終了を行うエクスポート関数で、それぞれ SetWindowsHookEx() と UnhookWindowsHookEx() を呼び出します。特に説明はいらないと思います。
DllMain.cpp
//! フックを開始する。
__declspec(dllexport) bool ImpHookStart(DWORD i_target_process_id)
{
  bool  retval = false;

  // 既に SetWindowsHookEx() が呼ばれている場合は無視する。
  if( s_hook_handle == NULL )
  {
    s_hook_handle = SetWindowsHookEx(
                      WH_GETMESSAGE,
                      GetMsgProc,
                      s_my_dll_handle,
                      0 // target thread id
                    );
    
    s_caller_process_id = GetCurrentProcessId();
    s_target_process_id = i_target_process_id;
    retval = s_hook_handle != NULL;
  }
  
  return retval;
}


//=============================================================================


//! フックを終了する。
__declspec(dllexport) bool ImpHookStop()
{
  bool  retval = false;
  
  if( s_hook_handle )
  {
    UnhookWindowsHookEx(s_hook_handle);
    s_hook_handle = NULL;
    retval = true;
  }
  
  return retval;
}

 DllMain() では DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH 時にプロセス ID がフック対象プロセスかどうかを判断し、フック処理、アンフック処理を行っています。HookImports() に true を渡すとフック、false を渡すとフックの解除となります。
DllMain.cpp
//! DLL のエントリポイント。フック処理を行う。
BOOL WINAPI DllMain(
  HINSTANCE   i_module_handle,
  DWORD       i_reason_for_call,
  void*
)
{
  switch( i_reason_for_call )
  {
  case DLL_PROCESS_ATTACH:
    s_my_dll_handle = reinterpret_cast<HMODULE>(i_module_handle);
    if( s_hook_handle
    &&  s_caller_process_id != GetCurrentProcessId()
    &&  (s_target_process_id == 0 || s_target_process_id == GetCurrentProcessId()) )
    {
      for(size_t i = 0; i < numberof(s_hook_info); i++)
      {
        s_hook_info[i].old_func_p = GetProcAddress(
                                      GetModuleHandleA(s_hook_info[i].dll_name),
                                      s_hook_info[i].func_name
                                    );
      }

      DebugOut(L"DLL_PROCESS_ATTACH ProcessId:%08X\n", GetCurrentProcessId());
      HookImports(true);
    }
    
    break;
  case DLL_PROCESS_DETACH:
    if( s_hook_handle
    &&  s_caller_process_id != GetCurrentProcessId()
    &&  (s_target_process_id == 0 || s_target_process_id == GetCurrentProcessId()) )
    {
      DebugOut(L"DLL_PROCESS_DETACH ProcessId:%08X\n", GetCurrentProcessId());
      HookImports(false);
    }
    break;
  }
  
  return TRUE;
}

 HookImports() ではモジュールを列挙し、全てのモジュールに対して PEFile を受け取る HookImports() でフック処理を行っています。
DllMain.cpp
//! 全てのモジュールのインポートシンボルのフック/アンフックを行う。
static bool HookImports(bool i_hook)
{
  bool          retval            = false;
  HANDLE        snapshot_handle   = INVALID_HANDLE_VALUE;
  MODULEENTRY32 module_entry      = { sizeof(MODULEENTRY32), 0 };
  
  do
  {
    // モジュールを列挙
    snapshot_handle = CreateToolhelp32Snapshot(
                        TH32CS_SNAPMODULE,
                        GetCurrentProcessId()
                      );
    for(BOOL is_next = Module32First(snapshot_handle, &module_entry);
        is_next;
        is_next = Module32Next(snapshot_handle, &module_entry))
    {
      // 自 DLL ではなかった場合のみ IAT を書き換える。
      if( module_entry.hModule != s_my_dll_handle )
      {
        HookImports(PEFile(module_entry.hModule), i_hook);
      }
    }
    
    retval = true;
  }
  while( 0 );
  
  if( snapshot_handle != INVALID_HANDLE_VALUE )
  {
    CloseHandle(snapshot_handle);
    snapshot_handle = INVALID_HANDLE_VALUE;
  }
  
  return retval;
}

 以下は PEFile を受け取る HookImports() の定義。
DllMain.cpp
//! 指定したモジュールのインポートシンボルのフック/アンフックを行う。
static bool HookImports(PEFile& io_pe_file, bool i_hook)
{
  for(size_t i = 0; i < numberof(s_hook_info); i++)
  {
    if( i_hook )
    {
      void** iat_entry_p = io_pe_file.HookImportSymbol(
                             s_hook_info[i].old_func_p,
                             s_hook_info[i].new_func_p
                           );
      if( iat_entry_p )
      {
        DebugOut(L"フックしました。:%S@%S, %p\n",
                  s_hook_info[i].func_name, s_hook_info[i].dll_name, iat_entry_p
                );
      }
    }
    else
    {
      io_pe_file.HookImportSymbol(
        s_hook_info[i].new_func_p,
        s_hook_info[i].old_func_p
      );
    }
  }
  
  return true;
}

 DLL 側の処理は以上になります。

 次は ImpHookStart() を呼び出す側です。フック対象となるターゲットプログラムをサスペンド状態で起動し、ImpHookStart() を呼び出した後、スレッドを再開させて終了を待ちます。
main.cpp
#define UNICODE
#define _UNICODE
#include <windows.h>


//! フックを開始する。
__declspec(dllimport) bool ImpHookStart(DWORD i_target_process_id);
//! フックを終了する。
__declspec(dllimport) bool ImpHookStop();
#pragma comment(lib, "..\\hook.lib")

//=============================================================================


//! エラー表示
static void Error(const wchar_t* i_format, ...)
{
  va_list   val;
  wchar_t   message[1024 + 1] = { 0 };
  
  va_start(val, i_format);
  wvsprintf(message, i_format, val);
  va_end(val);
  
  MessageBox(NULL, message, L"エラー", MB_ICONEXCLAMATION);
}


//=============================================================================


//! プライマリスレッドをサスペンド状態にしてターゲットプログラムを起動する。
static bool ExecuteTarget(PROCESS_INFORMATION& o_process_information)
{
  STARTUPINFO   startup_info = { 0 };
  
  return CreateProcess(
           L"HelloWorld.exe",
           NULL,
           NULL,
           NULL,
           FALSE,
           CREATE_SUSPENDED,
           NULL,
           NULL,
           &startup_info,
           &o_process_information
         ) != FALSE;
}


//=============================================================================


//! メインルーチン
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
//int main()
{
  PROCESS_INFORMATION   process_information = { 0 };

  do
  {
  
    // ターゲットプログラムをサスペンド状態で起動する。
    if( ExecuteTarget(process_information) == false )
    {
      Error(L"ターゲットプログラムの起動に失敗しました。\n");
      break;
    }

    // フックを開始する。
    if( ImpHookStart(process_information.dwProcessId) == false )
    {
      Error(L"フックに失敗しました。");
      break;
    }
    
    // ターゲットプログラムのスレッドを再開する。
    ResumeThread(process_information.hThread);
    
    // ターゲットが終了するまで待機する。
    WaitForSingleObject(process_information.hProcess, INFINITE);
  }
  while( 0 );
  
  if( process_information.hThread != NULL )
  {
    CloseHandle(process_information.hThread);
    process_information.hThread = NULL;
  }
  
  if( process_information.hProcess != NULL )
  {
    CloseHandle(process_information.hProcess);
    process_information.hProcess = NULL;
  }
  
  // フックを終了する。
  ImpHookStop();

  return 0;
}

 テスト用のターゲットプログラムは訳あってメッセージボックスで「いいえ」が押されるまでループするプログラムとしました。
HelloWorld.cpp
#include <windows.h>


//! メインルーチン
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
  while( MessageBox(NULL, "world!", "Hello", MB_YESNO) == IDYES )
  {
  }

  return 0;
}


 さて、実行すると最初に表示されるメッセージボックスではフックされておらず、「はい」を押した次からフックされているのが確認できます。これは WinMain() が呼び出された時点ではフック用の DLL がロードされずに普通に MessageBoxA() が呼ばれ、さらにその中で呼ばれている GetMessage() が呼び出されて初めてフック用の DLL がロードされるからです。おまけに HelloWorld.cpp に最適化オプション /Ox をつけてコンパイルするとフックを確認できません。MessageBoxA のアドレスを別の場所に格納してそっちから呼び出しているので書き換えた IAT エントリを参照してくれないのです。困ったもんですね。
 次に説明する CreateRemoteThread() を使う方法ではこのような問題は起きません。

CreateRemoteThread() を使う方法

 CreateRemoteThread() を使う方法では SetWindowsHookEx() 関連の処理やグローバル変数がなくなっています。静的グローバル変数は s_hook_info のみとなり、DllMain() でも対象プロセスの判別がなくなっています。その他は SetWindowsHookEx() を使う方法と変わっていません。が、SetWindowsHookEx() を使う場合と比べて随分すっきりしました。

DllMain.cpp
//! DLL のエントリポイント。フック処理を行う。
BOOL WINAPI DllMain(
  HINSTANCE   i_module_handle,
  DWORD       i_reason_for_call,
  void*
)
{
  switch( i_reason_for_call )
  {
  case DLL_PROCESS_ATTACH:
    DebugOut(L"DLL_PROCESS_ATTACH ProcessId:%08X\n", GetCurrentProcessId());

    for(size_t i = 0; i < numberof(s_hook_info); i++)
    {
      s_hook_info[i].old_func_p = GetProcAddress(
                                    GetModuleHandleA(s_hook_info[i].dll_name),
                                    s_hook_info[i].func_name
                                  );
    }

    HookImports(i_module_handle, true);
    break;
  case DLL_PROCESS_DETACH:
    DebugOut(L"DLL_PROCESS_DETACH ProcessId:%08X\n", GetCurrentProcessId());
    HookImports(i_module_handle, false);
    break;
  }
  
  return TRUE;
}

 注入を担当する EXE 側では、ImpHookStart() の代わりに InjectHookDll() が呼ばれています。そしてフック対象プロセスの終了を待機していません。
main.cpp
//! メインルーチン
int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int)
//int main()
{
  PROCESS_INFORMATION   process_information = { 0 };

  do
  {
    // ターゲットプログラムをサスペンド状態で起動する。
    if( ExecuteTarget(process_information) == false )
    {
      Error(L"ターゲットプログラムの起動に失敗しました。\n");
      break;
    }
    
    // hook.dll をロードさせる。
    InjectHookDll(process_information.hProcess);
    
    // スレッドを開始する。
    ResumeThread(process_information.hThread);
  }
  while( 0 );
  
  if( process_information.hThread != NULL )
  {
    CloseHandle(process_information.hThread);
    process_information.hThread = NULL;
  }
  
  if( process_information.hProcess != NULL )
  {
    CloseHandle(process_information.hProcess);
    process_information.hProcess = NULL;
  }

  return 0;
}

 InjectHookDll() はフック用の DLL を他プロセスへ読み込ませる関数そのものです。
main.cpp
//! hook.dll をターゲットプロセスにロードさせる。
static bool InjectHookDll(HANDLE i_process_handle)
{
  const wchar_t*  DLL_NAME        = L"hook.dll";
  const size_t    DLL_NAME_SIZE   = (lstrlen(DLL_NAME) + 1) * sizeof(wchar_t);
  bool            retval          = false;
  void*           dll_name_vp     = NULL;
  DWORD           written_size    = 0;
  void*           LoadLibraryW_p  = NULL;
  DWORD           thread_id       = 0;
  HANDLE          thread_handle   = NULL;
  DWORD           exit_code       = 0;

  do
  {
    // ターゲットプロセス内に DLL 名を格納するためのメモリを確保。
    dll_name_vp = VirtualAllocEx(
                    i_process_handle,
                    NULL,
                    DLL_NAME_SIZE,
                    MEM_COMMIT,
                    PAGE_READWRITE
                  );
    if( dll_name_vp == NULL )
    {
      Error(L"ターゲットプロセスでのメモリの割り当てに失敗しました。\n");
      break;
    }
    
    // DLL 名を書き込み
    if( WriteProcessMemory(
          i_process_handle,
          dll_name_vp,
          DLL_NAME,
          DLL_NAME_SIZE,
          &written_size
        ) == FALSE )
    {
      Error(L"DLL 名の書き込みに失敗しました。\n");
      break;
    }
    
    // LoadLibraryW のアドレスを取得
    LoadLibraryW_p = GetProcAddress(
                       GetModuleHandle(L"kernel32.dll"),
                       "LoadLibraryW"
                     );

    // DLL をロードさせる。
    thread_handle = CreateRemoteThread(
                      i_process_handle,
                      NULL,
                      0,
                      reinterpret_cast<LPTHREAD_START_ROUTINE>(LoadLibraryW_p),
                      dll_name_vp,
                      0,
                      &thread_id
                    );
    if( thread_handle == NULL )
    {
      Error(L"CreateRemoteThread() に失敗しました。\n");
      break;
    }
    
    // 終了するまで待機。
    WaitForSingleObject(thread_handle, INFINITE);
    GetExitCodeThread(thread_handle, &exit_code);
    retval = exit_code != 0;
  }
  while( 0 );
  
  // スレッドハンドルを閉じる。
  if( thread_handle )
  {
    CloseHandle(thread_handle);
    thread_handle = NULL;
  }
  
  // DLL 名用に割り当てたメモリを解放する。
  if( dll_name_vp )
  {
    VirtualFreeEx(i_process_handle, dll_name_vp, 0, MEM_RELEASE);
    dll_name_vp = NULL;
  }

  return retval;
}

 CreateRemoteThread() は他プロセスにスレッドを生成する API です。
HANDLE CreateRemoteThread(
  HANDLE hProcess,        // 新しいスレッドを稼働させるプロセスを識別するハンドル
  LPSECURITY_ATTRIBUTES lpThreadAttributes, // スレッドのセキュリティ属性へのポインタ
  DWORD dwStackSize,     // 初期のスタックサイズ (バイト数)
  LPTHREAD_START_ROUTINE lpStartAddress, // スレッド関数へのポインタ
  LPVOID lpParameter,     // 新しいスレッドの引数へのポインタ
  DWORD dwCreationFlags,  // 作成フラグ
  LPDWORD lpThreadId      // 取得したスレッド識別子へのポインタ
);

 LPTHREAD_START_ROUTINE は CreateThread() で指定するルーチンと同じもので、以下のような宣言となっています。
DWORD WINAPI ThreadProc(
  LPVOID lpParameter   // スレッドのデータ
);
 この ThreadProc() はポインタ型のパラメータを一つ受け取り、32bit の整数値を返す WINAPI(__stdcall) 規約の関数ということが分かります。

 そして、LoadLibraryA(), LoadLibraryW() はまさにこの宣言に合致する API です。
HMODULE WINAPI LoadLibrary(
  LPCTSTR lpFileName   // モジュールのファイル名
);

 つまり CreateRemoteThread() の lpStartAddress には LoadLibrary を、lpParameter に DLL パスを渡せば良いということになります。
 ただし、lpParameter に渡すポインタはスレッドを生成する他プロセス内のポインタとなります。ですので VirtualAllocEx() API でフック対象プロセス内にメモリを割り当て、そこに WriteProcessMemory() で DLL パスを書き込み、その値を lpParameter として渡します。

 というわけで実行結果。最初からフックされていることが確認できます。
実行結果