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 这种系统范围的调用表,地位非常重要,如果被换出物理内
存,系统岂不就崩溃了,所以坦白讲我们只是因为需要写权限才调用它的。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-1-11 19:35
被kanxue编辑
,原因: