DLL 注入之 Windows 消息钩子

Windows 下的窗口应用程序是基于事件驱动方式工作的,操作系统中点击鼠标和按下键盘都是一种事件,当事件发生时操作系统会将消息发送给相应的应用程序,应用程序收到消息之后会做出响应。

钩子(Hook),是Windows提供的一种截获和监视系统中消息的方法,应用程序可以通过 SetWindowsHook 函数设置钩子以监视指定窗口的某种消息,而且所监视的窗口可以是其他进程所创建的。当消息到达后,在目标窗口处理函数之前处理它。

0x01 钩子原理

操作系统维护着一个链表进行钩子的管理,每设置一个钩子就在钩链中增加一个节点,最新设定的钩子将会最早获得消息的控制权。此外,每个钩子需要设定一个回调函数(钩子函数),在产生指定消息后作出处理。当指定消息发生时,系统会调用这些回调函数。在回调函数中可以监视消息、修改消息,或者屏蔽消息,使消息无法传递到目的窗口。

根据钩子的范围可分为全局钩子和局部钩子,全局钩子可以钩取所有基于消息机制的应用程序,局部钩子只是钩取指定线程的消息。全局钩子将钩子函数放在一个 DLL 中,当某个进程产生指定消息之后,操作系统会自动将该 DLL 注入到该进程中。

常用钩子类型有以下几种:
(1)键盘钩子和低级键盘钩子可以监视各种键盘消息。
(2)鼠标钩子和低级鼠标钩子可以监视各种鼠标消息。
(3)外壳钩子可以监视各种Shell事件消息。比如启动和关闭应用程序。
(4)日志钩子可以记录从系统消息队列中取出的各种事件消息。
(5)窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。

Windows 提供消息钩子相关的 API 主要有 SetWindowsHookEx()、CallNextHookEx() 和 UnhookWindowsHookEx()。

0x02 键盘钩子

键盘记录器是恶意代码中常见的一种类型,木马编写者通常以隐蔽的方式将键盘记录器安装在目标主机以窃取登录凭证等敏感信息。通过消息钩子可以实现一个键盘记录器,但是这种方法极容易被杀毒软件发现。下面通过一个简单的例子演示全局键盘钩子。

1. 安装与卸载钩子

由于是全局消息钩子,所以需要将消息钩子的安装与卸载放在 DLL 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifdef __cplusplus // If used by C++ code,
extern "C" { // export the C interface
#endif
_declspec(dllexport) void InstallHook()
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hModule, 0);
}
_declspec(dllexport) void UninstallHook()
{
if (g_hHook)
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL;
}
}
#ifdef __cplusplus
}
#endif

SetWindowsHookEx 用于安装消息钩子,该函数第二个参数为钩取消息后系统调用的回调函数,函数的返回值为钩子句柄。函数原型如下:

1
2
3
4
5
6
HHOOK WINAPI SetWindowsHookExW(
_In_ int idHook, // type of hook, WH_KEYBOARD is Keyboard hook
_In_ HOOKPROC lpfn, // hook procedure
_In_opt_ HINSTANCE hmod, // handle of hook's DLL
_In_ DWORD dwThreadId // thread ID,0 means global hook
);

UnhookWindowsHookEx 用于卸载消息钩子,它只有一个参数,即需要卸载消息钩子的句柄。

在 DLL 中要将该函数导出供主程序使用,_declspec(dllexport)声明 InstallHook() 和 UninstallHook() 为导出函数。

2. 钩子函数

全局键盘消息钩子会截获所有应用程序的键盘消息,包括系统的控制台程序,为了方便操作,若目标程序为控制台程序(conhost.exe)则直接将消息传递给它;否则当有键盘按下都会弹出消息窗口,并显示按下的按键。具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if(nCode >= 0)
{
// bit 31 : transition state. 0 => press, 1 => release
if (!(lParam & 0x80000000))
{
char tcKey[1000] = { 0 };
char tcPath[MAX_PATH] = { 0 };
char *name = NULL;
GetKeyNameTextA(lParam, tcKey, 50); // Retrieves a string that represents the name of a key
GetModuleFileNameA(NULL, tcPath, MAX_PATH);
name = strrchr(tcPath,'\\') + 1 ;
if(!strcmp(name,"conhost.exe")) // Console Host Process
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
else
MessageBoxA(NULL, tcKey, name, MB_OK);
}
}
else
return CallNextHookEx(g_hHook, nCode, wParam, lParam);
}

GetKeyNameTextA 用于获取按键名字符串。

1
2
3
4
5
int WINAPI GetKeyNameText(
_In_ LONG lParam,
_Out_ LPTSTR lpString, // buffer to receive the key name
_In_ int cchSize // The maximum of the key name
);

CallNextHookEx 将消息继续传递给钩子链中下一个钩子函数,直到目标窗口。

1
2
3
4
5
6
LRESULT WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk, // handle of hook
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);

3. 测试

以上即为消息钩子相关的函数,下面调用这些函数测试键盘钩子的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include "stdio.h"
#include "windows.h"
typedef void(*funptr)();
void main()
{
HMODULE hDll = NULL;
funptr InstallHook = NULL;
funptr UninstallHook = NULL;
char cmd[10]={0};
printf("----------------------Command-----------------------\n\n");
printf("[+] install : Install hook\n");
printf("[+] uninstall : Uninstall hook\n");
printf("----------------------------------------------------\n\n");
while(1)
{
gets(cmd);
if(!strcmp(cmd ,"install"))
{
hDll = LoadLibraryA("keyhook.dll");
if (hDll == NULL)
{
printf("LoadLibrary Fail!\n");
return;
}
InstallHook = (funptr)GetProcAddress(hDll, "InstallHook");
UninstallHook = (funptr)GetProcAddress(hDll, "UninstallHook");
InstallHook();
printf("Keyboard hook installed!\n\n");
}
if(!strcmp(cmd, "uninstall"))
{
UninstallHook();
FreeLibrary(hDll);
printf("Sucess to uninstall hook!\n\n");
}
}
}

在记事本中按下按键,弹出按键值。

查看记事本进程模块,可以看到 DLL 已成功注入该进程。

0x03 调试

使用 OllyDbg 可以调试注入到目标进程中的 DLL 文件,具体步骤如下:

1.运行 notepad.exe,使用 OD attach 运行中的 notepad;
2.选项/ 调试选项/ 事件/ 中断于新模块(dll);
3.运行 Hook.exe,安装全局消息钩子;
4.在 notepad 中使用键盘输入,keyhook.dll 被注入到 notepad 中;
5.OD 暂停调试,并弹出 Executable modules 窗口;
6.取消之前设置的 “ 中断于新模块(dll)” ,双击 keyhook.dll 即可到达其 EP 地址处。


References:
[1] 逆向工程核心原理
[2] Windows API 教程(七)hook 钩子监听
[3] DLL注入浅析(上)