调用源检测逻辑,故障排查
分类:美高梅-操作

————————————————————————————————————————————————————————————————

——————————————————————————————————————————————————————

在上一篇文章中,我们已经看到 IopParseDevice() 如何对传入的 OPEN_PACKET 结构进行验证。假设 ObReferenceObjectByName() 的调用者没有分配并初始化第七个参数 ParseContext,而仅是简单地传入 “NULL” ,那么当调用链深入到 IopParseDevice() 内部时,就会因验证失败返回 C0000024(STATUS_OBJECT_TYPE_MISMATCH)。

 

我们根据源码中的暗示来追踪 OPEN_PACKET 结构究竟在哪分配的,如前所述,调用链 NtCreateFile->IoCreateFile()->IopCreateFile() 的结尾,也就是在 IopCreateFile() 内部,实际负责 OPEN_PACKET 的初始化。下面贴出的代码片段以 NT 5.2 版内核源码为样例:

在写 filter driver 或 rootkit 时,经常需要 attach 到设备栈中的目标设备,来拦截途经的 IRP(I/O Request Packet),实现过滤功能。
首先要获悉目标设备向 Windows Object Manager 维护的全局名称空间注册的 _DEVICE_OBJECT 名,此类信息可以通过像是 WinObj.exe 的工具获取。

 

接着调用 ObReferenceObjectByName(),该函数把获取的目标对象地址存储到它的最后一个参数(指针)中,然后返回给调用者。
实战时我们会发现,引用 _DRIVER_OBJECT 几乎总是成功;而引用 _DEVICE_OBJECT,则不一定会成功,返回的 NTSTATUS 状态码一般以两种居多:

美高梅网站是多少 1

1 C0000022(STATUS_ACCESS_DENIED)
2 C0000024(STATUS_OBJECT_TYPE_MISMATCH)

也就是说,我们直接复制 IopCreateFile() 中的 OPEN_PACKET 结构初始化部分逻辑就行了?

 

这里还有一个问题,负责分配该结构体内核内存的例程 IopAllocateOpenPacket() 是一个宏,Visual C++ 2015 中给出它是用 ExAllocatePoolWithTag() 定义的。这就好办了,在我们自己的驱动源码中,添加相应定义即可,如下图:

第一种情况通常是由于创建目标 _DEVICE_OBJECT 时指定的 session id 与当前的 session id 不一致,或者目标对象持有特殊的安全访问令牌/安全属性,所以我们无法以常规方式获取,而且这种错误频繁出现在 IoGetDeviceObjectPointer() 调用时,偏偏多数讲过滤驱动和 rootkit 的书籍都用 IoGetDeviceObjectPointer() 作为示例代码的一部分,真是有点误人子弟的意味。

 

第二种情况普遍出现在通过 ObReferenceObjectByName() 引用某些 _DEVICE_OBJECT 的场景中,缘由与 ObReferenceObjectByName() 利用其它执行体组件例程,在全局名称空间中执行的名字查找逻辑密切相关,后面会解释。

美高梅网站是多少 2

需要指出,既然通过 ObReferenceObjectByName() 引用绝大多数 _DRIVER_OBJECT 都会成功,而且 _DRIVER_OBJECT.DeviceObject 又指向该驱动创建的设备链中第一个 _DEVICE_OBJECT,那么这就是最稳当的方法。不过我们还是要知道 STATUS_OBJECT_TYPE_MISMATCH 的原因。

 

 ObReferenceObjectByName() 是一个未公开的例程,在 MSDN 中没有文档描述,另一方面,包含的 ntddk.h 或 wdm.h 头文件中也没有相关原型声明;

————————————————————————————————————————————————————————————

但是内核映像 ntoskrnl.exe 和其它的版本,的确导出了它的符号,换言之,我们只需要告诉链接器把这个函数名作为外部符号来解析即可。
此外,ObReferenceObjectByName() 的第五个参数也是一个未文档化的数据类型(POBJECT_TYPE),所以相关的声明是必须的,如下图所示:

因为 OPEN_PACKET 结构同样没有公开的文档来描述,所以要么在我们的驱动源码中用  “#include” 包含定义它的头文件,要么直接复制定义的那一部分黏贴进来。很显然,后者比较轻松——OPEN_PACKET 在内核源码的 “iomgr.h” 中定义,而该头文件又嵌套包含了一堆杂七杂八的内核头文件,要理清这些嵌套包含关系很麻烦,而且最重要的是,其中一些头文件定义的数据类型会与驱动开发中用的 “ntddk.h” 和“wdm.h”重复,引起编译器的抱怨。所以直接在 “iomgr.h” 中搜索字串 “typedef struct _OPEN_PACKET”,把找到的定义块拷贝进来即可。

 

然而,OPEN_PACKET 结构中唯有一个字段不是 “原生” 定义的——这就是 “PDUMMY_FILE_OBJECT” 类型,需要包含其它头文件才不致使编译器报错。

美高梅网站是多少 3

我的解决方案是,直接把该字段的声明所在行注释掉,下图展示了该字段具体的位置(在 “iomgr.h” 中的行号),方便各位快速查找:

—————————————————————————————————————————————————————————————

 

请注意,我们声明了一个指向类型“POBJECT_TYPE”的指针——IoDeviceObjectType——而“POBJECT_TYPE”自身又是指向类型“OBJECT_TYPE”的指针,所以在传入第五个参数时,一定要谨慎,使用操作符 “*” 解引 IoDeviceObjectType,才会与它的形参类型(POBJECT_TYPE)匹配,否则会导致 ObReferenceObjectByName() 失败,干扰我们对返回的 NTSTATUS 原因判断!

美高梅网站是多少 4

 

——————————————————————————————————————————————————————————————————

假设我们自己的驱动要获得“DeviceQQProtect”对应的 _DEVICE_OBJECT 指针,然后检查返回的 NTSTATUS 状态码,如下图所示:

注意,NT 6.1 版内核在编译时刻的 OPEN_PACKET 结构显然是未经 “恶意” 修改的,所以编译器为其 “sizeof(OPEN_PACKET)” 表达式计算 0x70 的值,而我们在自己的驱动中拿掉了 OPEN_PACKET 其中一个字段使得编译器为表达式 “sizeof(OPEN_PACKET)” 预计算 0x58 的值(后面的调试阶段会验证),这会造成 “Size” 字段不是 IopParseDevice() 内部逻辑预期的 0x70,从而导致返回 C0000024(STATUS_OBJECT_TYPE_MISMATCH)。

(“DeviceQQProtect”是与即时通信软件 QQ 一同安装的两个过滤驱动之一:QQProtect.sys 创建的设备对象名,
它也是我们稍后的 IRP Dispatch Routine Hook 实验目标!)

解决办法也很简单,我们的驱动中,不要依赖编译时刻的计算,直接把 “Size” 字段的值硬编码为 0x70 不就好了?

 

如下图所示,你还会注意到,我把 “Type” 字段的常量 “IO_TYPE_OPEN_PACKET” 改成了对应的数值,以确保万一。

美高梅网站是多少 5

 

 

美高梅网站是多少 6

可以看到,在虚拟机中测试时,DbgPrint() 打印返回的状态码为 C0000024(STATUS_OBJECT_TYPE_MISMATCH),也就是对象类型不匹配,如下图所示:

 

 

另外,由于 IopAllocateOpenPacket() 等价于 ExAllocatePoolWithTag(),而后者通常返回泛型指针(“ PVOID ,亦即 void * ”),
所以我强制把它转型为与 “openPacket” 一致的类型。
万事俱备,“东风” 就在于调用 ObReferenceObjectByName() 时,为第七个参数传入“openPacket” 即可,上图显示的很清楚了。

美高梅网站是多少 7

——————————————————————————————————————————————————————————————————

 

很不幸的是,我把编译出来的驱动放到虚拟机(Windows 7,基于 NT 6.1 版内核)里面动态加载测试,还是无法获取到

美高梅网站是多少 ,刚好手边有一份 NT 5.2 版内核的源码,它用来编译 Windows XP/Server 2003 使用的内核,尽管与我的测试机器的 NT 6.1 版内核有所差异,不过
还是姑且来看下 ObReferenceObjectByName() 内部究竟干了些什么。ObReference*() 系列的例程多数放在内核源码的“obref.c” 与“obdir.c
文件内。通过对相关调用链的分析,如下图所示:

“DeviceQQProtect” 相应的设备对象指针,ObReferenceObjectByName() 返回 C0000024。

美高梅网站是多少 8

为了找出故障原因,我在分配 OPEN_PACKET 逻辑的前面利用内联汇编添加了一个软中断 “__asm{ int 3; }  ”,宿主机器上启动内核调试器 kd.exe,我的启动参数像是这样:

上图中有两处关键点:其一是 ObpLookupObjectName() 中,检查目标对象类型的初始化设定(用 _OBJECT_TYPE_INITIALIZER 结构表示)中,是否指定了 ParseProcedure 例程,对于“设备”类对象,该函数值指针总是为 IopParseDevice() ,最终导致调用 IopParseDevice()

kd.exe -n -v -logo d:virtual_machine_debugging.txt -y SRV*C:Symbols* -k com:pipe,port=\.pipecom_1,baud=115200,reconnect

仔细观察前面的图片可知,从最初我调用 ObReferenceObjectByName() 开始,就为它的第七个参数 ParseContext 传入 NULL,而 ParseContext 会在调用链中一路往下传递,最终由 IopParseDevice() 接受并对该参数进行验证,如果它为空,就返回 STATUS_OBJECT_TYPE_MISMATCH

 

现在你知道为啥 ObReferenceObjectByName() 引用目标设备总是让人如此蛋疼,关键就在需要分配并初始化那个 ParseContext。。。

参数 “logo” 指定要把整个调试过程的输出信息写入日志;

 ———————————————————————————————————————————

“-y” 指定符号文件的位置(机器指令中没有内核函数与变量的符号,所以调试器需要查找额外的符号以向用户显示人类可读的名称);
“-k” 参数指定调试类型为 “命名管道模拟串口1”,波特率数值越高,响应越快。

我在源码中提取了相关代码片段,如下面这些图所示,最好能把它与上面的流程图对比加深理解,
后面我会拿虚拟机上的 Windows 7(基于 NT 6.1 版内核)调试,你会惊讶地发现,追踪栈回溯信息时,
竟然与 NT 5.2 版内核源码中的调用链非常相似,这说明版本之间的迁移并没有让对象名查找和验证逻辑改动太大。
(至少从 Windows XP 到 7 而言是如此,之后的版本由于没测试过,就不清楚了!)

把重新编译好的驱动放到虚拟机中,在提升权限后的命令提示符中执行 bcdedit.exe,启用调试模式,这样重启虚拟机后,就会进入调试模式(无需在启动过程中按下 F8 选择菜单)。

美高梅4858官方网站 , 

我把自己的驱动实现成按需加载,也就是利用服务控制管理器sc.exe)发出命令来动态加载和卸载,实现此功能的相应批处理文件内容如下图,注意该文件要放在虚拟机中执行,“start= demand” 表明通过 sc.exe 按需启动;“binpath” 就是驱动文件存放的磁盘路径,假设我的驱动名为 hideprocess.sys,执行该批处理任务后,就在相关的注册表位置添加了一项,往后只需在 cmd.exe 中执行 “sc.exe start/stop hideprocess” 就能够动态加卸载。

美高梅网站是多少 9

美高梅网站是多少 10

 

 

美高梅网站是多少 11

按照上述方式加载时,就会自动触发我们设定好的软件断点,即可在宿主机中检查虚拟机的内核空间。
另外还需注意一点:编译驱动时的 “构建” 环境应该选择 Check Build,这样会一并生成同名称的符号文件,后缀为 “.pdb”,从而调试器能够显示我们自己驱动中的函数与变量名称,提高调试效率,如下图:

美高梅网站是多少 12

 

 

美高梅网站是多少 13

 

——————————————————————————————————————————————————————————————————————

 

触发软件断点后,我们一般会用 “kv” 命令查看栈回溯信息,它披露出我们的驱动入口点 DriverEntry() 是由 I/O 管理器的 IopLoadDriver() 调用的:

IopParseDevice() 内部的那段注释,我依稀得到了绕过调用源检测的思路——那就是跟踪 NtCreateFile() ,看看 OPEN_PACKET 具体是在哪里

 

