Rootkit 核心技术——利用 nt!_MDL(内存描述符链表)突破 SSDT(系统服务描述符表)的只读访问限制 Part I
————————————————————————————————————————————————————————
在 rootkit 与恶意软件开发中有一项基本需求,那就是 hook Windows 内核的系统服务描述符表(下称 SSDT),把该表中的
特定系统服务函数替换成我们自己实现的恶意例程;当然,为了确保系统能够正常运作,我们需要事先用一个函数指针保存原始
的系统服务,并且在我们恶意例程的逻辑中调用这个函数指针,此后才能进行 hook,否则损坏的内核代码与数据结构将导致
一个 BugCheck(俗称的蓝屏)。
尽管 64 位 Windows 引入了像是 PatchGuard 的技术,实时监控关键的内核数据,包括但不限于 SSDT,IDT,GDT。。。等等,
保证其完整性,但在 32 系统上修改 SSDT 是经常会遇到的场景,所以本文还是对此做出了介绍。
OS 一般在系统初始化阶段把 SSDT 设定成只读访问,这也是为了避免驱动与其它内核组件无意间改动到它;所以我们的首要任务
就是设法绕过这个只读属性。
在此之前,先复习一下与 SSDT 相关的几个数据结构,并解释定位 SSDT 的过程。
我们知道,每个线程的 _KTHREAD 结构中,偏移 0xbc 字节处是一枚叫做 ServiceTable 的泛型指针(亦即 PVOID 或 void*),
该字段指向一个全局的数据结构,叫做 KeServiceDescriptorTable,它就是 SSDT,SSDT 中首个字段又是一枚指针,指向
全局的数据结构 KiServiceTable,而后者是一个数组,其内的每个成员都是一枚函数指针,持有相应的系统服务例程入口地址。
有的时候,用言语来描述内核的一些概念过于抽象和词穷,还是来看看下图吧,它很形象地展示了上述关系:
根据上图我们有了思路:首先设法获取当前运行线程的 _KTHREAD 结构,然后即可逐步定位到 KiServiceTable,它就是我们最终
hook 的对象!
鉴于 ServiceTable 是一枚指针,持有另一枚指针 KeServiceDescriptorTable 的地址
(亦即“指向指针的指针”,往后我会不加以区分“持有”与“指向”术语),而 KiServiceTable 则是一个函数指针数组;
在 Rootkit 源码中,它们可以分别用三个全局变量(在驱动的入口点 DriverEntry() 之外声明 )表示,如下图,我使用了
“自注释”的变量名,很易于理解;而且我把星号紧接类型保留字后面,避免与“解引”操作混淆(所以星号是一个重载的运算
符):
————————————————————————————————————————————————————————
对于内核模式驱动程序开发人员来讲,自己实现一个例程来获取当前运行线程的 _KTHREAD 结构显然并不轻松,幸运的是,文档
化的 PsGetCurrentThread() 例程能够完成这一任务。
(事实上,PsGetCurrentThread()的反汇编代码恰恰说明了这很简单,如下代码,仅仅
只是把 fs:[00000124h] 地址处的内容移动到 eax 寄存器作为返回值,而且 KeGetCurrentThread() 的逻辑与它如出一撤! )
kd> u PsGetCurrentThread nt!PsGetCurrentThread:
83c6cd19 64a124010000 mov eax,dword ptr fs:[00000124h]
83c6cd1f c3 ret
83c6cd20 nop
83c6cd21 nop
83c6cd22 nop
83c6cd23 nop
83c6cd24 nop
nt!KeReadStateMutant:
83c6cd25 8bff mov edi,edi kd> u KeGetCurrentThread nt!PsGetCurrentThread:
83c6cd19 64a124010000 mov eax,dword ptr fs:[00000124h]
83c6cd1f c3 ret
83c6cd20 nop
83c6cd21 nop
83c6cd22 nop
83c6cd23 nop
83c6cd24 nop
老生常谈,fs 寄存器通常用来存放“段选择符”,“段选择符”用来索引 GDT 中的一个“段描述符”,后者有一个“段基址”
属性,也就是 KPCR(Kernel Processor Control Region,内核处理器控制区域)结构(nt!_KPCR)的起始地址;nt!_KPCR
偏移 0x120 字节处是一个 nt!_KPRCB 结构,后者偏移 0x4 字节处的“CurrentThread”字段就是一个 _KTHREAD 结构,每次
线程切换都会更新该字段,这就是 fs:[00000124h] 简洁的背后隐藏的强大设计思想!
注意,PsGetCurrentThread() 返回一枚指向 _ETHREAD 结构的指针(亦即“PETHREAD”,如你所见,微软喜欢在指针这一概念
上大玩“头文字 P”游戏),而 _ETHREAD 结构的首个字段 Tcb 就是一个 _KTHREAD 实例——这意味着,我们无需计算额外的
偏移量,只要考虑那个 ServiceTable 的偏移量 0xbc 即可,如下图:
而我们需要在这枚指针上执行加法运算,移动它到 ServiceTable 字段处,所以不能声明一个 PETHREAD 变量来存储
PsGetCurrentThread() 的返回值,因为“指针加上数值 n ”会把指针当前持有的地址加上( n * 该指针所指的数据类型大小 )个
字节—— 表达式
PETHREAD ethread_ptr += 0xbc;
实际上把起始地址加上了 0xbc * sizeof(ETHREAD) 个字节,远远超出了我们的预期。。。。
怎么办呢?好办,声明一个字节型指针来保存 PsGetCurrentThread() 的返回值,同时把返回值强制转型为一致的即可!
如此一来,表达式
BYTE* byte_ptr += 0xbc;
就是把起始地址加上 0xbc * sizeof(BYTE) 个字节,符合我们的预期。
注意,这要求我们添加相关的类型定义,如下图:
这表明 BYTE 与 无符号字符型等价(还等于微软自家的 UCHAR),大小都是单字节;DWORD 则与无符号长整型等价,大小都是
四字节——我们用一个 DWORD 变量存储数组 KiServiceTable 的地址。
————————————————————————————————————————————————————————
接下来就是通过一系列的指针转型和解引操作,定位到 KiServiceTable 的过程,再次凸显了指针在 C 编程中的地位,无论是应用
程序还是内核。。。。。经过如下图的赋值运算,最终,全局变量 os_ki_service_table 持有了 KiServiceTable 的地址。注意,除
了那个偏移量的宏定义外,所有的运算都在我们的驱动入口例程 DriverEntry() 中完成,而且为了支持动态卸载,我注册了
Unload() 回调,稍后你会看到 Unload() 的内部实现——大致就是卸载时取消对 KiServiceTable 的写权限映射。
————————————————————————————————————————————————————————————————————————————————
为了验证定位 KiServiceTable 过程的准确性,我添加了下列打印输出语句,注意,DbgPrint() 的输出需要在被调试机器上以
DbgView.exe 查看;抑或直接输出到调试机器上的 windbg.exe/kd.exe 屏幕上:
——————————————————————————————————————————————————————————————————————————————
结合上图,在调试器中进行验证——“dd”命令可以按双字(四字节)显示给定虚拟内存地址处的内容;“dps”命令可以按照函
数符号显示从给定内存地址开始的例程地址——它就是专为函数指针数组(例如 KiServiceTable)设计的,如下图:
——————————————————————————————————————————————————————————
现在,KiServiceTable 可以经由全局变量 os_ki_service_table 以只读形式访问,在我们 hook 它之前,需要设法更改为可写。
先来看看尝试向只读的 KiServiceTable 写入时会发生什么事情,如下图所示,我通过 RtlFillMemory() 试图向 KiServiceTable
持有的第一个四字节(亦即系统服务 nt!NtAcceptConnectPort )填充 4 个 ASCII 字符“A”:
————————————————————————————————————————————————————————————
注意,RtlFillMemory() 的第一个参数是一个指针,指向要被填充的内存块,后面二个参数分别是填充的长度与数据;由于我们的
变量 os_ki_service_table 是 DWORD 型,所以我把它强制转型为匹配的指针,再作为实参传入。。。。重新构建驱动,
放入以调试模式运行的虚拟机中加载,宿主机中发生的情况如下图所示,假设我们编译好的 rootkit 名称为
UseMdlMappingSSDT.sys ,
图中表明出现一个致命系统错误,代码为 0x000000BE,圆括号里边是携带错误信息的四个参数,在故障排查时会用到它们。
事实上,这就是一个 BugCheck,当错误检查发生时,如果目标系统连接着宿主机上的调试器,就断入调试器,否则目标系统
上将执行 KeBugCheckEx() 例程,后者会屏蔽掉所有处理器核上的中断事件,然后将显示器切换到低分辩率的 VGA 图形模式下,
绘制一个蓝色背景,然后向用户显示 “检查结果” 对应的停机代码。这就是“蓝屏”的由来。
——————————————————————————————————————————————————————————
在此场景中,我们得到一个 0x000000BE 的停机代码,将其作为关键字串搜索 MSDN 文档,给出的描述如下图:
————————————————————————————————————————————————————————
官方讲解的很清楚:0x000000BE(ATTEMPTED_WRITE_TO_READONLY_MEMORY)停机代码是由于驱动程序尝试向一个只读
的内存段写入导致的;第一个参数是试图写入的虚拟地址,第二个参数是描述该虚拟地址所在虚拟页-物理页的 PTE(页表项)
内容;后面两个参数为保留未来扩展使用,所以被我截断了。结合前面一张图我们知道,尝试写入的虚拟地址为
0x83CAFF7C,描述映射它的物理页的 PTE 内容是 0x03CAF121,后面两个参数就目前而言可以忽略。
如下图所示,0x83CAFF7C 就是 KiServiceTable 的起始地址;描述它的 PTE 经解码后的标志部分有一个“R”属性,表示
只读;BugCheck 时刻的栈回溯信息显示,内核中通用的异常处理程序 MmAccessFault() 负责处理与内存访问相关的错误,
它是一个前端解析例程,如果异常或错误能够处理,它就分发至实际的处理函数,否则,它调用 KeBugCheck*() 系列函数,
该家族函数会根据调试器的存在与否作出决定——要么调用 KiBugCheckDebugBreak() 断入调试器;要么执行如前文所述的操作
流程来绘制蓝屏:
————————————————————————————————————————————————————————————
至此确定了 BugCheck 是由于在驱动中调用 RtlFillMemory() 写入只读的内核内存引发的。另一个更强大的调试器扩展命令
“!analyze -v”可以输出详细的信息,包括 BugCheck “现场”的指令地址和寄存器状态,如下图所示,导致 BugCheck 的
指令地址为 0x9ff990b4,该指令把 eax 寄存器的当前值(0x41414141,亦即我们调用 RtlFillMemory() 传入的 4 个 ASCII 字
符“A”)写入 ecx 寄存器持有的内存地址处,试图把 nt!NtAcceptConnectPort() 的入口点地址替换成 0x41414141 ;另外它会
给出驱动源码中对应的行号——也就是第 137 行的 RtlFillMemory() 调用:
——————————————————————————————————————————————————————————
如你所见,微软 C/C++ 编译器(cl.exe)把 RtlFillMemory() 内联在它的调用者内部,换言之,尽管有公开的文档描述它的
返回值,参数。。。。具体的实现还是由编译器说了算——为了性能优化,RtlFillMemory() 直接实现为一条简洁的数据移动
指令,相关的参数由寄存器传递,没有因函数调用创建与销毁栈帧带来的额外开销!
到目前为止,尽管我们通过一系列步骤从 _KTHREAD 定位到了系统服务指针表,但以常规手段却无法 hook 其中的系统服务函
数,因为它是只读的。
下一篇文章我将讨论如何使用 MDL(Memory Descriptor List,内存描述符链表)来绕过这种限制,随心所欲地读写
KiServiceTable!
Rootkit 核心技术——利用 nt!_MDL(内存描述符链表)突破 SSDT(系统服务描述符表)的只读访问限制 Part I的更多相关文章
- -------- Rootkit 核心技术——利用 nt!_MDL 突破 KiServiceTable 的只读访问限制 Part II --------
------------------------------------------------------------------------------------------- 本篇开始进入正题 ...
- Linux之内存描述符mm_struct
Linux对于内存的管理涉及到非常多的方面,这篇文章首先从对进程虚拟地址空间的管理说起.(所依据的代码是2.6.32.60) 无论是内核线程还是用户进程,对于内核来说,无非都是task_struct这 ...
- 进程—内存描述符(mm_struct)
http://blog.csdn.net/qq_26768741/article/details/54375524 前言 上一篇我们谈论了task_struct这个结构体,它被叫做进程描述符,内部成员 ...
- -------- ROOTKIT 核心技术——系统服务调度表挂钩调试(PART III) --------
---------------------------------------------------------------------------------------- 本篇开始进行真枪实弹的 ...
- python2.7高级编程 笔记二(Python中的描述符)
Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解.这些特性包括列表/集合/字典推导式,属性(property).以及装饰器(decorator).对于大部分特性来说,这些" ...
- 【python】描述符descriptor
开始看官方文档,各种看不懂,只看到一句Properties, bound and unbound methods, static methods, and class methods are all ...
- Python描述符(descriptor)解密(转)
原文:http://www.geekfan.net/7862/ Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解.这些特性包括列表/集合/字典推导式,属性(property).以及装 ...
- Linux中断技术、门描述符、IDT(中断描述符表)、异常控制技术总结归类
相关学习资料 <深入理解计算机系统(原书第2版)>.pdf http://zh.wikipedia.org/zh/%E4%B8%AD%E6%96%B7 独辟蹊径品内核:Linux内核源代码 ...
- Python——描述符(descriptor)解密
本文由 极客范 - 慕容老匹夫 翻译自 Chris Beaumont.欢迎加入极客翻译小组,同我们一道翻译与分享.转载请参见文章末尾处的要求. Python中包含了许多内建的语言特性,它们使得代码简洁 ...
随机推荐
- Asp,NET控制文件上传的大小
在web.config中的system.web 节点下添加如下代码: 第2行的maxRequestLength="8192",这里限制最大为8MB,可以自行设置.execution ...
- Xamarin.Android 使用Timer 并更改UI
http://blog.csdn.net/ozhangsan12345/article/details/72653070 第一步:创建timer对象 //创建timer对象 Timer _dispat ...
- Ubuntu 16.04 升级 PHP 版本至 7.1
安装swoole扩展,怎么安装到7.0下去了,我本来编译的版本是7.19版本,但是没吃 升级步骤 $ sudo add-apt-repository ppa:ondrej/php $ sudo apt ...
- vmware一步步安装centos
软件环境:vmware10.0破解版 centos版本:6.4 1.启动vmware,新建虚拟机,选择自定义安装 2,出现如下界面,保持默认,点击下一步 3.这个步骤要特别注意,选择“稍后安装”,我们 ...
- Hibernate学习笔记(4)---hibernate的核心接口
Configuration类 该类主要是读取配置文件,启动hibernate,并负责管理hibernate的配置信息,一个程序只创建一个Configuration对象. Configuration类操 ...
- CDN 边缘规则,三秒部署、支持定制、即时生效,多种规则覆盖常用业务场景
2017年的最后一周,又拍云进行了一次重要升级,将自定义 Rewrite 升级为"边缘规则".互联网应用场景的日益多样化,简单.方便.快速的根据不同应用场景实现不同的功能变得越来越 ...
- CSS Content 属性
content 属性,用在after,before伪元素上,用于在选中的元素前后插入内容. 插入的内容多种多样, 纯文字 h1::after{ content:"h1后插入内容" ...
- JS 时间转换为时间戳
Date.prototype.format = function(fmt) { var o = { "M+" : this.getMonth()+1, //月份 "d+& ...
- Mtklog 分析(学习)
常见log异常类型: JE NE KE EE(external(modem) exception) 一.常见应用异常搜索关键信息 1. 常见应用crash的(系统提示***已停止运行) ,可以在ma ...
- JAVA读取和写入properties文件
1.读取 Properties prop = new Properties(); try { //这个getResourceAsStream方法就是把文件转为inputStream的方式 prop.l ...