窥探CVE-2018-8120与exploit编写
2021-2-1 14:41:28 Author: mp.weixin.qq.com(查看原文) 阅读量:1 收藏

环境: win7  x86

一、 概要

前面的文章写了,要实现一个exploit的编写,分析的方向就要放在漏洞点的下半部分,

本文写的是通过IOCL对象进行的池溢出,参考的是b2ahex的文章,因为别的都是利用是BITMAP进行布局。

二、 自己对Exploit的理解

要写一个Exploit,首先就要梳理以下几点:

1. 目前该漏洞的利用方向是什么(好像大多数都是提权)。

2. 根据利用方向再寻找利用的位置,以及该位置的元素(参数)是否可控。

因为CVE-2018-8120的利用点是任意读写,最大利用就是提权。因此我们从提权的角度开始研究。

1. 首先要明白的事情是何为提权?

用语言描述就是权限提升,从普通用户权限提升到了管理员权限。那么对应到代码的世界是什么呢?其实就是csssEIPESP(也诠释了各种门的设计)(微观角度),如果在加上一条那便是ACL机制-常用就是TOKEN)(宏观角度)的控制。

举个列子:

从应用层代码到了内核层,但是由于通过漏洞我们控制了csssEIPESP,从而执行了我们预先安排好的代码,创建了CMD进程,并进行了token的替换,在用户层使用whoami指令查看用户,就会发现是管理员权限。

(1) 从上面自己的看法中,因此编写这个程序分为两点去考虑。

① 微观的把控,其实就是寻找可以任意读写的位置,通过这个位置,执行指定位置的shellcode

② 宏观的把控,修改ACL的规则,常用的就是替换Token

(2) 通过中断门实验看待这2点,我觉得可以得出,宏观的把控是为了上层更好的利用,以及简便的考虑,其实微观的把控才是最重要的。因为中断门就是把控了微观的角度,从而让R3某个函数具有R0的权限。

2. 简而言之,exploit就是对微观和宏观控制的诠释。

三、 分析

1. 找到可以利用的位置

从漏洞点的位置出发,可以看到敏感函数 qmemcpy,那么它是否就是利用点呢?还要看它的参数是否能被我们控制。

从上图不难看出,它的第一个参数V4的来源于V3V2的参数来源于这个函数本身的参数。于是焦点就变成了这个函数的两个参数是否可控,那便就要看上一层。

这个函数的v2来源于GetProcessWindowStation函数,对于分析过这个漏洞的人,应该知道这个是可控的,而V4来源于V1。此时我们可以可以断定qmemcpy就是利用位置。

2. 顺序分析注意的点

为什么要进行这一步呢?这是因为为了保证构造的数据要走到漏洞利用的位置。

由上图可知,参数1+0x14(NULL)+0x2c(填充目的位置)+0x48(NULL),满足这几个条件才可以走到我们的利用点的位置。

3. 总体流程图

由于可以控制读写的位置,所以控制这个位置到池的位置,覆盖池的某些位置来达到触发shellcode代码。

其实主要是为了溢出池中对象头的TypeIndex。通过构造TypeIndex=0(这里涉及到使用零页面),通过对象执行某些回调函数,来达到执行shellcode的目的。

常见的方法就是通过closehandle来执行相应的回调。

四、 代码编写思路

采用的方式是池溢出:

