查看: 36|回复: 0

[转载图文] 一种枚举系统热键的思路及代码实现(Win7&Win10)

[复制链接]
发表于 6 天前 | 显示全部楼层 |阅读模式

  前言
  现在使用全局快捷键的软件越来越多,经常遇到快捷键被占用的情况,想找出被谁占用了,网上找了一些工具,不过都不支持新版本的Win10。
  
  于是自己写了一个(支持Win7到最新的Win10 2004 32/64位,XP就不考虑了),本文顺带记录一下分析思路。


  思路分析
  说明:Win7/8和Win10的热键数据结构基本一致,本文以Win10 1903 x64作为分析对象,重点是在Hash表的搜索方法上。
  众所周知,注册热键需要调用RegisterHotKey,首先看一下函数原型:
[pre]BOOL RegisterHotKey(
  HWND hWnd,
  int  id,
  UINT fsModifiers,
  UINT vk
  );
  [/pre]


  • hWnd 窗口句柄
  • id 热键ID
  • fsModifiers 控制位,如Ctrl/Alt/Shift...
  • vk 虚拟键码 Virtual Key Codes

简单跟一下RegisterHotKey函数,到ntdll!NtUserRegisterHotKey,看一下NtUserRegisterHotKey函数原型:
  
[pre]BOOL APIENTRY NtUserRegisterHotKey(HWND hWnd,
  int     id,
  UINT     fsModifiers,
  UINT     vk
  );
  [/pre]

  可以看到Native的参数和上层API一致,往下则是Shadow SSDT,进入Win32k,Win7/Win8/8.1还是Win32k.sys,Win10已拆分成Win32k/Win32kfull/Win32kbase三个模块,其中NtUserRegisterHotKey是Win32kfull的导出函数。
  
  打开IDA,定位到NtUserRegisterHotKey函数,其调用RegisterHotKey如下:
  
[pre]mov     r9d, edi
  mov     dword ptr [rsp+48h+BugCheckParameter2], ebp ; BugCheckParameter2
  mov     r8d, r14d
  xor     edx, edx
  mov     rcx, rax        ; struct tagWND *
  call    _RegisterHotKey
  [/pre]

  IDA推导的参数显示有问题,跟一下参数来源,不难知道RegisterHotKey参数1是hWnd,参数2是NULL,参数3是ID,参数4是fsModifiers,参数5是vk。
  
  继续跟进RegisterHotKey,直接F5,有两段代码需要注意:
  
  第一段很意图很明显,通过FindHotKey查找热键是否已经注册。
  
  
  第二段则是若HotKey未找到,则从Win32kPool中分配HotKey数据结构,并填充相应的字段,最后加入Hash表gphkHashTable中。接下来看看结构的填充方式以及Hash表的Index如何计算的。
  
  
  v19则是HotKey结构体,对照RegisterHotKey参数,很容易分析出下面的偏移代表的字段:
  
[pre]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表
  [/pre]

  最终可得到结构如下:
  
[pre]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;
  [/pre]

  至此,热键数据结构已经分析清楚,本文目的是枚举热键,因此关键问题是如何定位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线程。
  
[pre]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;
  }
  [/pre]

  接着获取NtUser*Hotkey相关函数,注册热键填充Hash表后,后面就可以开始搜索搜索Hash表了,HotKey是由Win32k从NonPagedPool中分配,因此用MmIsAddressValid检查无须担心换页问题,具体代码如下:
  
[pre]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:%x\n", 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:%x\n", 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;
  }
  }
  //开始搜索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;
  break;
  }
  continue;
  }
  //初步过滤内核地址
  if (ptr > MmSystemRangeStart) {
  j++;
  continue;
  }
  j = 0;
  }
  //取消注册成功的热键
  for (int i = 1; i <= MAX_VK; i++) {
  if (hkmarks) {
  pNtUserUnregisterHotKey(NULL, hkmarks);
  }
  }
  return 1;
  }
  [/pre]

  递归解析Hash表,Dump出系统热键,代码如下所示:
  
[pre]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;
  if (hk)
  DumpHotkeyNode(hk);
  }
  }
  [/pre]



  结束语
  注意:发现输入法的快捷键没有注册到系统热键中,应该是自己管理的,因此这种方式不能被检测到,如果有快捷键占用而没找到,则优先检查输入法设置。
  
  驱动效果如下图所示:
  
  
  图形化工具下载地址:https://github.com/BlackINT3/OpenArk,点击内核--进入内核模式--查看系统热键:
  
  
ps. 操作热键附件可点击“阅读原文”下载~
  - End -
  看雪ID:深山修行之人
https://bbs.pediy.com/user-857678.htm
  *本文由看雪论坛 深山修行之人 原创,转载请注明来自看雪社区。
游客,如果您要查看本帖隐藏内容请回复


温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046
微信公众号
快速回复 返回顶部 返回列表