Windows内核新手上路3——挂钩KeUserModeCallBack
1. 简介
在Windows系统中,提供了几种方式从R0调用位于R3的函数,其中一种方式是KeUserModeCallBack,此函数流程如下:
nt!KeUserModeCallback->nt!KiCallUserMode->nt!KiServiceExit->ntdll!KiUserCallbackDispatcher->回调函数-> int2B->nt!KiCallbackReturn-> nt!KeUserModeCallback(调用后)这是一个 ring0->ring3->ring0的过程,在堆栈准备完毕后,借用KiServiceExit的力量回到了ring3,它的着陆点是 KiUserCallbackDispatcher,然后KiUserCallbackDispatcher从PEB中取出 KernelCallbackTable的基址,再以ApiIndex作为索引在这个表中查找对应的回调函数并调用,调用完之后再int2B触发 nt!KiCallbackReturn再次进入内核,修正堆栈后跳回KeUserModeCallback,完成调用。
系统所有的消息钩子回调都是利用KeUserModeCallBack完成的。所以可以通过挂钩KeUserModeCallback用来过滤对钩子的调用。
1.1 inline hook
KeUserModeCallback没有对应的R3调用接口,所以没有在SSDT SHADOW中出现,需要用另外的方式来HOOK,可以采用的Inline Hook,即在函数头部加入一条JMP指令(机器码E9)跳入代理函数,然后过滤之后决定是否调用真实的KeUserModeCallback函数。挂钩过程如下:
ULONG StartHookKeyUserModeCallBack() { ULONG tmp; memset (Ori_Func, 0x90, 100); // nop Ori_Func[50] = 0xE9; tmp = (ULONG)ProxyFunc - (ULONG)KeUserModeCallback - 5; memcpy(jmp_bytes+1, &tmp, 4); HeadLen = GetPatchSize (KeUserModeCallback, 5); memcpy(Ori_Func, (PVOID)KeUserModeCallback, HeadLen); //原始字节 //中间跳 tmp = (ULONG)KeUserModeCallback + HeadLen - (ULONG)(&Ori_Func[50]) - 5; memcpy(&Ori_Func[51], &tmp, 4); // 去掉内存保护 __asm { cli mov eax, cr0 and eax, not 10000h mov cr0, eax } memcpy(KeUserModeCallback, jmp_bytes, 5); // 恢复内存保护 __asm { mov eax, cr0 or eax, 10000h mov cr0, eax sti } return TRUE; } |
代码1 挂钩KeUserModeCallback
代理函数ProxyFunc代码如代码2:
__declspec(naked) VOID ProxyFunc(VOID) { __asm { MOV EAX, ESP PUSHAD
PUSH [EAX+4*5] PUSH [EAX+4*4] PUSH [EAX+4*3] PUSH [EAX+4*2] PUSH [EAX+4] LEA EAX, MyKeUserModeCallback CALL EAX
TEST EAX, EAX JZ continue_exe //return STATUS_SUCCESS
POPAD RETN 0x14
continue_exe: POPAD LEA EAX, Ori_Func //跳回原始函数 JMP EAX } } |
代码2 代理函数ProxyFunc
首先调用自定义的过滤函数MyKeUserModeCallback,然后判断返回值是否是STATUS_SUCCESS,如果是,则调用真实的KeUserModeCallback,否则直接返回,拒绝调用R3的回调用函数。
1.2 MyKeUserModeCallback
下面分部分讲解MyKeUserModeCallback对钩子的过滤操作,其中涉及到很多未公开技术,其代码通过逆向工程技术得到。
1.2.1 过滤WH_KEYBOARD_LL类型钩子
在Windows操作系统中,按键消息到达具体的窗口消息队列之前会调用WH_KEYBOARD_LL钩子函数,可以利用此类钩子截取键盘输入。伪代码如代码3所示。
判断ApiNumber是否是KBD_LL_HOOK_API_NUM 将InputBuffer转换为PFNHKINLPKBDLLHOOKSTRUCTMSG 填充SYS_EVENT_STRUCT结构,通知R3控制程序作出判断 如果用户允许该调用则返回STATUS_SUCCESS 否则返回STATUS_UNSUCCESSFUL |
伪代码3 WH_KEYBOARD_LL过滤
在对WH_KEYBOARD_LL的过滤过程中,还有一点需要做特殊处理。现在有很多的软件采用WH_KEYBOARD_LL钩子来做安全输入控件,而不是通过传统的EDIT控件,所以在返回的时候需要将虚拟键盘产生的虚假的按键修正为真实的按键。
1.2.2 过滤WH_KEYBOARD钩子
在Windows操作系统中,按键消息到达具体的窗口消息队列之前会调用WH_KEYBOARD钩子函数,可以利用此类钩子截取键盘输入。过滤的伪代码如代码4所示。
判断ApiNumber是否是KBD_HOOK_API_NUM 将InputBuffer转为PFNHKINLPKBDLLHOOKSTRUCTMSG结构 判断PFNHKINLPKBDLLHOOKSTRUCTMSG结构中GeneralHookHeader成员的nCode成员的值是否为0x20000 如果是则是WH_KEYBOARD钩子 填充SYS_EVENT_STRUCT结构,通知R3控制程序作出判断 如果用户允许该调用则返回STATUS_SUCCESS 否则返回STATUS_UNSUCCESSFUL
|
伪代码4 WH_KEYBOARD钩子过滤
WH_KEYBOARD钩子会同其他一些回调共用一个ApiNumber,这里要做进一步的判断,而且全局的WH_KEYBOARD钩子需要在其他进程中LoadLibrary。
同WH_KEYBOARD_LL钩子,有的程序会利用WH_KEYBOARD钩子做安全输入控件,在返回时需要将虚拟键盘产生的虚假按键修正为真实的按键。
1.2.3 WH_DEBUG钩子过滤
在Windows操作系统中,调用其他类型的钩子函数之前,会首先调用WH_DEBUG钩子函数,用于钩子调试。恶意程序利用WH_DEBUG类型钩子可以获取键盘输入。过滤伪代码如代码5所示。
判断ApiNumber是否是DEBUG_HOOK_API_NUM 判断此DEBUG信息中的钩子类型是否包含键盘输入信息 如果包含,则填充SYS_EVENT_STRUCT结构,通知R3控制程序作出判断 如果用户允许该调用则返回STATUS_SUCCESS 否则返回STATUS_UNSUCCESSFUL
|
伪代码5 WH_DEBUG钩子过滤
同WH_KEYBOARD_LL WH_KEYBOARD类型的钩子,有些程序会使用WH_DEBUG钩子实现安全输入控件,在返回时,需要将虚拟键盘产生的虚假按键修正为真实的按键。
1.2.4 Ke_LoadLibrary过滤
在Windows操作系统中,除了WH_KEYBOARD_LL钩子外,其他类型的全局消息钩子都需要有DLL被加载进入目标进程(WH_KEYBOARD_LL钩子只是一次线程切换,详细信息可以查看MSDN)。所以过滤Ke_LoadLibrary可以阻止大部分的消息钩子(事实上,目前所有的密码保护程序都通过此方法过滤消息钩子,用来保护密码,但是此方法对WH_KEYBOARD_LL类型的钩子无效)。过滤伪代码如代码6所示。
判断ApiNumber是否LOAD_LIBRARY_API_NUM 如果是,获取参数信息(包括DLL路径) 填充SYS_EVENT_STRUCT结构,通知R3控制程序作出判断 如果用户允许该调用则返回STATUS_SUCCESS 否则返回STATUS_UNSUCCESSFUL |
伪代码6 Ke_LoadLibrary过滤
1.2.5 WH_JOURNALRECORD过滤
在Windows操作系统中,WH_JOURNALRECORD设计的初衷是做消息的记录和回放,但是恶意程序利用此类型的钩子可以获取键盘输入消息,从而达到记录密码的目的。过滤伪代码如代码7所示。
判断ApiNumber是否EVENT_MSG_HOOK_API_NUM 进一步判断消息类型是否是按键消息 填充SYS_EVENT_STRUCT结构,通知R3控制程序作出判断 如果用户允许该调用则返回STATUS_SUCCESS 否则返回STATUS_UNSUCCESSFUL |
伪代码7 WH_JOURNALRECORD过滤