lsec096
- 关注
一、消息钩子
消息钩子(Hook)是Windows提供的一种拦截和处理消息/事件的机制,允许应用程序安装回调函数来监控系统和应用中的消息流量,在消息被目标窗口过程处理前/后对消息进行记录、修改或丢弃。由于这种特性,使得消息钩子在普通场景和恶意场景下都有广泛的应用。
普通场景:比如输入法监听键盘事件、UI自动化工具监听窗口消息来进行模拟点击、按键精灵等记录和播放操作序列等
恶意场景:比如键盘记录木马(keylogger)窃取密码、远控软件劫持鼠标操作、注入恶意代码到指定进程等
本文主要关注利用消息钩子注入代码的场景。
二、使用介绍
消息钩子是通过SetWindowsHookEx
和UnhookWindowsHookEx
这两个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的返回值
);
钩子范围
根据钩子的监控范围,可以分为全局钩子和线程钩子
全局钩子:
dwThreadId
填0
,监控同一桌面下的所有线程的消息钩子函数必须放到dll中,以便被加载到其他进程中
全局钩子影响范围大,使用时慎重
线程钩子:
dwThreadId
非0
,填目标线程的id
,仅监控目标线程的消息线程可以是本进程的,也可以是其他进程的
如果是其他进程的线程,钩子函数需要放到dll中,以便被加载到其他进程中
钩子类型
钩子类型指明了钩子可以监控哪些消息/事件,windows支持15种钩子类型:
注意:微软的文档中提到WH_JOURNALRECORD
,WH_JOURNALPLAYBACK
,WH_KEYBOARD_LL
,WH_MOUSE_LL
这几种钩子是在安装它们的线程上下文中执行,这意味着它们不会注入dll到目标进程。而WH_KEYBOARD
和WH_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注入后,会断下来,查看调用栈:
栈上目标进程
target.exe
中最后的代码是在调用GetMessage
,但下一层栈就到了KiUserCallbackDispatcher
,中间是内核的一些操作内核中获取到消息后,判断是否有对应的消息钩子,如果有,获取钩子所在的dll的路径(安装钩子时保存的),如果dll还没有加载,就主动回调用户层函数加载dll
内核回调用户代码,是通过
KiUserCallbackDispatcher
和一张内核回调表,KiUserCallbackDispatcher
是内核回调用户代码的入口,内核回调表是内核可以调用的函数列表,保存在user32.dll
中,在PEB
中可以查到表的地址(32位:PEB+0x20
,64位:PEB+0x58
)。内核将准备调用的函数的index
和参数传给KiUserCallbackDispatcher
,它去分发调用,调用完成后再通过NtCallbackReturn
返回内核。具体到上面加载dll的场景,内核指定调用的函数是__ClientLoadLibrary
,它的内部再调用LoadLibraryExW
加载dll,后续就和普通的dll加载过程一样了。dll加载完成后,回到内核,内核根据钩子的类型,选择user32中对应的钩子回调函数
__fnHk*
。user32
中的钩子回调函数可以看成是对用户钩子的封装,它里面再去调用用户的钩子函数,所以内核会将user32
回调函数的index
,以及用户钩子函数的地址一起传给KiUserCallbackDispatcher
。user32的内核回调函数表中和钩子相关的回调 用户钩子函数的调用栈
四、检测与对抗
主要介绍目标进程内的检测与对抗。
检测
感知dll加载的通用方法:hook
LoadLibraryExW
、LdrLoadDll
(事前),或者通过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
参考
[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
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)