freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

代码注入之消息钩子注入
lsec096 2025-04-02 23:17:59 10793
所属地 广东省

一、消息钩子

消息钩子(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种钩子类型:

钩子类型

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

This hook iscalled in the context of the thread that installed it. The call is made by sending a messageto the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.[1]

This hook may becalled in the context of the thread that installed it. The call is made by sending a messagto the thread that installed the hook. Therefore, the thread that installed the hook must have a message loop.[2]

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], nullptr, 10);
  DWORD targetTid = strtoul(argv[2], nullptr, 10);

  // 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, NULL, 0, 0)) {
    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;
}

注入成功

三、内部原理

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
    内核回调表中和钩子相关的部分回调

    user32的内核回调函数表中和钩子相关的回调

    用户钩子函数的调用栈

    用户钩子函数的调用栈

四、检测与对抗

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

检测

  • 感知dll加载的通用方法:hook LoadLibraryExWLdrLoadDll(事前),或者通过LdrRegisterDllNotification注册dll加载通知(事中)

  • 针对消息钩子:hook __ClientLoadLibrary(事前),inline hook或者hook回调表中的指针,可通过PEB找到内核回调表KernelCallbackTable,然后根据index找到__ClientLoadLibrary,但是要注意不同系统版本上,index可能不同

  • 模块扫描(事后)

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

对抗

  • 主动退出进程:事前/事中/事后,检测到加载非白名单dll或无签名dll时

  • 静默处理:使dll加载失败,比如禁止使用消息钩子加载dll,在__ClientLoadLibraryhook中跳过对LoadLibraryExW的调用,注意__ClientLoadLibrary中有一些额外操作,而且它内部会调用NtCallbackReturn返回内核,没有通过KiUserCallbackDispatcher,所以不能直接return

参考

[1] LowLevelKeyboardProc function. https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc#remarks

[2] MouseProc function. https://learn.microsoft.com/en-us/windows/win32/winmsg/mouseproc#remarks

# 代码注入
本文为 lsec096 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
lsec096 LV.2
这家伙太懒了,还未填写个人描述!
  • 4 文章数
  • 0 关注者
签名不等于可信:详解PE数字签名校验的漏洞与主动规避方案
2025-04-10
代码注入之CreateProcess注入
2025-04-03
代码注入之Hook注入
2025-04-03
文章目录