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

在 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的更多相关文章

  1. -------- Rootkit 核心技术——利用 nt!_MDL 突破 KiServiceTable 的只读访问限制 Part II --------

    ------------------------------------------------------------------------------------------- 本篇开始进入正题 ...

  2. Linux之内存描述符mm_struct

    Linux对于内存的管理涉及到非常多的方面,这篇文章首先从对进程虚拟地址空间的管理说起.(所依据的代码是2.6.32.60) 无论是内核线程还是用户进程,对于内核来说,无非都是task_struct这 ...

  3. 进程—内存描述符(mm_struct)

    http://blog.csdn.net/qq_26768741/article/details/54375524 前言 上一篇我们谈论了task_struct这个结构体,它被叫做进程描述符,内部成员 ...

  4. -------- ROOTKIT 核心技术——系统服务调度表挂钩调试(PART III) --------

    ---------------------------------------------------------------------------------------- 本篇开始进行真枪实弹的 ...

  5. python2.7高级编程 笔记二(Python中的描述符)

    Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解.这些特性包括列表/集合/字典推导式,属性(property).以及装饰器(decorator).对于大部分特性来说,这些" ...

  6. 【python】描述符descriptor

    开始看官方文档,各种看不懂,只看到一句Properties, bound and unbound methods, static methods, and class methods are all ...

  7. Python描述符(descriptor)解密(转)

    原文:http://www.geekfan.net/7862/ Python中包含了许多内建的语言特性,它们使得代码简洁且易于理解.这些特性包括列表/集合/字典推导式,属性(property).以及装 ...

  8. Linux中断技术、门描述符、IDT(中断描述符表)、异常控制技术总结归类

    相关学习资料 <深入理解计算机系统(原书第2版)>.pdf http://zh.wikipedia.org/zh/%E4%B8%AD%E6%96%B7 独辟蹊径品内核:Linux内核源代码 ...

  9. Python——描述符(descriptor)解密

    本文由 极客范 - 慕容老匹夫 翻译自 Chris Beaumont.欢迎加入极客翻译小组,同我们一道翻译与分享.转载请参见文章末尾处的要求. Python中包含了许多内建的语言特性,它们使得代码简洁 ...

随机推荐

  1. Mac说——关闭SIP

    今天在安装keras的时候总是提示numpy无法安装,百度了下,说是新版本的os系统加入了spi机制. 什么是SIP: 系统集成保护(System Integrity Protection,SIP), ...

  2. Python学习_07_错误、异常

    地毯式地过语法终于快要结束了... Python中的常见异常 1.NameError:尝试访问一个未初始化的变量 2. ZeroDivisionError:除数为0 3. SyntaxError:Py ...

  3. java 集合类基础问题汇总

     1.Java集合类框架的基本接口有哪些? 参考答案 集合类接口指定了一组叫做元素的对象.集合类接口的每一种具体的实现类都可以选择以它自己的方式对元素进行保存和排序.有的集合类允许重复的键,有些不允许 ...

  4. Selectize使用总结

    一.简介 Selectize是一个可扩展的基于jQuery 的自定义下拉框的UI控件.它对展示标签.联系人列表.国家选择器等比较有用.它的大小在~ 7kb(gzip压缩)左右.提供一个可靠且体验良好的 ...

  5. Head First设计模式之代理模式

    一.定义 定义:为其他对象提供一种代理以控制对这个对象的访问 在代理模式中,我们创建具有现有对象的对象,以便向外界提供功能接口. 二.结构 代理模式一般会有三个角色: 抽象角色(Subject):指代 ...

  6. mp3格式转wav格式 附完整C++算法实现代码

    近期偶然间看到一个开源项目minimp3 Minimalistic MP3 decoder single header library 项目地址: https://github.com/lieff/m ...

  7. sql servel 报错:将 expression 转换为数据类型 int 时出现算术溢出错误。

    执行sql语句:SELECT   AVG( DATEDIFF(s,s.CreatedDate,s.SendDate)  ) AS submitTime FROM dbo.SmsSend AS s    ...

  8. asp.net core 教程(五)-配置

    Asp.Net Core-配置 Asp.Net Core-配置 在这一章,我们将讨论 ASP.NET Core项目的相关的配置.在解决方案资源管理器中,您将看到 Startup.cs 文件.如果你有以 ...

  9. 有具体名称的匿名函数var bar = function foo(){}

    http://kangax.github.io/nfe/ 命名的函数表达式 函数表达式实际上可以经常看到.Web开发中的一个常见模式是基于某种特性测试来"分叉"函数定义,从而获得最 ...

  10. JAVA面向对象的三大特性 封装

    将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问. 优点: 1只能通过规定的方法访问数据. 2隐藏类的实例细节,方便修改和实现. public c ...