代码注入之消息钩子注入

64 阅读7分钟

一、消息钩子

消息钩子(Hook)是Windows提供的一种拦截和处理消息/事件的机制,允许应用程序安装回调函数来监控系统和应用中的消息流量,在消息被目标窗口过程处理前/后对消息进行记录、修改或丢弃。由于这种特性,使得消息钩子在普通场景和恶意场景下都有广泛的应用。

  • 普通场景:比如输入法监听键盘事件、UI自动化工具监听窗口消息来进行模拟点击、按键精灵等记录和播放操作序列等
  • 恶意场景:比如键盘记录木马(keylogger)窃取密码、远控软件劫持鼠标操作、注入恶意代码到指定进程等

本文主要关注利用消息钩子注入代码的场景。

二、使用介绍

消息钩子是通过SetWindowsHookExUnhookWindowsHookEx这两个API来安装和卸载的

// hook回调的原型  
typedef LRESULT (CALLBACK* HOOKPROC)(int nCode, WPARAM wParam, LPARAM lParam);  
  
// 安装hook  
HHOOK SetWindowsHookExW(  
  [in] int       idHook,    // hook类型  
  [in] HOOKPROC  lpfn,      // hook回调  
  [in] HINSTANCE hmod,      // hook回调所在模块的句柄,如果dwThreadId是本进程的线程,填NULL  
  [in] DWORD     dwThreadId // hook的目标线程  
);  
  
// 卸载hook  
BOOL UnhookWindowsHookEx(  
  [in] HHOOK hhk  // SetWindowsHookExW的返回值  
);

钩子范围

根据钩子的监控范围,可以分为全局钩子和线程钩子

全局钩子:

  • dwThreadId0,监控同一桌面下的所有线程的消息
  • 钩子函数必须放到dll中,以便被加载到其他进程中
  • 全局钩子影响范围大,使用时慎重

线程钩子:

  • dwThreadId0,填目标线程的id,仅监控目标线程的消息
  • 线程可以是本进程的,也可以是其他进程的
  • 如果是其他进程的线程,钩子函数需要放到dll中,以便被加载到其他进程中

钩子类型

钩子类型指明了钩子可以监控哪些消息/事件,windows支持15种钩子类型:

image.png

注意: 微软的文档中提到WH_JOURNALRECORDWH_JOURNALPLAYBACKWH_KEYBOARD_LLWH_MOUSE_LL这几种钩子是在安装它们的线程上下文中执行,这意味着它们不会注入dll到目标进程。而WH_KEYBOARDWH_MOUSE可能(注意是可能)会在安装钩子的线程中执行,但没指出什么条件下会,实测是能够注入dll到第三方进程,可能没触发对应的条件,如果实测过程中遇到注入失败的情况,可以留意下是否是这种情况。

This hook is called in the context of the thread that installed it. The call is made by sending a message to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop. ( learn.microsoft.com/en-us/windo…)

This hook may be called in the context of the thread that installed it. The call is made by sending a messag to the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop. (learn.microsoft.com/en-us/windo…)

Hook链

每种钩子类型都可以安装多个Hook,它们组成一个钩子链,后安装钩子最先被调用。全局钩子和线程钩子保存在两个链中,系统会先执行线程钩子,再执行全局钩子。为了不影响其他钩子的执行,一般在钩子函数中会调用CallNextHookEx函数执行下一个钩子,时机可以是我们的钩子开始时或结束时。

LRESULT CallNextHookEx(
  [in, optional] HHOOK  hhk,    // 忽略,填NULL;后续参数为HookProc的参数
  [in]           int    nCode,
  [in]           WPARAM wParam,
  [in]           LPARAM lParam
);

示例代码

Injector.cpp,编译成Injector.exe:

// Usage: Injector.exe <idHook> <targetTid>
int main(int argc, char **argv) {
  LOG_INFO("Injector starts, pid=%d"GetCurrentProcessId());

  // 1. 读取hook类型和目标进程id
  int idHook = strtol(argv[1], nullptr10);
  DWORD targetTid = strtoul(argv[2], nullptr10);

  // 2. 加载hook DLL
  HMODULE hDll = LoadLibraryW(L"WindowsHookDll.dll");
  if (!hDll) {
    LOG_ERR("Load hook dll failed: %d"GetLastError());
    return 1;
  }
  LOG_INFO("Load hook dll success: %p", hDll);

  // 3. 获取导出函数地址
  auto pInstallHook = (P_InstallHook)GetProcAddress(hDll, "InstallHook");
  auto pUninstallHook = (P_UninstallHook)GetProcAddress(hDll, "UninstallHook");
  if (pInstallHook == nullptr || pUninstallHook == nullptr) {
    LOG_ERR("Install and Uninstall are not found");
    FreeLibrary(hDll);
    return 2;
  }

  // 4. 安装钩子
  pInstallHook(idHook, targetTid);

  // 5. 消息循环可选,看钩子类型,比如WH_KEYBOARD_LL在injector中执行,需要消息循环
  MSG msg;
  while (GetMessage(&msg, NULL00)) {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }

  // 6. 卸载钩子
  pUninstallHook();
  FreeLibrary(hDll);

  return 0;
}

WindowsHookDll.cpp,编译成WindowsHookDll.dll:

// WH_CBT
LRESULT CALLBACK CbtProc(int nCode, WPARAM wParam, LPARAM lParam) {
  LOG_INFO("%s: nCode=%d", __FUNCTION__, nCode);
  return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
// 其他钩子函数类似CbtProc,省略

// 钩子函数列表
HOOKPROC gHookProcList[] = {
    JournalRecordProc,    // WH_JOURNALRECORD 0
    JournalPlaybackProc,  // WH_JOURNALPLAYBACK 1
    KeyboardProc,         // WH_KEYBOARD 2
    GetMsgProc,           // WH_GETMESSAGE 3
    CallWndProc,          // WH_CALLWNDPROC 4
    CbtProc,              // WH_CBT 5
    SysMsgProc,           // WH_SYSMSGFILTER 6
    MouseProc,            // WH_MOUSE 7
    nullptr,              // WH_HARDWARE 8
    DebugProc,            // WH_DEBUG 9
    ShellProc,            // WH_SHELL 10
    ForegroundIdleProc,   // WH_FOREGROUNDIDLE 11
    CallWndRetProc,       // WH_CALLWNDPROCRET 12
    LowLevelKeyboardProc, // WH_KEYBOARD_LL 13
    LowLevelMouseProc,    // WH_MOUSE_LL 14
    MessageProc,          // WH_MSGFILTER (-1)
};

// hook id 转 proc
#define ID_TO_HOOK_PROC(idHook) gHookProcList[(uint32_t)(idHook) & 0x0000000f]

EXTERN_C IMAGE_DOS_HEADER __ImageBase;
HHOOK gHHook = nullptr// 钩子句柄

// 安装钩子
extern "C" __declspec(dllexport) void InstallHook(int idHook, DWORD targetTid) {
  gHHook = SetWindowsHookEx(idHook, ID_TO_HOOK_PROC(idHook), (HINSTANCE)&__ImageBase, targetTid);
  if (gHHook) {
    LOG_INFO("Install hook success, hHook=%p", gHHook);
  } else {
    LOG_INFO("Install hook failed: %d"GetLastError());
  }
}

// 卸载钩子
extern "C" __declspec(dllexport) void UninstallHook() {
  if (gHHook) {
    UnhookWindowsHookEx(gHHook);
    LOG_INFO("Unhook OK");
    gHHook = NULL;
  }
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
  if (reason == DLL_PROCESS_ATTACH || reason == DLL_PROCESS_DETACH) {
    char path[MAX_PATH] = "";
    GetModuleFileNameA(nullptr, path, MAX_PATH);
    if (reason == DLL_PROCESS_ATTACH) {
      LOG_INFO("Hook dll attach to process %d, %s"GetCurrentProcessId(), path);
    } else {
      LOG_INFO("Hook dll detach from process %d, %s"GetCurrentProcessId(), path);
    }
  }
  return TRUE;
}

image.png

三、内部原理

x64dbg中在目标进程中LoadLibraryExW下断点,dll注入后,会断下来,查看调用栈:

dll加载的调用栈

  1. 栈上目标进程target.exe中最后的代码是在调用GetMessage,但下一层栈就到了KiUserCallbackDispatcher,中间是内核的一些操作
  2. 内核中获取到消息后,判断是否有对应的消息钩子,如果有,获取钩子所在的dll的路径(安装钩子时保存的),如果dll还没有加载,就主动回调用户层函数加载dll
  3. 内核回调用户代码,是通过KiUserCallbackDispatcher和一张内核回调表KiUserCallbackDispatcher是内核回调用户代码的入口,内核回调表是内核可以调用的函数列表,保存在user32.dll中,在PEB中可以查到表的地址(32位:PEB+0x20,64位:PEB+0x58)。内核将准备调用的函数的index和参数传给KiUserCallbackDispatcher,它去分发调用,调用完成后再通过NtCallbackReturn返回内核。具体到上面加载dll的场景,内核指定调用的函数是__ClientLoadLibrary,它的内部再调用LoadLibraryExW加载dll,后续就和普通的dll加载过程一样了。 image.png
  4. dll加载完成后,回到内核,内核根据钩子的类型,选择user32中对应的钩子回调函数__fnHk*user32中的钩子回调函数可以看成是对用户钩子的封装,它里面再去调用用户的钩子函数,所以内核会将user32回调函数的index,以及用户钩子函数的地址一起传给KiUserCallbackDispatcher内核回调表中和钩子相关的部分回调 用户钩子函数的调用栈

四、检测与对抗

主要介绍目标进程内的检测与对抗。

检测

  • 感知dll加载的通用方法:hook LoadLibraryExWLdrLoadDll(事前),或者通过LdrRegisterDllNotification注册dll加载通知(事中)
  • 针对消息钩子:hook __ClientLoadLibrary(事前),inline hook或者hook回调表中的指针,可通过PEB找到内核回调表KernelCallbackTable,然后根据index找到__ClientLoadLibrary,但是要注意不同系统版本上,index可能不同
  • 模块扫描(事后)

事后模块和文件可能会被隐藏,也可能dll不会常驻(比如dll只是用来释放和执行shellcode,完成后就自动卸载了),增加对抗难度,优先考虑事前或事中。

对抗

  • 主动退出进程:事前/事中/事后,检测到加载非白名单dll或无签名dll时
  • 静默处理:使dll加载失败,比如禁止使用消息钩子加载dll,在__ClientLoadLibrary hook中跳过对LoadLibraryExW的调用,注意__ClientLoadLibrary中有一些额外操作,而且它内部会调用NtCallbackReturn返回内核,没有通过KiUserCallbackDispatcher,所以不能直接return