————————————————————————————————————————————————————————————————————————
QQ 是一款热门的即时通信(IM)类工具,在安装时刻会向系统分区的 \..\windows\system32\drivers 路径下生成两个驱动程序文件:
QQProtect.sys 与 QQFrmMgr.sys ,前者是 QQProtect.exe(QQ 安全防护进程,又称 Q 盾)的内核模式组件;后者是一种过滤型驱动。
同时还会向注册表位置 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\ 创建对应名称的键,其中有重要的两个子键控制这两个驱动的加载
方式:“type”与“start”——对于 QQProtect.sys ,其键值分别为 1 和 2,这意味着由 services.exe(服务控制管理器)自动将 QQProtect.sys 载入内核
空间;对于 QQFrmMgr.sys,其键值分别为 1 和 1,这意味着在内核初始化期间,由 ntoskrnl.exe 将 QQFrmMgr.sys 载入内核空间,就加载
的顺序而言,QQFrmMgr.sys 早于 QQProtect.sys ,如下图所示:
尽管这两个驱动并非 Rootkit 或者恶意软件,但它们确实会 hook 系统服务调度表/Shadow、在 System 进程中注入内核线程、注
册一些通知回调。。。。
所以也算是更改了系统的一些关键数据结构来进行非正当活动。
本文探讨如何使用内核调试器 WinDbg.exe 来检查诸如此类为保护 QQ 进程而采取的内核空间手段,然后把系统还原至“干净”状态。
测试环境是两台真实的计算机(双机物理调试)——运行 Windows 7 的 宿主机(调试机),以及运行 Windows 8.1 的目标机(被调试机,已安装了 QQ)
两者通过以太网线连接进行调试。
注意,通过以太网线执行双机物理调试时,对调试机的网卡无特殊要求;但是被调试机的网卡必须被 Debugging Tools for Windows 所支持(亦即 Kd.exe
与 WinDbg.exe),而且被调试机上的操作系统版本需要是 windows 8 或者更后面的版本;调试机上的操作系统需要是 windows xp 或更后面的版本。
使用以太网调试的一大好处就是,通信介质获取方便——相较于老旧的串口线(RS-232)以及主板上基本被淘汰的 COM 模块而言,Cat5 标准以上的
网络线随便在电脑城就能买到,而且主板上绝不可能没有网络接口卡使用的 RJ-45 端口。。。。想必以太网调试一定会成为日后的标准!
另一方面,我也实施了物理-虚拟机调试,虚拟机作为被调试机,其上运行 Windows 7,这样不但能够对比出,QQ 驱动针对不同内核版本
(Windows 7 是内核版本 6.1 ;Windows 8.1 是内核版本 6.3)所表现出来的逻辑差异,还能够明确 QQ 驱动是否采取了“反虚拟机”技术,并且揭示它在
真实机器上的行为!
因此下面的调试过程中,所有与真实机器上不同的结果我都会另行说明。在开始之前,来过目一下我配置的双机物理调试参数:
cd "d:\Windows Kits\10\Debuggers\x86" && d: && windbg.exe -n -v -logo d:\networking_physical_host-target_debugging.txt -y SRV*E:\windows8_1_retail_symbols*http://msdl.microsoft.com/download/symbols -k net:port=60111,key=shayi.1983.gmail.com
其中,
❶ 我将 Windows Kits 驱动开发工具包安装到了“d:\Windows Kits”目录下;
❷ 输出调试信息到指定的日志文件;
❸ 指定微软的符号服务器 URL,这样调试器就可以通过 HTTP GET 请求,按需从服务器下载并解析特定内核模块中的函数符号;
❹ 以及预先存储在本地的符号文件(可以从 MSDN 站点下载,整个 MSI 封装的符号包大小约为五、六百 MB)所在路径;
(注意,宿主机上内核版本的不同导致需要分别下载对应的符号文件,并指定为调试参数)
❺ 指定通过以太网调试(net),宿主机上开启调试端口为 UDP 的 60111;
❻ 最后的 key 可以任意指定,但其中的 4 个子域之间需要用点号分隔开。
关于目标机上的对应配置,请各位参见 MSDN 文档,这里就不再赘述。
——————————————————————————————————————————————————————————
首先在 Windows 8.1 目标机上通过 Process Explorer 浏览到 System 进程中的系统线程,其中有一个 QQ 内核线程是由
QQFrmMgr.sys 创建的,该线程的启动地址距离所属模块被载入基址的偏移量为 0x5e34 :
我们的目标是结束该线程的执行,通常的做法是用系统内置的 APC(异步过程调用)机制来实现。APC 就是运行在特定线程上下文
中的例程。
从编程角度来讲,调用 KeInitializeApc() 初始化一个 nt!_KAPC 结构,并将其关联到该 QQ 内核线程的 nt!_KTHREAD 结构,设定
该 APC 例程回调为 PspExitThread();然后利用 KeInsertQueueApc() 通过这个 nt!_KTHREAD 结构来排入该 QQ 内核线程的
APC 队列,如此一来,当该 APC 被交付时,就会在该 QQ 内核线程的执行上下文中调用 PspExitThread(),从而终止掉该 QQ 内
核线程。
而在调试环境下,没有对应的内核 API 可用,所以我们必须手工构造 APC、指定回调函数、关联线程、以及排入队列,如下步骤所
示:
第一步:查询 System 进程的 nt!_EPROCESS 结构地址;
第二步:定位到其中的线程双向链表头部,然后开始遍历这个链表中的每一个 nt!_ETHREAD 结构,找出那些启动地址位于
QQFrmMgr.sys 模块空间内的线程:
相应的 WinDbg 命令如下:
!list "-t nt!_LIST_ENTRY.FLink -e -x \"r @$t3=@$extret-@$t1;
r @$t4= @$t3+@$t2;
r @$t5=poi(@$t4);
.if(@@((unsigned long)@$t5>(unsigned long)0x82000000 && (unsigned long)@$t5<(unsigned long)0x82017b00)){r @$t3;dt -b nt!_ETHREAD Cid. @$t3; dds @$t4 l1;};
\" 8565e5c0+@$t0"
第三步:手工构建一个 nt!_KAPC 结构,指定回调函数、关联线程、以及排入队列:
(3-1):查询 QQFrmMgr.sys 模块内部的 section 信息,注意到其中的 .data section 后紧接 INIT section:
从上图可知,.data section 起始 RVA 为 11800,大小 3C80,结束 RVA 为 15480,这刚好是 INIT section 的起始 RVA。
INIT section 的属性中,“Discardable”与“Execute Read Write”完美匹配了手工构建 APC 需要的写属性,以及回调函数需要
的执行属性,所以它是理想的目标 section。
(3-2):从 INIT section 起始地址初始化 0x200 字节内存,此块区域用于 nt!_KAPC 结构和回调函数(nt!_KAPC 的
KernelRoutine 字段)。如下图所示,
我们在地址 处构造的回调函数调用 PspExitThread() 来结束当前线程的运行;然后在地址 处构造一个
nt!_KAPC 结构;
r @$t0=;
r @$t1=8b9ccbc0;
r@$t2=;
?? ((nt!_KAPC*)@$t0)->Type=;
?? ((nt!_KAPC*)@$t0)->Size=sizeof(nt!_KAPC);
?? ((nt!_KAPC*)@$t0)->Thread=@$t1;
?? ((nt!_KAPC*)@$t0)->KernelRoutine=@$t2;
?? ((nt!_KAPC*)@$t0)->Inserted=;
r @$t3=@@(&(((nt!_ETHREAD*)@$t1)->Tcb.ApcState.ApcListHead[]));
r @$t4=@@(&(((nt!_KAPC*)@$t0)->ApcListEntry));
r @$t5=@@(((nt!_LIST_ENTRY*)@$t3)->Flink);
?? ((nt!_LIST_ENTRY*)@$t4)->Flink=@$t5;
?? ((nt!_LIST_ENTRY*)@$t4)->Blink=@$t3;
?? ((nt!_LIST_ENTRY*)@$t5)->Blink=@$t4;
?? ((nt!_LIST_ENTRY*)@$t3)->Flink=@$t4;
?? ((nt!_ETHREAD*)@$t1)->Tcb.ApcState.KernelApcPending=;
变量“t1”的值是前面查询到的 QQ 内核线程的 nt!_ETHREAD 结构;
变量“t3”用来定位到 nt!_ETHREAD 结构中的第一个 APC 队列头部(Tcb.ApcState.ApcListHead[0]);这个队列头部
的“Flink”字段(指向下一个 nt!_KAPC 结构)由变量“t5”存储;
变量“t4”亦即我们构建的 nt!_KAPC 结构中的“ApcListEntry”字段,它被用来初始化“t5”;
这种初始化逻辑类似于下面的 C 代码:
KTHREAD.ApcState.ApcListHead[]->Flink = KAPC->ApcListEntry;
验证我们的操作是否正确:
整个过程的形象图示:
为了理解 APC 交付的机制,我们在回调函数入口处设置一个断点,然后就能够通过栈回溯信息得知该回调是如何被调用的,按
下“g”键恢复目标机器的执行,等待 APC 交付时触发断点:
从上图可以看到,这种 APC 交付机制其实并不神秘—— 传递给 PspSystemThreadStartup() 的首个参数就是 QQFrmMgr.sys 创
建的 QQ 内核线程的启动地址,表明它被调度运行了;经过一系列调用后,KiSwapThread() 从它接收到的首个参数
(0x8b9ccbc0,亦即 QQ 内核线程的 nt!_ETHREAD 结构地址)中,定位到其 APC 队列头部,然后调用链表中第一个 nt!_KAPC
结构的“KernelRoutine”回调,从而触发我们先前设置的断点。
按下“g”键继续运行,导致 PspExitThread() 把 QQ 内核线程终止掉然后返回,现在通过 Process Explorer 浏览目标机器上,
System 进程中的系统线程们,已经找不到 QQFrmMgr.sys+0x5e34 那个线程了,另一方面,也可以在调试机器上验证:
r @$t0=@@(#FIELD_OFFSET(nt!_EPROCESS, ThreadListHead));
r @$t1= @@(#FIELD_OFFSET(nt!_ETHREAD, ThreadListEntry));
r @$t2=@@(#FIELD_OFFSET(nt!_ETHREAD, StartAddress));
!list "-t nt!_LIST_ENTRY.FLink -e -x \"r @$t3=@$extret-@$t1;
r @$t4= @$t3+@$t2;
r @$t5=poi(@$t4);
.if(@@((unsigned long)@$t5>(unsigned long)0x82000000 && (unsigned long)@$t5<(unsigned long)0x82017b00)){r @$t3;dt -b nt!_ETHREAD Cid. ExitStatus @$t3; dt -b nt!_KTHREAD Header. @$t3; };
\" 8565e5c0+@$t0"
——————————————————————————————————————————————————————————————————————————————————
小结:本篇讨论了如何利用内核提供的基础设施——APC——来挫败 QQ 过滤驱动向内核空间注入的可执行代码,并在基于 Windows 8.1(NT 6.3 版内
核)的真实机器上成功实践,限于篇幅,后续博文将介绍如何检测并还原 QQ 驱动修改的其它内核数据结构,以及清除它安装的钩子例程!
——————————————————————————————————————————————————————————————————————————————————
------- 软件调试——挫败 QQ.exe 的内核模式保护机制 -------的更多相关文章
-
------- 软件调试——还原 QQ 过滤驱动对关键内核设施所做的修改 -------
-------------------------------------------------------------------------------- 在前一篇博文中,我们已经处理完最棘手的 ...
-
------- 软件调试——注销 QQ 过滤驱动设置的事件通知 CallBack (完)-------
---------------------------------------------------------------------------------- 本系列的最后一篇演示如何通过调试手 ...
-
使用WinDbg调试入门(内核模式)
windbg是一个内核模式和用户模式调试器,包含在Windows调试工具中.这里我们提供了一些实践练习,可以帮助您开始使用windbg作为内核模式调试器. 设置内核模式调试 内核模式调试环境通常有两台 ...
-
通过USB 2.0电缆手动设置内核模式调试
Windows的调试工具支持通过USB 2.0电缆进行内核调试.本文介绍如何手动设置USB 2.0调试.通过USB 2.0电缆进行调试需要以下硬件: USB 2.0调试电缆.此电缆不是标准USB 2. ...
-
Windows内核开发-5-(2)-内核模式调试
Windows内核开发-5-(2)-内核模式调试 普通用户模式的调试,采取的是给进程添加一个线程来挂起断点,作为一个调试器的线程在进程中使用.照这样来类推,对操作系统调试相当于添加一个进程来限制操作系 ...
-
QQ浏览器X5内核问题汇总
原文:http://itindex.net/detail/53391-qq-浏览器-x5 常常被人问及微信中使用的X5内核的问题,其实我也不是很清楚,只知道它是基于android 4.2的webkit ...
-
转{QQ浏览器X5内核问题汇总}
转自https://www.qianduan.net/qqliu-lan-qi-x5nei-he-wen-ti-hui-zong/ 常常被人问及微信中使用的X5内核的问题,其实我也不是很清楚,只知道它 ...
-
微信、QQ浏览器X5内核问题汇总
一. 资料汇总 1.前端H5调起QQ浏览器的总结:http://km.oa.com/group/22486/articles/show/210189?kmref=search 2.Android We ...
-
Windbg在软件调试中的应用
Windbg在软件调试中的应用 Windbg是微软提供的一款免费的,专门针对Windows应用程序的调试工具.借助于Windbg, 我们常见的软件问题:软件异常,死锁,内存泄漏等,就可以进行高效的排查 ...
随机推荐
-
SQL SERVER 2008 中三种分页方法与总结
建立表: CREATE TABLE [TestTable] ( , ) NOT NULL , ) COLLATE Chinese_PRC_CI_AS NULL , ) COLLATE Chinese_ ...
-
【译】 AWK教程指南 附录C-AWK的内建函数
C.1 字串函数 index( 原字串, 查找的子字串 ) 若原字串中含有欲寻找的子字串,则返回该子字串在原字串中第一次出现的位置,若未曾出现该子字串则返回0. 例如: $ awk 'BEGIN{ p ...
-
【转】Java并发编程:如何创建线程?
一.Java中关于应用程序和进程相关的概念 在Java中,一个应用程序对应着一个JVM实例(也有地方称为JVM进程),一般来说名字默认是java.exe或者javaw.exe(windows下可以通过 ...
-
Coding daily
@2017-7月 1可视化控件的awakeFromNib不调用? 因为用代码注册了cell 2scrollView添加子控件布局无效? 最好不要用masonry,直接用frame 还有tableVie ...
-
【学习】DataFrame&;Series类【pandas】
参考链接:http://blog.csdn.net/yhb315279058/article/details/50226027 DataFrame类: DataFrame有四个重要的属性: index ...
-
nowcoder172C 保护 (倍增lca+dfs序+主席树)
https://www.nowcoder.com/acm/contest/172/C (sbw大佬太强啦 orz) 先把每一个路径(x,y)分成(x,lca),(y,lca)两个路径,然后就能发现,对 ...
-
JavaScript--序列化以及转义
一.序列化 1.1 序列化 将其他对象转换为字符串,用法:JSON.stringify() var li = [1,2,3,4]; var new_li = JSON.stringify(li); n ...
-
getopenfilename多选文件/文件夹问题和getsavefilename另存为路径
关于使用getopenfilename多选多个文件是可以的. 以下是多选文件的代码 bool GetNeedOpenFilePath(vector<tstring>& vectFi ...
-
【前端开发】 JS面试题整理
1.截取字符串abcdace的acealert('abcdace'.substring(4)); 2.规避javascript多人开发函数重名问题命名空间封闭空间js模块化mvc(数据层.表现层.控制 ...
-
PHP中有丰富的运算符集,它们中大部分直接来自于C语言
PHP中有丰富的运算符集,它们中大部分直接来自于C语言.按照不同功能区分,运算符可以分为:算术运算符.字符串运算符.赋值运算符.位运算符.条件运算符,以及逻辑运算符等.当各种运算符在同一个表达式中时, ...