分配并初始化的;由于 IopParseDevice() 会检测 POPEN_PACKET 结构实例的一些字段来保证 ObReferenceObjectByName() 调用
是从 NtCreateFile() 发起的,NtCreateFile() 实现在 NT 5.2 版内核源码的 creater.c 中,它只是简单地执行调用链
IoCreateFile()->IopCreateFile()(此两例程都实现在源码的 iosubs.c 中),而具体由 IopCreateFile() 分配并初始化 OPEN_PACKET 结构。

美高梅网站是多少 14

所以我们只要在引用目标设备对象前,仿照 IopCreateFile() 的相关逻辑来分配并初始化 OPEN_PACKET,并作为 ObReferenceObjectByName()
的参数传入,就会绕过 IopParseDevice() 的“调用源检测”逻辑。
这部分 Patch 就留待后面的随笔再发表。我们当前先要验证“设备”类对象的“ParseProcedure”确实为 IopParseDevice()。。。。。

栈的顶层函数 “ReferenceDeviceAndHookIRPdispatchRoutine+0x56” 是我添加软中断的地方。执行 “r” 命令查看当前的 x86 通用寄存器状态,EIP 指向地址 0x8f4a3196 ,执行 “u hideprocess!ReferenceDeviceAndHookIRPdispatchRoutine+0x56 L2”,反汇编输出的第一行地址就是 0x8f4a3196,与 EIP 的值相符;第二行是把一个 16 进制值 “ 704F6F49h” 压栈,实际上它是 ASCII 字符 “pOoI” 的 16 进制编码,换言之,这是在通过内核栈传递 ExAllocatePoolWithTag() 的第三个参数(从右往左传递,请回顾之前的 IopAllocateOpenPacket() 宏定义那张图)。

——————————————————————————————————————————————————

————————————————————————————————————————————————————————————————

在双击内核调试环境中,首先通过设备名称“DeviceQQProtect”取得相应对象的信息:

继续按下 “t” 单步执行,如下图所示,你可以看到,ExAllocatePoolWithTag() 的第二个参数,分配的内核内存大小为 0x70 字节,因为我在宏定义中硬编码了这个值,而不是用 sizeof(OPEN_PACKET) 表达式让编译器计算;另一方面,图中的 “dt” 命令也证实了它的大小为 0x70 字节。

美高梅网站是多少 15

首个传入的参数 “NonPagedPool” 为不可换页池,其内的数据无法被换出物理内存,该常量对应的数值为 “0”:

得到对象头地址后,格式化并转储其中的字段,我们感兴趣的是“TypeIndex”字段,它用来索引“对象类型表”中的相应“对象类型”:

美高梅网站是多少 16

 

 

美高梅网站是多少 17

我不想浪费时间在查看内核内存的分配细节上,所以我按下 “p”,步过 ExAllocatePoolWithTag() 函数调用,接下来的 cmp/jne 汇编序列对应源码中检查是否成功分配了内存并用于 openPacket 指针,实际的执行结果是跳转到地址 0x8f4a31c6 ,对应源码中初始化 OPEN_PACKET 结构前两个字段的逻辑:

WInodws 内核使用一个数据结构——ObTypeIndexTable 存放有关各种“对象类型”的信息,本质上 ObTypeIndexTable 是一个指针数组,在 32 位体系结构上,每个指针大小 4 字节,而我们得到的索引号为(下标**从 0 开始**)19,下图中的两条表达式据此计算出该“对象类型”的地址:

美高梅网站是多少 18

美高梅网站是多少 19

接下来一直单步执行到调用 ObReferenceObjectByName() 前夕,在此处我们要 “步入” 它的内部,进行故障排查,所以按下 “t” 跟进,这里有一个小技巧,我们已经分析过 ObReferenceObjectByName() 的源码,知道它会调用很多函数,而且大致清楚问题出现在 ObpLookupObjectName() 里面,所以指令 “tc”可以跟踪到每个函数调用处停止,再由用户决定是否跟进该函数内部。

由此可知,相应“对象类型”结构的地址为 0x855cef78——Windows 内核用数据结构 _OBJECT_TYPE 来表示“对象类型”的概念,所以再次
格式化并转储其中的字段,我们感兴趣的字段为“TypeInfo”,如前所述,它是一个“对象类型初始化设定”结构,内核用
_OBJECT_TYPE_INITIALIZER 来表示“对象类型初始化设定”的概念。需要注意,TypeInfo 偏移它的母结构起始地址 0x28 字节,所以要加上这个
offset 再查看,如你所见,其中的“ParseProcedure”为 IopParseDevice()

这是我的美好梦想,但现实总是残酷的,在我跟踪到原子操作系列函数

 

nt!ExInterlockedPopEntrySList() 调用时,kd.exe 就卡住了,无法继续追踪此后的调用链。从稍早的栈回溯信息来看,与源码中和我们预测的调用序列大致相符,只是不晓得为啥在 nt!ObpAllocateObjectNameBuffer() 中,为了给传入的驱动对象名称 “DeviceQQProtect” 分配内核内存,调用 nt!ExInterlockedPopEntrySList(),而后者却无法追踪。。。。是虚拟机环境的缘故,还是原子操作类函数的不可分割性质?

美高梅网站是多少 20

 

 

美高梅网站是多少 21

下篇文章将讨论如何绕过 IopParseDevice() 的调用源检测,并调试我们的成果,将其应用于 rootkit 开发技术中。

 ——————————————————————————————————————————————————————————————

 

讲一点废话,一般我们在栈回溯中看到的顶层说明行,有一个 “Args to Child” 项目,表示调用者传递给它的参数,不过最多也只能显示前三个。

以下图为例子吧,传递给 nt!ExAllocatePoolWithTag() 的三个参数(从左到右)就是 00000000(NonPagedPool),00000070(我硬编码的值),704f6f49(ASCII 字符串“pOoI”),同理,传递给 hideprocess!DriverEntry() 的第一个参数 867c3550 是 _DRIVER_OBJECT 结构的地址,由I/O 管理器加载它时为它分配(注意与源码中 DriverEntry() 定义的一枚 _DRIVER_OBJECT 指针不同,“Args to Child”

列出的数据相当于执行解引操作符 * 后的结果),第二个参数是 UNICODE_STRING 结构的地址,对应源码定义中的一枚 _UNICODE_STRING 指针,该结构中存储的是我们驱动在注册表中的完整路径:

 

美高梅网站是多少 22
——————————————————————————————————————————————————————————————————

总而言之 ,基于以上理由我无法继续跟进到 ObpLookupObjectName() 里面查看它是否执行了 IopParseDevice() 回调,从而无法确定究竟为啥后者返回 C0000024。

我想可能是因为内核源码版本的变化,导致相关例程的判断逻辑也不一样了,不能根据前一版源码的逻辑来编写预计运行在后一版内核上的驱动。

其实解决方案还是有的,比较花时间罢了,就是利用 “u” 指令反汇编 ObpLookupObjectName() 起始处对应的机器指令,再反编译成近似的 C 伪码,与 NT 5.2 版内核源码对比,找出其中改动的地方,但这是一个费时费力的工作,且收益甚微,还不如直接在互联网上搜释出的 NT 6.1 版内核源码,或者接近的版本,再思考绕过的方法。

顺带说一下,根据 A 设备名获得 A 设备对象的指针,然后把 rootkit/自己驱动创建的恶意设备 attach 到 A 设备所在的设备栈,从而拦截检查经过 A 设备的 IRP 内数据。。。。这种方法已经比较过时了,因为现在反病毒软件的内核模式组件也会检查这些设备栈,寻找任何匹配特征码的恶意设备,再者,内核调试器的 “!devstack” 命令很容易遍历揭示出给定设备所在的设备栈内容,被广泛用于计算机调查取证中,从 rootkit 的首要目标——实现隐身——的角度来看, attach 到设备栈就不是一个好榜样。

相反,通过 ObReferenceObjectByName() 总是能够获得驱动对象的指针,进而能够 hook 该驱动的 IRP 分发例程,这种手段隐蔽性极高,而且不容易被检测出来。

后续的博文将讨论如何将这种技术用在 rootkit 中,同时适应当前流行的对称多处理器(SMP)环境。

————————————————————————————————————————————————————————————————

本文由美高梅网站是多少发布于美高梅-操作,转载请注明出处:调用源检测逻辑,故障排查

上一篇:强大的windbg定位内存泄露 下一篇:配置指南【美高梅网站是多少】
猜你喜欢
热门排行
精彩图文