Part I 中,我们已经处理完最棘手的部分:杀掉 QQFrmMgr.sys 创建的系统线程。剩余的工作就轻松多了——移除 QQFrmMgr.sys 和 QQProtect.sys
安装的 SSDT(系统服务调度表)钩子与 SSDT Shadow 钩子、销毁它们注册的事件通知 callback,从而将系统恢复至干净状态。
在此之前,按照惯例,还是先来检查一下这两个 QQ 驱动是否attach到了其它设备栈中的设备上,因为 rootkit 或恶意软件通常会挂载到其它合法驱动创建的
设备上,以便拦截或修改途经的 IRP(I/O 请求包)中携带的敏感数据,比如用户的击键数据。
如果发现了任何挂载迹象,则可以通过前一篇介绍的 APC 机制结合 IoDetachDevice() 例程,把恶意设备从设备栈中清理掉。
由于这两个 QQ 驱动会向 Windows 对象管理器维护的全局名称空间中,注册相应的设备对象名,如下图所示:
————————————————————————————————————————————————————————————————
Windows I/O 管理器导出的两个例程 IoCreateDevice() 与 IoCreateSymbolicLink() 普遍被驱动程序用来向 NT 名称空间中注册设备对象名,以及相应的符
号链接。用户态进程使用符号链接访问该对象;而在内核空间中,可以直接通过对象名进行访问,所以我们先用内核调试器的 “!devstack” 扩展命令,后接这
两个 QQ 设备对象的名称,查询它们是否挂载到了任何系统现存的设备栈上:
如你所见,这两个设备对象各自所在的设备栈中,都只有它们自身——如果它们挂载到了任何其它设备上,“!devstack” 的输出中就会含有那些 “受害” 的
设备。其中,“!DevObj” 栏位下的 4 字节 16 进制数是该设备对象的 “nt!_DEVICE_OBJECT” 结构地址;“!DrvObj” 栏位下的则是创建它们的驱动对象名
称。其实这两个 QQ 设备对象还算是 “良性” 的——某些 rootkit 创建设备对象时,根本不注册名字到 NT 名称空间(通过向 IoCreateDevice() 的第三个参
数传入 NULL,就可以做到这一点),对于此类 “恶性” 的匿名设备,需要获悉它的 “nt!_DEVICE_OBJECT” 结构地址,然后才能用 “!devstack” 遍历设备
栈,这个难度就不小了。
言归正传,接下来先检查系统的 SSDT,寻找有无被挂钩的系统服务,如下图,SSDT 的起始地址为 0x83c80f7c,一共有 0x191(401)个系统服务,其中一
部分已经被替换成 QQFrmMgr.sys 的钩子函数:
本来可以利用 “!chkimg” 扩展命令执行自动化检查,将 nt 模块(ntoskrnl.exe)的内存映像与磁盘文件比较,从而找出那些被修改了的部分,但不知为何我
的宿主机上 WinDbg 无法对 nt 模块实施检查,总是提示 ntkrpamp.exe/ntoskrnl.exe 的版本不匹配。(——还请成功执行 “!chkimg” 命令检查 nt 模块的
各位提供经验——)
一种最原始的方法就是先记录下受感染机器上 QQFrmMgr.sys 的钩子函数在 SSDT 中的位置,然后把它与另一个干净系统上的 SSDT 对比,以得知被 hook
的是哪个系统服务——前面那张截图就是用这种脏累的体力活实现的。
注意,系统每次初始化时,SSDT 的基地址,以及其中的系统服务入口点地址都是随机变化的,因而我们不能记录它们的内核地址,而是要记录函数名,在复
原前用反汇编指令 “u” ,即可强制解析原始系统服务在本次启动时分配到的内核地址,然后以 “eb” 指令编辑还原被 hook 的表项——言词苍白无力,还是
看图有真相吧:
注意,Intel x86/x64 体系结构的微处理器采用“小端字节序”在内存中放置数据,换言之,一个 “双字” (DWORD)数据的最低有效字节位于连续的四字节
存储空间中的最小地址处;最高有效位则存放在最大地址处。以上图为例,系统服务 “nt!NtCreateSection” 的入口点地址——83e5583b,其中最低有效字
节是 “3b”,所以我们在编辑时把它放在最前面(最小地址处)。
这个游戏规则是处理器硬件厂商规定的,如果不遵守它来办事就无法正确地恢复被挂钩的系统函数!
此外,通过分析我们还得知:QQFrmMgr.sys 利用 “inline hook” 技术硬挂钩了KeUserModeCallback() 内核例程中的正常函数调用
,由于我机器上的 “!chkimg” 不能工作,无法依赖它检测处挂钩前的原始函数调用,但是我们可以用 IDA PRO 逆向 ntkrpamp.exe/ntoskrnl.exe 的磁盘文
件,定位到 KeUserModeCallback() 中的原始函数调用——这种不修改函数指针数组(如 SSDT,一般位于 .data section),而是修改特定函数(一般位于
.text section)中的调用逻辑,就称为“inline hook”。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)