--------驱动开发之 ObReferenceObjectByName() 故障排查--------
——————————————————————————————————————————————————————
在写 filter driver 或 rootkit 时,经常需要 attach 到设备栈中的目标设备,来拦截途经的 IRP(I/O Request Packet),实现过滤功能。
首先要获悉目标设备向 Windows Object Manager 维护的全局名称空间注册的 _DEVICE_OBJECT 名,此类信息可以通过像是 WinObj.exe 的工具获取。
接着调用 ObReferenceObjectByName(),该函数把获取的目标对象地址存储到它的最后一个参数(指针)中,然后返回给调用者。
实战时我们会发现,引用 _DRIVER_OBJECT 几乎总是成功;而引用 _DEVICE_OBJECT,则不一定会成功,返回的 NTSTATUS 状态码一般以两种居多:
1 C0000022(STATUS_ACCESS_DENIED) 2 C0000024(STATUS_OBJECT_TYPE_MISMATCH)
第一种情况通常是由于创建目标 _DEVICE_OBJECT 时指定的 session id 与当前的 session id 不一致,或者目标对象持有特殊的安全访问令牌/安全属性,所以我们无法以常规方式获取,而且这种错误频繁出现在 IoGetDeviceObjectPointer() 调用时,偏偏多数讲过滤驱动和 rootkit 的书籍都用 IoGetDeviceObjectPointer() 作为示例代码的一部分,真是有点误人子弟的意味。
第二种情况普遍出现在通过 ObReferenceObjectByName() 引用某些 _DEVICE_OBJECT 的场景中,缘由与 ObReferenceObjectByName() 利用其它执行体组件例程,在全局名称空间中执行的名字查找逻辑密切相关,后面会解释。
需要指出,既然通过 ObReferenceObjectByName() 引用绝大多数 _DRIVER_OBJECT 都会成功,而且 _DRIVER_OBJECT.DeviceObject 又指向该驱动创建的设备链中第一个 _DEVICE_OBJECT,那么这就是最稳当的方法。不过我们还是要知道 STATUS_OBJECT_TYPE_MISMATCH 的原因。
ObReferenceObjectByName() 是一个未公开的例程,在 MSDN 中没有文档描述,另一方面,包含的 ntddk.h 或 wdm.h 头文件中也没有相关原型声明;
但是内核映像 ntoskrnl.exe 和其它的版本,的确导出了它的符号,换言之,我们只需要告诉链接器把这个函数名作为外部符号来解析即可。
此外,ObReferenceObjectByName() 的第五个参数也是一个未文档化的数据类型(POBJECT_TYPE),所以相关的声明是必须的,如下图所示:
—————————————————————————————————————————————————————————————
请注意,我们声明了一个指向类型“POBJECT_TYPE”的指针——IoDeviceObjectType——而“POBJECT_TYPE”自身又是指向类型“OBJECT_TYPE”的指针,所以在传入第五个参数时,一定要谨慎,使用操作符 “*” 解引 IoDeviceObjectType,才会与它的形参类型(POBJECT_TYPE)匹配,否则会导致 ObReferenceObjectByName() 失败,干扰我们对返回的 NTSTATUS 原因判断!
假设我们自己的驱动要获得“\Device\QQProtect”对应的 _DEVICE_OBJECT 指针,然后检查返回的 NTSTATUS 状态码,如下图所示:
(“\Device\QQProtect”是与即时通信软件 QQ 一同安装的两个过滤驱动之一:QQProtect.sys 创建的设备对象名,
它也是我们稍后的 IRP Dispatch Routine Hook 实验目标!)
可以看到,在虚拟机中测试时,DbgPrint() 打印返回的状态码为 C0000024(STATUS_OBJECT_TYPE_MISMATCH),也就是对象类型不匹配,如下图所示:
刚好手边有一份 NT 5.2 版内核的源码,它用来编译 Windows XP/Server 2003 使用的内核,尽管与我的测试机器的 NT 6.1 版内核有所差异,不过
还是姑且来看下 ObReferenceObjectByName() 内部究竟干了些什么。ObReference*() 系列的例程多数放在内核源码的“obref.c” 与“obdir.c”
文件内。通过对相关调用链的分析,如下图所示:
上图中有两处关键点:其一是 ObpLookupObjectName() 中,检查目标对象类型的初始化设定(用 _OBJECT_TYPE_INITIALIZER 结构表示)中,是否指定了 ParseProcedure 例程,对于“设备”类对象,该函数值指针总是为 IopParseDevice() ,最终导致调用 IopParseDevice()。
仔细观察前面的图片可知,从最初我调用 ObReferenceObjectByName() 开始,就为它的第七个参数 ParseContext 传入 NULL,而 ParseContext 会在调用链中一路往下传递,最终由 IopParseDevice() 接受并对该参数进行验证,如果它为空,就返回 STATUS_OBJECT_TYPE_MISMATCH。
现在你知道为啥 ObReferenceObjectByName() 引用目标设备总是让人如此蛋疼,关键就在需要分配并初始化那个 ParseContext。。。
———————————————————————————————————————————
我在源码中提取了相关代码片段,如下面这些图所示,最好能把它与上面的流程图对比加深理解,
后面我会拿虚拟机上的 Windows 7(基于 NT 6.1 版内核)调试,你会惊讶地发现,追踪栈回溯信息时,
竟然与 NT 5.2 版内核源码中的调用链非常相似,这说明版本之间的迁移并没有让对象名查找和验证逻辑改动太大。
(至少从 Windows XP 到 7 而言是如此,之后的版本由于没测试过,就不清楚了!)
从 IopParseDevice() 内部的那段注释,我依稀得到了绕过调用源检测的思路——那就是跟踪 NtCreateFile() ,看看 OPEN_PACKET 具体是在哪里
分配并初始化的;由于 IopParseDevice() 会检测 POPEN_PACKET 结构实例的一些字段来保证 ObReferenceObjectByName() 调用
是从 NtCreateFile() 发起的,NtCreateFile() 实现在 NT 5.2 版内核源码的 creater.c 中,它只是简单地执行调用链
IoCreateFile()->IopCreateFile()(此两例程都实现在源码的 iosubs.c 中),而具体由 IopCreateFile() 分配并初始化 OPEN_PACKET 结构。
所以我们只要在引用目标设备对象前,仿照 IopCreateFile() 的相关逻辑来分配并初始化 OPEN_PACKET,并作为 ObReferenceObjectByName()
的参数传入,就会绕过 IopParseDevice() 的“调用源检测”逻辑。
这部分 Patch 就留待后面的随笔再发表。我们当前先要验证“设备”类对象的“ParseProcedure”确实为 IopParseDevice()。。。。。
——————————————————————————————————————————————————
在双击内核调试环境中,首先通过设备名称“\Device\QQProtect”取得相应对象的信息:
得到对象头地址后,格式化并转储其中的字段,我们感兴趣的是“TypeIndex”字段,它用来索引“对象类型表”中的相应“对象类型”:
WInodws 内核使用一个数据结构——ObTypeIndexTable 存放有关各种“对象类型”的信息,本质上 ObTypeIndexTable 是一个指针数组,在 32 位体系结构上,每个指针大小 4 字节,而我们得到的索引号为(下标从 0 开始)19,下图中的两条表达式据此计算出该“对象类型”的地址:
由此可知,相应“对象类型”结构的地址为 0x855cef78——Windows 内核用数据结构 _OBJECT_TYPE 来表示“对象类型”的概念,所以再次
格式化并转储其中的字段,我们感兴趣的字段为“TypeInfo”,如前所述,它是一个“对象类型初始化设定”结构,内核用
_OBJECT_TYPE_INITIALIZER 来表示“对象类型初始化设定”的概念。需要注意,TypeInfo 偏移它的母结构起始地址 0x28 字节,所以要加上这个
offset 再查看,如你所见,其中的“ParseProcedure”为 IopParseDevice()。
下篇文章将讨论如何绕过 IopParseDevice() 的调用源检测,并调试我们的成果,将其应用于 rootkit 开发技术中。
posted on 2018-01-16 17:51 f1yin9_0x5hark 阅读(2768) 评论(2) 编辑 收藏 举报