新闻资讯

原创]---RootKit 核心技术——利用 NT

来源:bet9娱乐场-bet9注册-bet9官网发布时间:2020-01-13 01:24:50浏览:5

  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 结构。

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

  Windows 中某些类型的驱动程序,例如网络栈,它们支持 MDL 链,其内的多个 MDL 描述的那些缓冲区实际上是零散的,

  假设栈中每个驱动都分配一个 MDL,其后跟着一些物理页框号来描述它们各自用到的虚拟缓冲区,那么这些缓冲区就通过

  每个 _MDL 的 Next 字段(指向下一个 MDL)链接起来。

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

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

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

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

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

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

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

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

  如果你现在看得云里雾里,不用担心,后面我们在调试时会把描述 KiServiceTable 的一个 nt!_MDL 结构实例拿出来分析,

  到时候你就会恍然大悟这些字段的设计思想了。

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

  属物理页锁定在内存中,并赋予对这张页面的读写访问权限(实际是将描述该页面的 PTE 内容中的 “R” 标志位修改成 “W”)

  如此一来,KiServiceTable 的原始虚拟地址与新映射的虚拟地址都转译到相同的物理地址,而且描述新虚拟地址的 PTE 内容标记了

  写权限比特位,这样我们就能够通过修改这个新的虚拟地址中的系统服务例程实现安全挂钩 KiServiceTable,不会导致BugCheck。

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

  把它从 DWORD 转换为泛型指针是为了符合 MapMdl() 中的 IoAllocateMdl() 调用时的形参要求;最后一个参数——表达式

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

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

  调用链的每一阶段发生变化,所以我总共添加了三个断点在相关的检查区域,有助于我们在后面的调试过程中深入理解 nt!_MDL

  我把使用 Windows 执行体组件例程进行的操作放入一个 try-except 块内,以便处理可能出现的异常,except 块内的逻辑如下

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

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

  IoAllocateMdl() 的第二个参数指定要通过分配的 MDL 描述的缓冲区的大小。如果这个长度小于 4KB,

  那么映射它的 MDL 就只描述了一个被锁定的物理页面;

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

  此例程支持的最大缓冲区长度(以字节为单位)是:

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

  写访问的关键所在,能否锁定内存倒是其次,因为像 KiServiceTable 这种系统范围的调用表,地位非常重要,如果被换出物理内

  存,系统岂不就崩溃了,所以坦白讲我们只是因为需要写权限才调用它的。

  第二个断点紧跟其后,这样就可以在调试器中检查 MmProbeAndLockPages() 是如何修改 MDL 中的标志;也可以使用编程手段

  驱动加载到的内核空间某处;局部变量 mapped_addr 持有这个新地址,最终用来返回并初始化全局变量mapped_ki_service_table。

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

  接下来的逻辑有助于你理解 MDL 头部后面的 PFN 数组:mdl_ptr 指向 nt!_MDL 结构头部,把它加上 1 ,意味着把它持有的

  内存地址加上 1 * sizeof(MDL) 个字节,于是就定位到了 MDL 头部后面的 PFN 数组起始地址——现在全局变量

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

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

  后者就是实际上存储 KiServiceTable 的地方。接下来,我们用一枚函数指针保存 KiServiceTable 中某个原始的系统服务,

  然后用我们的钩子例程地址替换掉该位置处的原始系统服务,而钩子例程内部仅仅是调用原始系统服务,实现安全转发。

  为了演示简单起见,我选取 KiServiceTable 中 0x39(57)号例程,因为它的参数只有一个,方便我们的钩子例程仿效同样的

  参数声明——内核系统服务调度器(nt!KiFastCallEntry())并不知道它调用的目标系统服务已经被替换成我们的钩子例程,

  所以他会以既定方式使用钩子例程的返回值和输出参数,在这种情况下,只要我们的钩子例程原型声明与被挂钩系统服务有

  细微差别,都可能导致非预期的内核错误而蓝屏,显然,那些参数既多又复杂的系统服务不适合我用来演示。

  此外,某些系统服务接收的参数类型的定义不在 wdm.h / ntddk.h 头文件内,讲明了这些数据类型不是给驱动开发人员使用的,

  仅供内核组件使用,为了引入包含该定义的头文件则会碰到复杂的头文件嵌套包含问题,其麻烦程度丝毫不逊于 Linux 平台上

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

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

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

  通过 typedef 定义一个函数指针,其返回值类型与形参类型与 NtCompleteConnectPort() 一致,然后声明一个该函数指针

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

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

  mapped_ki_service_table 是普通指针,它的数组名称表示法结合数组下标,实际上被视为一个存储对应元素的 DWORD 变量,

  就目前而言我们可以无视这两条警告,因为含有这段代码的 rootkit 源码在编译后确实能够安全地 hook 目标系统服务函数,系统

  正常运作不会有问题,类似的警告可以通过指定警告级别的编译选项来过滤掉。

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

  讲到这里你一定会嫌我既罗嗦又婆婆妈妈的,那么来看下面这一张简明扼要的全局概览,它解释了 MDL 是如何把一片缓冲区

  映射到另一处,并描述两者相同的物理布局,注意,图中的组织结构是执行完 MmGetSystemAddressForMdlSafe() 后才会产生的。

  注意,上图中我没有给出 PFN 数组中第一个成员携带的具体 20 位物理页框号,原始和映射到的新内核缓冲区,以及实际 RAM

  中的物理页框号,而“byte within page”就是页内特定偏移处开始的字节序列,亦即系统服务例程入口点的实际物理地址!

  这些 “占位符” 我会在第三部分的调试单元内给出,毕竟,驱动开发与调试是相辅相成的,只有理论没有实践怎么行,只有源码

  没有调试怎知真理,不然,任何人对于内存的需求就线 K 了。。。。。

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

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

  [2020元旦礼物]《看雪论坛精华17》发布!(补齐之前所有遗漏版本)!

  简单来说,MDL是用来描述一段物理内存,将物理内存映射成虚拟内存,然后修改页面属性,从而绕过只读.

  归根到底,物理内存没有任何保护,所有的保护都在页表中实现,基本也就是这里面的猫腻,同样的物理地址指向不同的线性地址,同样的线性地址指向不同的物理地址==。

  wowocock归根到底,物理内存没有任何保护,所有的保护都在页表中实现,基本也就是这里面的猫腻,同样的物理地址指向不同的线性地址,同样的线性地址指向不同的物理地址==。

  之前一直纠结,是如何办到的,内核中对应着什么数据结构了?

  本来睡着了,爬起来看了下reactos的实现,自问自答吧:

  如果是用户地址锁住进程的工作集,如果是内核地址使用的pfnLock

  又对着wrk看了一遍,竟然没看懂,有人能对着wrk看下,说下怎么实现的吗?

  2018-4-16 14:30 被又出bug了编辑 ,原因:

  wowocock归根到底,物理内存没有任何保护,所有的保护都在页表中实现,基本也就是这里面的猫腻,同样的物理地址指向不同的线性地址,同样的线性地址指向不同的物理地址==。

  等第三部分调试章节出来,您就知道其中奥秘了 ^^

  又出bug了wowocock 归根到底,物理内存没有任何保护,所有的保护都在页表中实现,基本也就是这里面的猫腻,同样的物理地址指向不同的线性地址,同样的线性地址指向不同的 ...

  请问大神。WIN7以上的系统。R3层还有办法直接访问内核层?比如R3使用调用门,中断门?

  冰雄请问大神。WIN7以上的系统。R3层还有办法直接访问内核层?比如R3使用调用门,中断门?

  win7 以上不太清楚,不过我猜底层的用户-内核隔离机制都大同小异:Ring3 无法直接访问 Ring0,要么得通过内联汇编 int 2e 或

  sysenter/syscall 指令;要么得通过 ntdll.dll 中的系统服务前端函数;Ring3 使用调用门或陷阱门,恐怕无法通过段描述符的权限检查;就算在用户模式直接用 int 2e 或 sysenter,也是无法通过段权限检查的吧

  2018-4-23 18:26 被shayi编辑 ,原因:

  段权限检查。可以在GDT或 idt中加入一致代码段的调用门或中断门,tss段。达到低权限访问高权限。。最近看了滴水的教程。可以通过tss硬件切换任务。架空系统来绕过权限检查,只是自己测试失败。

  冰雄段权限检查。可以在GDT或 idt中加入一致代码段的调用门或中断门,tss段。达到低权限访问高权限。。最近看了滴水的教程。可以通过tss硬件切换任务。架空系统来绕过权限检查,只是自己测试失败。

  这部分课题我涉猎较少,期待您后续的研究心得分享!

  域名:加速乐SSL证书:公众号:ikanxue

 
bet9娱乐场-bet9注册-bet9官网