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

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

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

无法换出。

因为一个虚拟的,地址上连续的用户或内核缓冲区可能映射到多个不连续的物理页,所以 nt!_MDL 定长(0x1b 字节)的头部后紧

跟数量可变的页框号(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,其内的 Process 字段指向所属进程的 EPROCESS 结构,进程中的这块虚拟地址空间被 MDL 锁

住。

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

仅当 _MDL 的 MdlFlags 字段内设置了 MDL_MAPPED_TO_SYSTEM_VA 或 MDL_SOURCE_IS_NONPAGED_POOL 比特位,

MappedSystemVa 字段才有效。

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

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

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 地址处是一个 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 语言核心规范允许的,

但是上图那段代码在编译器会产生警告,如下:

-------- Rootkit 核心技术——利用 nt!_MDL 突破 KiServiceTable 的只读访问限制 Part II --------的更多相关文章

  1. Rootkit 核心技术——利用 nt!_MDL(内存描述符链表)突破 SSDT(系统服务描述符表)的只读访问限制 Part I

    -------------------------------------------------------- 在 rootkit 与恶意软件开发中有一项基本需求,那就是 hook Windows ...

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

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

  3. 利用淘宝ip库限制地区访问

    https://sss.one/97.html 利用淘宝ip库限制地区访问 有些应用可能需要对某些地区的用户进行限制访问 在采用才此方法的时候,可以利用一些ip库对访问者的ip进行判断 淘宝ip库地址 ...

  4. ----------- Rootkit 核心技术之绕过 IopParseDevice() 调用源检测逻辑 ---------------

    ---------------------------------------------------------------- 在上一篇文章中,我们已经看到 IopParseDevice() 如何对 ...

  5. (利用tempdata判断action是直接被访问还是重定向访问)防止微信活动中用户绕过关注公众号的环节

    说明:这个不是在进行微信公众号开发,也就是说在不能获取用户openid的前提下做的下面操作 1.动机:最近有个微信活动(关注了服务号的可以免费领取礼品),要做这么一个功能,活动的入口在微信服务号的菜单 ...

  6. 利用KeyVault来加强存储Azure Storage访问密钥管理

    很多时候管理Azure的存储账号我们都需要通过下面的界面管理访问密钥,大部分情况下通过密钥的轮替使用更新就可以做到安全管理了. 但是很多时候存储账号的Key就会不小心分发到开人员.测试人员.和管理员手 ...

  7. EF Core下利用Mysql进行数据存储在并发访问下的数据同步问题

    小故事 在开始讲这篇文章之前,我们来说一个小故事,纯素虚构(真实的存钱逻辑并非如此) 小刘发工资后,赶忙拿着现金去银行,准备把钱存起来,而与此同时,小刘的老婆刘嫂知道小刘的品性,知道他发工资的日子,也 ...

  8. 利用AMPScript获取Uber用户数据的访问权限

    现代项目开发和资产管理方法正在不停地快速变化.在这场创新和扩张的竞赛中,新资产被迅速部署并暴露于公共互联网,已有资产也在不断发展. 要跟上这个不断变化的攻击面是很难的,更不用说保护这些应用程序和系统了 ...

  9. 利用SSH隧道技术穿越内网访问远程设备

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/11899478.html 通常,我们用于调试的计算机无法远程访问位于局域网中的待调试设备. ...

随机推荐

  1. ASP.NET Core学习之三 NLog日志

    上一篇简单介绍了日志的使用方法,也仅仅是用来做下学习,更何况只能在console输出. NLog已是日志库的一员大佬,使用也简单方便,本文介绍的环境是居于.NET CORE 2.0 ,目前的版本也只有 ...

  2. 快速开发基于 HTML5 网络拓扑图应用1

    今天开始我们就从最基础解析如何构建 HTML5 Canvas 拓扑图应用,HT 内部封装了一个拓扑图形组件 ht.graph.GraphView(以下简称 GraphView)是 HT 框架中 2D ...

  3. Python的*args与**kwargs

    当Python的函数的参数不确定时,可以使用*args与**kwargs来指代不定数量的参数. 两者的区别是,*args是个tuple(元组),而**kwargs是个dict(字典). 先通过代码来验 ...

  4. [Python学习] Django 权限控制

    本文为大家讲解 Django 框架里自带的权限模型,从理论到实战演练,带领大家了解 Django 里权限是怎么一回事. 一.主要内容 1.什么是权限管理? 2.Web 权限 3.Django 权限机制 ...

  5. css实现平行四边形、菱形图片效果

    一.平行四边形 1. 使用两个元素实现 html <a class="button"> <div>click me</div> </a&g ...

  6. 千万别在开发阶段用 uglify 插件了!(from Requirejs to Webpack)

    webpack 各种好用,打算把 sf.gg 的前端构建工具从 gulp+requirejs 尝试着迁移到 webpack,没想到刚迈出第一步随即翻车. 因为 sf.gg 本质是个后端路由项目,每个页 ...

  7. Jeecg踩坑不完全指南

    公司用了这个叫做jeecg的快速开发框架,我不知道有多少公司在用这个框架,园子里有的可以吱一声.个人觉得这框架唯一优势就是可以让不会ssh的人也能进行开发,只要你会J2SE,有web后台发开经验即可. ...

  8. java字符串比较

    我最近刚学java,今天编程的时候就遇到一个棘手的问题,就是关于判断两个字符串是否相等的问题.在编程中,通常比较两个字符串是否相同的表达式是"==",但在java中不能这么写.在j ...

  9. 下载的js文件本地编辑器打开中文乱码如何解决

    今天遇到的小问题,已解决,直接上图 下载直接打开是这样的 用记事本打开 另存为utf-8格式 正常了!

  10. Java与算法之(9) - 直接插入排序

    直接插入排序是最简单的排序算法,也比较符合人的思维习惯.想像一下玩扑克牌抓牌的过程.第一张抓到5,放在手里:第二张抓到3,习惯性的会把它放在5的前面:第三张抓到7,放在5的后面:第四张抓到4,那么我们 ...