----------- Rootkit 核心技术之绕过 IopParseDevice() 调用源检测逻辑 ---------------
————————————————————————————————————————————————————————————————
在上一篇文章中,我们已经看到 IopParseDevice() 如何对传入的 OPEN_PACKET 结构进行验证。假设 ObReferenceObjectByName() 的调用者没有分配并初始化第七个参数 ParseContext,而仅是简单地传入 “NULL” ,那么当调用链深入到 IopParseDevice() 内部时,就会因验证失败返回 C0000024(STATUS_OBJECT_TYPE_MISMATCH)。
我们根据源码中的暗示来追踪 OPEN_PACKET 结构究竟在哪分配的,如前所述,调用链 NtCreateFile->IoCreateFile()->IopCreateFile() 的结尾,也就是在 IopCreateFile() 内部,实际负责 OPEN_PACKET 的初始化。下面贴出的代码片段以 NT 5.2 版内核源码为样例:
也就是说,我们直接复制 IopCreateFile() 中的 OPEN_PACKET 结构初始化部分逻辑就行了?
这里还有一个问题,负责分配该结构体内核内存的例程 IopAllocateOpenPacket() 是一个宏,Visual C++ 2015 中给出它是用 ExAllocatePoolWithTag() 定义的。这就好办了,在我们自己的驱动源码中,添加相应定义即可,如下图:
————————————————————————————————————————————————————————————
因为 OPEN_PACKET 结构同样没有公开的文档来描述,所以要么在我们的驱动源码中用 “#include” 包含定义它的头文件,要么直接复制定义的那一部分黏贴进来。很显然,后者比较轻松——OPEN_PACKET 在内核源码的 “iomgr.h” 中定义,而该头文件又嵌套包含了一堆杂七杂八的内核头文件,要理清这些嵌套包含关系很麻烦,而且最重要的是,其中一些头文件定义的数据类型会与驱动开发中用的 “ntddk.h” 和“wdm.h”重复,引起编译器的抱怨。所以直接在 “iomgr.h” 中搜索字串 “typedef struct _OPEN_PACKET”,把找到的定义块拷贝进来即可。
然而,OPEN_PACKET 结构中唯有一个字段不是 “原生” 定义的——这就是 “PDUMMY_FILE_OBJECT” 类型,需要包含其它头文件才不致使编译器报错。
我的解决方案是,直接把该字段的声明所在行注释掉,下图展示了该字段具体的位置(在 “iomgr.h” 中的行号),方便各位快速查找:
——————————————————————————————————————————————————————————————————
注意,NT 6.1 版内核在编译时刻的 OPEN_PACKET 结构显然是未经 “恶意” 修改的,所以编译器为其 “sizeof(OPEN_PACKET)” 表达式计算 0x70 的值,而我们在自己的驱动中拿掉了 OPEN_PACKET 其中一个字段使得编译器为表达式 “sizeof(OPEN_PACKET)” 预计算 0x58 的值(后面的调试阶段会验证),这会造成 “Size” 字段不是 IopParseDevice() 内部逻辑预期的 0x70,从而导致返回 C0000024(STATUS_OBJECT_TYPE_MISMATCH)。
解决办法也很简单,我们的驱动中,不要依赖编译时刻的计算,直接把 “Size” 字段的值硬编码为 0x70 不就好了?
如下图所示,你还会注意到,我把 “Type” 字段的常量 “IO_TYPE_OPEN_PACKET” 改成了对应的数值,以确保万一。
另外,由于 IopAllocateOpenPacket() 等价于 ExAllocatePoolWithTag(),而后者通常返回泛型指针(“ PVOID ”,亦即 “ void * ”),
所以我强制把它转型为与 “openPacket” 一致的类型。
万事俱备,“东风” 就在于调用 ObReferenceObjectByName() 时,为第七个参数传入“openPacket” 即可,上图显示的很清楚了。
——————————————————————————————————————————————————————————————————
很不幸的是,我把编译出来的驱动放到虚拟机(Windows 7,基于 NT 6.1 版内核)里面动态加载测试,还是无法获取到
“\Device\QQProtect” 相应的设备对象指针,ObReferenceObjectByName() 返回 C0000024。
为了找出故障原因,我在分配 OPEN_PACKET 逻辑的前面利用内联汇编添加了一个软中断 “__asm{ int 3; } ”,宿主机器上启动内核调试器 kd.exe,我的启动参数像是这样:
kd.exe -n -v -logo d:\virtual_machine_debugging.txt -y SRV*C:\Symbols*http://msdl.microsoft.com/download/symbols -k com:pipe,port=\\.\pipe\com_1,baud=115200,reconnect
参数 “logo” 指定要把整个调试过程的输出信息写入日志;
“-y” 指定符号文件的位置(机器指令中没有内核函数与变量的符号,所以调试器需要查找额外的符号以向用户显示人类可读的名称);
“-k” 参数指定调试类型为 “命名管道模拟串口1”,波特率数值越高,响应越快。
把重新编译好的驱动放到虚拟机中,在提升权限后的命令提示符中执行 bcdedit.exe,启用调试模式,这样重启虚拟机后,就会进入调试模式(无需在启动过程中按下 F8 选择菜单)。
我把自己的驱动实现成按需加载,也就是利用服务控制管理器(sc.exe)发出命令来动态加载和卸载,实现此功能的相应批处理文件内容如下图,注意该文件要放在虚拟机中执行,“start= demand” 表明通过 sc.exe 按需启动;“binpath” 就是驱动文件存放的磁盘路径,假设我的驱动名为 hideprocess.sys,执行该批处理任务后,就在相关的注册表位置添加了一项,往后只需在 cmd.exe 中执行 “sc.exe start/stop hideprocess” 就能够动态加卸载。
按照上述方式加载时,就会自动触发我们设定好的软件断点,即可在宿主机中检查虚拟机的内核空间。
另外还需注意一点:编译驱动时的 “构建” 环境应该选择 Check Build,这样会一并生成同名称的符号文件,后缀为 “.pdb”,从而调试器能够显示我们自己驱动中的函数与变量名称,提高调试效率,如下图:
——————————————————————————————————————————————————————————————————————
触发软件断点后,我们一般会用 “kv” 命令查看栈回溯信息,它披露出我们的驱动入口点 DriverEntry() 是由 I/O 管理器的 IopLoadDriver() 调用的:
栈的顶层函数 “ReferenceDeviceAndHookIRPdispatchRoutine+0x56” 是我添加软中断的地方。执行 “r” 命令查看当前的 x86 通用寄存器状态,EIP 指向地址 0x8f4a3196 ,执行 “u hideprocess!ReferenceDeviceAndHookIRPdispatchRoutine+0x56 L2”,反汇编输出的第一行地址就是 0x8f4a3196,与 EIP 的值相符;第二行是把一个 16 进制值 “ 704F6F49h” 压栈,实际上它是 ASCII 字符 “pOoI” 的 16 进制编码,换言之,这是在通过内核栈传递 ExAllocatePoolWithTag() 的第三个参数(从右往左传递,请回顾之前的 IopAllocateOpenPacket() 宏定义那张图)。
————————————————————————————————————————————————————————————————
继续按下 “t” 单步执行,如下图所示,你可以看到,ExAllocatePoolWithTag() 的第二个参数,分配的内核内存大小为 0x70 字节,因为我在宏定义中硬编码了这个值,而不是用 sizeof(OPEN_PACKET) 表达式让编译器计算;另一方面,图中的 “dt” 命令也证实了它的大小为 0x70 字节。
首个传入的参数 “NonPagedPool” 为不可换页池,其内的数据无法被换出物理内存,该常量对应的数值为 “0”:
我不想浪费时间在查看内核内存的分配细节上,所以我按下 “p”,步过 ExAllocatePoolWithTag() 函数调用,接下来的 cmp/jne 汇编序列对应源码中检查是否成功分配了内存并用于 openPacket 指针,实际的执行结果是跳转到地址 0x8f4a31c6 ,对应源码中初始化 OPEN_PACKET 结构前两个字段的逻辑:
接下来一直单步执行到调用 ObReferenceObjectByName() 前夕,在此处我们要 “步入” 它的内部,进行故障排查,所以按下 “t” 跟进,这里有一个小技巧,我们已经分析过 ObReferenceObjectByName() 的源码,知道它会调用很多函数,而且大致清楚问题出现在 ObpLookupObjectName() 里面,所以指令 “tc”可以跟踪到每个函数调用处停止,再由用户决定是否跟进该函数内部。
这是我的美好梦想,但现实总是残酷的,在我跟踪到原子操作系列函数
nt!ExInterlockedPopEntrySList() 调用时,kd.exe 就卡住了,无法继续追踪此后的调用链。从稍早的栈回溯信息来看,与源码中和我们预测的调用序列大致相符,只是不晓得为啥在 nt!ObpAllocateObjectNameBuffer() 中,为了给传入的驱动对象名称 “\Device\QQProtect” 分配内核内存,调用 nt!ExInterlockedPopEntrySList(),而后者却无法追踪。。。。是虚拟机环境的缘故,还是原子操作类函数的不可分割性质?
——————————————————————————————————————————————————————————————
讲一点废话,一般我们在栈回溯中看到的顶层说明行,有一个 “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 指针,该结构中存储的是我们驱动在注册表中的完整路径:
——————————————————————————————————————————————————————————————————
总而言之 ,基于以上理由我无法继续跟进到 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)环境。
————————————————————————————————————————————————————————————————
----------- Rootkit 核心技术之绕过 IopParseDevice() 调用源检测逻辑 ---------------的更多相关文章
- Spring Boot核心技术之Rest映射以及源码的分析
Spring Boot核心技术之Rest映射以及源码的分析 该博客主要是Rest映射以及源码的分析,主要是思路的学习.SpringBoot版本:2.4.9 环境的搭建 主要分两部分: Index.ht ...
- 如何绕过CDN找源站ip?
这是一个总结帖,查了一下关于这个问题的国内外大大小小的网站,对其中说的一些方法总结归纳形成,里面具体发现ip的方法不是原创,所有参考的原贴都也贴在了后面,大家可以自行看看原贴. 首先,先要明确一个概念 ...
- .NET:线程本地存储、调用上下文、逻辑调用上下文
.NET:线程本地存储.调用上下文.逻辑调用上下文 目录 背景线程本地存储调用上下文逻辑调用上下文备注 背景返回目录 在多线程环境,如果需要将实例的生命周期控制在某个操作的执行期间,该如何设计?经典的 ...
- Rootkit 核心技术——利用 nt!_MDL(内存描述符链表)突破 SSDT(系统服务描述符表)的只读访问限制 Part I
-------------------------------------------------------- 在 rootkit 与恶意软件开发中有一项基本需求,那就是 hook Windows ...
- -------- Rootkit 核心技术——利用 nt!_MDL 突破 KiServiceTable 的只读访问限制 Part II --------
------------------------------------------------------------------------------------------- 本篇开始进入正题 ...
- -------- ROOTKIT 核心技术——系统服务调度表挂钩调试(PART III) --------
---------------------------------------------------------------------------------------- 本篇开始进行真枪实弹的 ...
- hadoop map任务Combiner被调用的源码逻辑简要分析
从MapTask类中分析下去,看一下map任务是如何被调用并执行的. 入口方法是MapTask的run方法,看一下run方法的相关介绍: org.apache.hadoop.mapred. ...
- SpringCloud服务调用源码解析汇总
相信我,你会收藏这篇文章的,本篇文章涉及Ribbon.Hystrix.Feign三个组件的源码解析 Ribbon架构剖析 这篇文章介绍了Ribbon的基础架构,也就是下图涉及到的6大组件: Ribbo ...
- 【转载】绕过CDN找到源站的思路
[原文:https://mp.weixin.qq.com/s/8NUvPqEzVjO3XbmCBukUvQ] 绕过CDN的思路 网上有很多绕过CDN的思路,但是存在很多问题,以下是收集并总结的思路.站 ...
随机推荐
- 非等高cell实战(01)-- 实现微博页面
非等高cell实战(01)-- 实现微博页面 学习过UITableView.AutoLayout以及MVC的相关知识,接下来通过一个微博页面实战来整合一下. 首先看一下效果图: 需求分析 此页面为非等 ...
- idea和Webstorm上使用git和github,码云
由于之前一直使用svn,现在项目使用git,顾根据网上找的学习资料,自己梳理了下,收获蛮多,这里做个记录,如果能帮助到您那是最好不过的. 1.大致步骤 使用工具:idea,github,码云 webs ...
- DataBase MongoDB高级知识-易扩展
MongoDB高级知识-易扩展 应用程序数据集的大小正在以不可思议的速度增长.随着可用宽带的增长和存储器价格的下跌,即使是一个小规模的应用程序,需要存储的数据也可能大的惊人,甚至超出了很多数据库的处理 ...
- OC学习12——字符串、日期、日历
前面主要学习了OC的基础知识,接下来将主要学习Foundation框架的一些常用类的常用方法.Foubdation框架是Cocoa编程.IOS编程的基础框架,包括代表字符串的NSString(代表字符 ...
- 字符串MD5加密运算
public static string GetMd5String(string str) { MD5 md5 = MD5.Create(); by ...
- 【ASP.NET Core】运行原理(4):授权
本系列将分析ASP.NET Core运行原理 [ASP.NET Core]运行原理(1):创建WebHost [ASP.NET Core]运行原理(2):启动WebHost [ASP.NET Core ...
- Head First设计模式之装饰者模式
一.定义 装饰者模式,英文叫Decorator Pattern,在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能.它是通过创建一个包装对象,也就是装饰来包裹真实的对象. 动态将职责附加到 ...
- Python核心编程笔记--私有化
一.私有化的实现 在Python中想定义一个类是比较简单的,比如要定义一个Person类,如下代码即可: # -*- coding: utf-8 -*- # __author : Demon # da ...
- 什么是ObjCTypes?
先看一下消息转发流程: 在forwardInvocation这一步,你必须要实现一个方法: - (NSMethodSignature *)methodSignatureForSelector:(SEL ...
- Java 测试驱动开发--“井字游戏” 游戏实战
TDD 介绍 TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论.TDD的原理是在开发功能代码之前,先编写单元测试用 ...