本文为看雪论坛优秀文章
看雪论坛作者ID:深山修行之人
一种枚举系统热键的思路及代码实现(Win7&Win10)
前言
现在使用全局快捷键的软件越来越多,经常遇到快捷键被占用的情况,想找出被谁占用了,网上找了一些工具,不过都不支持新版本的Win10。于是自己写了一个(支持Win7到最新的Win10 2004 32/64位,XP就不考虑了),本文顺带记录一下分析思路。
思路分析
说明:Win7/8和Win10的热键数据结构基本一致,本文以Win10
1903
x64作为分析对象,重点是在Hash表的搜索方法上。
众所周知,注册热键需要调用RegisterHotKey,首先看一下函数原型:
BOOL
RegisterHotKey(
HWND hWnd,
int
id,
UINT fsModifiers,
UINT vk
);
- hWnd 窗口句柄
- id 热键ID
- fsModifiers 控制位,如Ctrl/Alt/Shift...
- vk 虚拟键码 Virtual Key Codes
简单跟一下RegisterHotKey函数,到ntdll!NtUserRegisterHotKey,看一下NtUserRegisterHotKey函数原型:
BOOL
APIENTRY NtUserRegisterHotKey(HWND hWnd,
int
id,
UINT fsModifiers,
UINT vk
);
可以看到Native的参数和上层API一致,往下则是Shadow SSDT,进入win32k,Win7/Win8/8.1还是win32k.sys,Win10已拆分成win32k/win32kfull/win32kbase三个模块,其中NtUserRegisterHotKey是win32kfull的导出函数。
打开IDA,定位到NtUserRegisterHotKey函数,其调用RegisterHotKey如下:
mov r9d, edi
mov dword ptr [rsp+48h+BugCheckParameter2], ebp ; BugCheckParameter2
mov r8d, r14d
xor edx, edx
mov rcx, rax ; struct tagWND
*
call _RegisterHotKey
IDA推导的参数显示有问题,跟一下参数来源,不难知道RegisterHotKey参数1是hWnd,参数2是NULL,参数3是id,参数4是fsModifiers,参数5是vk。
继续跟进RegisterHotKey,直接F5,有两段代码需要注意:
第一段很意图很明显,通过FindHotKey查找热键是否已经注册。
![979da1c854c705934cf7b6b2e921ddb7.png](https://i-blog.csdnimg.cn/blog_migrate/05ab62fadb573f40194c91a5448bc5ef.png)
第二段则是若HotKey未找到,则从Win32kPool中分配HotKey数据结构,并填充相应的字段,最后加入Hash表gphkHashTable中。接下来看看结构的填充方式以及Hash表的Index如何计算的。
![6605046062c5084a35c555e413a68ad7.png](https://i-blog.csdnimg.cn/blog_migrate/e0eb5b850be98a6150109d1086081e87.png)
v19则是HotKey结构体,对照RegisterHotKey参数,很容易分析出下面的偏移代表的字段:
v27
=
*(struct tagTHREADINFO
**)gptiCurrent;
...
*(_QWORD
*)v19
=
v27;
//
wndinfo
*(_DWORD
*)(v19
+
32)
=
v30;
//
id
*(_WORD
*)(v19
+
26)
=
v12 | v22;
//
modifiers2
*(_WORD
*)(v19
+
24)
=
v11;
//
modifiers1
*(_DWORD
*)(v19
+
28)
=
BugCheckParameter2;
//
这里是vk
*(_QWORD
*)(v19
+
8)
=
v29;
//
callback
v24
=
*(_BYTE
*)(v19
+
28) &
0x7F;
//
Hash表,0x80个Buckets,Index是vk取模7f
*(_QWORD
*)(v19
+
40)
=
gphkHashTable[v24];
//
将之前的节点插入单链表
gphkHashTable[v24]
=
(struct tagHOTKEY
*
near
*)v19;
//
将节点插入Hash表
最终可得到结构如下:
typedef struct _THREADINFO {
PETHREAD thread;
//..省略其它字段..
}
*PTHREADINFO;
typedef struct _WNDINFO {
HWND wnd;
//..省略其它字段..
}
*PWNDINFO;
typedef struct _HOT_KEY {
PTHREADINFO thdinfo;
PVOID callback;
PWNDINFO wndinfo;
UINT16 modifiers1;
//eg:MOD_CONTROL(0x0002)
UINT16 modifiers2;
//eg:MOD_NOREPEAT(0x4000)
UINT32 vk;
UINT32
id;
#ifdef _AMD64_
PADDING32 pad;
#endif
struct _HOT_KEY
*slist;
//..省略其它字段..
} HOT_KEY,
*
PHOT_KEY;
至此,热键数据结构已经分析清楚,本文目的是枚举热键,因此关键问题是如何定位gphkHashTable,通常能想到两种方式:
- 通过解析PDB符号定位,然而Windows符号服务器,它配拥有mirror吗。
- 通过代码特征码搜索,想到要兼容Win10各种版本头大。
思考片刻,想了一种搜索思路,既然gphkHashTable是全局Hash表,位于DATA段,0x80个Bucket,里面全是HotKey结构,那么可以校验HotKey的vk%0x7F得到Index来检查合法性。其次win32k的DATA段大小也比较合理,因此搜索范围也不大。如果能将表填充完,过滤出内核地址,再配合校验Hash表的HotKey的合法性,应该就能搜索到。
RegHotkey的代码都是一些内存结构运算,依赖少,因此可以注册0x80个vk从1到0x80的vk值来填满Hash表,如果注册成功就记录,枚举完后再取消注册,做清理工作。
如果找到Hash表,删除热键就很简单了,找到对应的HotKey,常规摘单链节点的操作即可。
代码及实现
由于代码全在内核层实现(WDK7601),并且调用者不是GUI线程、Win7注册热键的函数未导出,因此调用RegHotKey会繁琐点,整体流程如下:
- 找到当前session的csrss,插内核APC,实现切换到GUI线程。
- 遍历找win32k模块基址(Win7 win32k.sys,Win10 win32kfull.sys)
- 解析得到.data段地址区域
- 获取NtUser*Hotkey函数地址,Win7从Shadow SSDT表中查,Win10从win32kfull.sys导出表中获取。
- 注册热键,1~0x80的vk值,记录注册成功的值。
- 搜索data段,首先过滤内核地址,搜索满足条件的0x80个区域,再校验是否满足HotKey Hash表条件。
- 取消注册成功的热键。
- 递归枚举热键列表,解析对应结构:hWnd、hk、id、fsModifiers。
下面分段解析,由于热键操作必须访问win32k session空间,而且ThreadInfo还必须存在,因此光Attach到GUI进程是不行的,最简单的方式就插APC到GUI线程。
NTSTATUS DriverEntry(PDRIVER_OBJECT drvobj, PUNICODE_STRING registry)
{
NTSTATUS status;
UNREFERENCED_PARAMETER(registry);
KdPrint(("OpsHotkey Running..."));
DoEnumHotkeys();
return
STATUS_SUCCESS;
}
NTSTATUS DoEnumHotkeys()
{
NTSTATUS Status;
PETHREAD Thread;
PKAPC Apc
=
NULL;
BOOLEAN Inserted;
//
获取csrss进程ID
ULONG csrss_pid
=
GetSessionProcessId();
//
得到进程第一个线程
Status
=
GetProcessFirstThread((ULONG)csrss_pid, &Thread);
if
(!NT_SUCCESS(Status)) {
return
Status;
}
//
分配内存
Apc
=
(PKAPC)ExAllocatePoolWithTag(NonPagedPool, sizeof(KAPC),
'cpak');
if
(Apc
==
NULL) {
ObDereferenceObject(Thread);
return
STATUS_UNSUCCESSFUL;
}
//
初始化APC对象
KeInitializeApc(Apc,
(PKTHREAD)Thread,
OriginalApcEnvironment,
&KernelApcRoutine,
NULL,
DoEnumHotkeysApc,
KernelMode,
NULL);
//
插APC
Inserted
=
KeInsertQueueApc(Apc, NULL, NULL, IO_NO_INCREMENT);
if
(Inserted) {
KdPrint(("[%s] KeInsertQueueApc ok.", __FUNCTION__));
}
else
{
KdPrint(("[%s] KeInsertQueueApc failed.", __FUNCTION__));
ExFreePool(Apc);
}
ObDereferenceObject(Thread);
return
Status;
}
接着获取NtUser*Hotkey相关函数,注册热键填充Hash表后,后面就可以开始搜索搜索Hash表了,HotKey是由win32k从NonPagedPool中分配,因此用MmIsAddressValid检查无须担心换页问题,具体代码如下:
BOOLEAN SearchHotkeyTable(PUCHAR*
&htable)
{
htable
=
NULL;
//找到Hash表位于的模块基址(Win7 win32k.sys,Win10 win32kfull.sys)
PUCHAR win32k;
ULONG win32ksize
=
0;
RTL_OSVERSIONINFOEXW info;
OsGetVersionInfo(info);
if
(info.dwMajorVersion
==
10) {
win32k
=
(PUCHAR)GetSystemModuleBase("win32kfull.sys", &win32ksize);
}
else
{
win32k
=
(PUCHAR)GetSystemModuleBase("win32k.sys", &win32ksize);
}
if
(!win32k) {
return
FALSE;
}
KdPrint(("win32k:%p, win32ksize:%xn", win32k, win32ksize));
//得到.data段区域(全局Hash表所在区域)
NTSTATUS status;
PUCHAR start;
ULONG size;
status
=
GetSectionRegion(win32k,
".data", start, size);
if
(!NT_SUCCESS(status)) {
return
FALSE;
}
KdPrint(("win32k-data start:%p, size:%xn", start, size));
//注册一遍热键,为了填充Hash表
__NtUserRegisterHotKey pNtUserRegisterHotKey
=
NULL;
__NtUserUnregisterHotKey pNtUserUnregisterHotKey
=
NULL;
if
(!GetHotkeyFunctions(win32k, pNtUserRegisterHotKey, pNtUserUnregisterHotKey)) {
return
FALSE;
}
int
hkmarks[MAX_VK]
=
{
0
};
for
(int
i
=
1; i <=
MAX_VK; i++) {
if
(pNtUserRegisterHotKey(NULL, ~i, MOD_ALT | MOD_NOREPEAT, i)) {
hkmarks[i]
=
~i;
}
}
//开始搜索Hash表
PUCHAR
*ptr
=
(PUCHAR*)start;
for
(int
i
=
0, j
=
0; i < size/sizeof(ptr); i++) {
if
(j
==
0x80) {
//得到起始位置
i
-=
j;
//校验特定Hotkey
INT
vks[]
=
{
5,
10
,15,
20,
25,
30,
35,
40,
45};
for
(INT
ck
=
0; ck < RTL_NUMBER_OF_V2(vks); ck++) {
INT
vk
=
vks[ck];
if
(!CheckHotkeyValid(ptr[i
+
vk], vk)) {
j
=
0;
break;
}
}
//找到HashTable
if
(j !=
0) {
htable
=
&ptr[i];
break;
}
continue;
}
//初步过滤内核地址
if
(ptr[i] > MmSystemRangeStart) {
j++;
continue;
}
j
=
0;
}
//取消注册成功的热键
for
(int
i
=
1; i <=
MAX_VK; i++) {
if
(hkmarks[i]) {
pNtUserUnregisterHotKey(NULL, hkmarks[i]);
}
}
return
1;
}
递归解析Hash表,Dump出系统热键,代码如下所示:
VOID DumpHotkeyNode(PHOT_KEY hk)
{
//
链表下一个节点存在
if
(MmIsAddressValid(hk->slist)) {
//
递归调用
DumpHotkeyNode(hk->slist);
}
PETHREAD thread
=
hk->thdinfo->thread;
PEPROCESS process
=
NULL;
HANDLE pid
=
NULL;
HANDLE tid
=
NULL;
if
(thread !=
NULL) {
process
=
IoThreadToProcess(thread);
pid
=
PsGetProcessId(process);
tid
=
PsGetThreadId(thread);
}
HWND wnd
=
NULL;
if
(hk->wndinfo && MmIsAddressValid(hk->wndinfo))
wnd
=
hk->wndinfo->wnd;
//
Dump系统热键
DbgPrint("HK:%x NAME:%s PROCESS:%d THREAD:%d HWND:%x MOD:%d VK:%d n",
hk, PsGetProcessImageFileName(process), pid, tid, wnd, hk->modifiers1, hk->vk);
}
VOID DumpHotkeyTable(PUCHAR*
table)
{
//
遍历Hash表
for
(INT
i
=
0; i <
0x7f; i++)
{
PHOT_KEY hk
=
(PHOT_KEY)table[i];
if
(hk)
DumpHotkeyNode(hk);
}
}
结束语
注意:发现输入法的快捷键没有注册到系统热键中,应该是自己管理的,因此这种方式不能被检测到,如果有快捷键占用而没找到,则优先检查输入法设置。
驱动效果如下图所示:
![42ab36de3aebedfbdebbef1d4767bd33.png](https://i-blog.csdnimg.cn/blog_migrate/254316b80c9a24957bc780186791dd6f.jpeg)
图形化工具下载地址:https://github.com/BlackINT3/OpenArk,点击内核--进入内核模式--查看系统热键:
![7c7608b282ba95df0818ace558449462.png](https://i-blog.csdnimg.cn/blog_migrate/f052630227e929557274de9e9d4391af.jpeg)
Thanks for reading...
本文未考虑XP,感兴趣的可以参看其它同学的文章。
- 《Windows热键注册》
- 《在内核里查看所有的快捷键》