喷射最重要的就是构造的布局,为了稳定性,可以选择泄漏内核地址(NtQuerySystemInformation

其次池的布局已经构造完毕,但是由于只能拷贝0x15c个字节,所以我们要计算从相邻的空闲池的哪个位置开始复制。

推导公式:

开始复制的地址  +0x15c  = 泄露的内核地址 -0xC +1(+1是为了将这个地址重写了,而不是刚好到这里)

公式:   开始复制的地址 = 泄露的内核地址 - 0xC +1 - 0x15c  =    泄露的地址-0xC-0x15b

由于要覆盖 POOL_HEADER + OBJECT_HEADER_QUOTA_INFO + OBJECT_HEADER(中的TypeIndex计算大小为 0x23个字节, 因此需要构造UserBuffer最后0x23 个字节

代码见附件1

五、 BUG调试

刚开始我的代码是构造了一个裸函数来进行残根函数调用,但是总是堆栈总是会少一个值,需要自己push一下,才可以堆栈平衡。这是因为裸函数外部也会实现一个 add esp,4

后来我也尝试了一下b2ahex的方法,但是发现他会push两次,比较好奇,我尝试了push一次,那么就直接BSOD。为什么会导致这个问题呢?其实可以通过分析系统写的函数来观察这个堆栈。

ZwOpenProcess为例:

堆栈是这样的:

但是如果少压入一个值的话,堆栈就会变成:

其实这样的堆栈只会影响R0拷贝R3的参数:

KiFastCallEntry的时候会将参数拷贝过去,但是会从残根函数返回地址+8esp的位置进行拷贝,如果我们不压入一个值,那么拷贝的就是随机值,就会导致蓝屏。

验证这个结论:

这是拷贝一个参数的过程。

从下图可以看出来,esp+8,才等于参数

从上面的KiFastSystemCall函数可以看出,进入内核给edx备份一下esp,方便后续使用edx进行拷贝参数。此时的esp指向残根函数的返回地址。

通过分析,我们会发现进行拷贝的时候会将edx+8,这也告诉我们微软在设计的时候,就指定了R3进入内核前存放参数的位置。

通过源码再探池管理机制

如果想了解内核池的管理和分配,那么就要分析一下InitializePoolMiAllocatePoolPagesExAllocatePoolWithTag的函数了。

由于ExAllocatePoolWithTag比较长,后续给出完整的流程。这个函数主要就是网上写的分配的流程,先从Lookaside、再ListHead,其次才分配大页。

先看InitializePool初始化池的函数。

函数为指定的池类型初始化池描述符。一旦初始化,该池可用于分配和释放。

在系统初始化期间,应针对每种基本池类型调用一次此函数。

每个池描述符包含一个用于空闲块的列表头数组。每个列表头保存的块是POOL_BLOCK_SIZE的倍数。列表[0]上的第一个元素将大小为POOL_BLOCK_SIZE的空闲条目链接在一起,第二个元素[1]POOL_BLOCK_SIZE * 2,第三个POOL_BLOCK_SIZE * 3等的条目链接在一起,最多可容纳一个页面的块数。

首先会计算PoolTrackTableSize的大小,获取的办法有两种,一种是注册表指定,一种是使用默认的值。

计算完大小后,就为其分配内存空间。

其次会使用Hash算法为这块内存初始化tag标签。

接下来,使用同样的算法,申请PoolBigPageTable表,PoolBigPageTable申请后,会有一个初始化过程。也就是将其中va成员置为0x1,以表示空闲。

其次插入到PoolTrackTable中,这个表通过分析,其实就是用来记录非分页和分页池的使用情况。也可以通过结构观察到其用途。

最后一步就是初始化非分页池的描述符,用来管理非分页池的使用情况。

总结一下:

InitializePool函数就是先初始化了一个空间用来管理各种池的分配情况,接下来就是初始化了PoolBigPageTable结构,最后就是初始化了非分页的描述符。PoolBigPageTable结构的用途目前还不清楚。

MiAllocatePoolPages函数的分析:

这个函数主要是用来分配池页面的。

首先会对输入的大小进行向上取整并+1

BASE_POOL_TYPE_MASK  = 1,说明了 0 1才是基础的池类型,同时也验证,只有非分页池和分页池才是基础类型

当要分配的页面小于1(页)时,首先是从单链表中获取。

如果需要的空间 大于40961页),那么就在空闲的非分页池链表中分配

MmNonPagedPoolFreeListHead链表有四个,要从哪个链表中开始分配是由 需要的页数决定的。(注意:这里是开始分配)

找到指定的链表后,判断其大小,如果满足就开始进行分配

得出的结构就是 这个池的 开始地址为 0x825c900

从这个算法可以看出,池页面是从最后一个地址开始分配的(也可以反向推导),分配完之后有一个摘链和插链的操作。这一步操作其实是调整链表,因为4个链表是123为同一个类型,分配对应的大小,当大于等于4时,就会从最后一个链表分配。所以有种情况就是当第4个链表分配后所剩的页数不足4页时,那么就要将其插入到前面的三个链表中。

分配完毕后,调整一下记录空闲非分页池的全局变量。

虚拟地址分配了也要在物理地址(PFN)标识一下,这里可以看出在PFN中标识了起始分配置位,以及结束分配位置。

总结一下:

MiAllocatePoolPages函数就是找到空闲的结点,通过这结点计算分配的位置,最后在物理地址(PFN)上标记一下。


文章来源: https://mp.weixin.qq.com/s?__biz=MzI2ODQwNzAzNw==&mid=2247484012&idx=1&sn=9ef0ebd20751aa2aeb31cb783e0b804a&chksm=eaf15bfedd86d2e88232b7e4b24b2788db544ae39e8ce08ccf126e77655ae9a2b81f8c77b11e&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh