判断字段是否与枚举相同_一种枚举系统热键的思路及代码实现(Win7&Win10)...

本文为看雪论坛优秀文章

看雪论坛作者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, edimov dword ptr [rsp+48h+BugCheckParameter2], ebp ; BugCheckParameter2mov r8d, r14dxor edx, edxmov rcx, rax ; struct tagWND *call _RegisterHotKey

IDA推导的参数显示有问题,跟一下参数来源,不难知道RegisterHotKey参数1是hWnd,参数2是NULL,参数3是id,参数4是fsModifiers,参数5是vk。

继续跟进RegisterHotKey,直接F5,有两段代码需要注意:

第一段很意图很明显,通过FindHotKey查找热键是否已经注册。

979da1c854c705934cf7b6b2e921ddb7.png

第二段则是若HotKey未找到,则从Win32kPool中分配HotKey数据结构,并填充相应的字段,最后加入Hash表gphkHashTable中。接下来看看结构的填充方式以及Hash表的Index如何计算的。

6605046062c5084a35c555e413a68ad7.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; // callbackv24 = *(_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;#endifstruct _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进程IDULONG 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);// 插APCInserted = 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;//校验特定HotkeyINT 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;}}//找到HashTableif (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://github.com/BlackINT3/OpenArk,点击内核--进入内核模式--查看系统热键:

7c7608b282ba95df0818ace558449462.png

Thanks for reading...

本文未考虑XP,感兴趣的可以参看其它同学的文章。

  • 《Windows热键注册》
  • 《在内核里查看所有的快捷键》
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值