空指针解引用分析
https://mp.weixin.qq.com/s/5O15hk3bOutm6O1Sqsf4fg
空指针解引用分析
空指针解引用分析
一、 概述
1.CVE-2018-8120 是通过一个空指针解引用达到任意读写的漏洞。其实个人感觉单凭空指针 解引用的危害就是 BSOD,但是利用方面还是要配合任意读写漏洞,如果单纯的只是 BSOD 那可能充其量算一个 BUG,但是如果能配合任意读写(全称:Write What Where)那么才算 是漏洞。通过思考得出的结论就是漏洞 = BUG+利用。
二、 初识空指针解引用
1.空指针解引用的伪代码 (1)解引用,顾名思义,其实就是对一个指针进行访问其中的值,那么空指针解引用就代 表了这个指针是 NULL,所以伪代码如下:
(2)用户层 如果这段代码身处在用户层环境,那么就是访问异常,0xC0000005 错误,这是因为操 作系统的顶层异常替我们处理了,所以只会弹出来错误对话框显示出错误码。
(3)内核层 如果这段代码身处在内核层环境,那么就会直接 BSOD,因为操作系统在派发两次异常 后发现没有人能处理,那么直接蓝屏。这也体现了操作系统对于内核层的容错性很低。
(4)0xC0000005 或 BSOD 原因 这是因为 NULL 处的地址并没有进行挂 PTE,只是单纯了挂了 PDE(如下图),简而言 之就是没有对应的物理页。此处让我想起了操作系统划分地址空间的时候,都是将虚拟地址 当做是空头支票,实际干活的时候还是得靠物理地址。
2.空指针解引用到任意读写
(1)空指针解引用到任意读写的这一步,就是锦上添花扩大战果的一步。如果从空指针解引用这一句代码为分界线,那么此前的很多代码就是造成空指针的原因,此后的代码就是是 否能够任意读写的关键(这里有点不严谨,因为还要看是否有可控的元素)。
三、 通过 HEVD 看空指针解引用
因为不太懂得漏洞什么,所以先挑选一个比较简单的漏洞环境来进行探一探究竟。这 有点初时不识君难免有些羞涩的味道。
1. 源码角度分析
(1) 漏洞的成因 漏洞原因就是没有对申请的结构体 NullPointerDereference 判断是否为 NULL,直接调用 了其中的函数。
(2) 利用分析
我们已经知道了漏洞的原因是直接对一空指针进行解引用,并且利用了空指针进行了一 个函数的调用。那么我们让此处的空指针变成不是名义上空的指针,并且在该指针调用的函 数偏移处进行放置我们的 shellcode,那么达到漏洞利用的效果。
1. 申请 0 地址页面内存 调用 NTDLL.NtAllocateVirtualMemory 函数进行为地址为 1 的位置申请内存,为什么不直接写 0 呢?这是如果写 0 的话就由系统确定你的内存从哪里开始申请。如果写 别的值,那么这个函数内部会自动向下取整一个页面大小。
这也就说明了我们写 0x1-0xFFF,都会帮我们分配到 0-0xFFF 这个虚拟地址空间的 内存。其次要想这个函数真正内部做了什么东西(以后详细分析,这里简述)。其实这个函数并没有直接帮我们申请到物理页面,我们通过可以分析后可知,这个函数调用完之后,并没有帮我们挂上PTE,那么它到底做了什么呢?其实它只是帮我们申请了虚拟地 址,也在虚拟内存中申请一个 VAD 来保留一段空间。当我们真正使用的时候操作系统 才会帮我们挂上物理页面。
2. 在特定位置放置 shellcode 这一步,其实就是挂上了 PTE,并在调用的函数偏移处写上我们 shellcode 的地址。
3. 提权代码
提权代码,其实就是将 system 系统进程的 token 令牌替换到我们进程中的 token 结构体中,这种就间接的提升了权限。(这里是操作系统的安全机制,目前不是很了解)
现在对空指针解引用有了初步的了解,当时觉得算是理解了,分析了CVE 后,发现自己以为的真的是自己以为的,这大概就是理想与现实吧!接下来看看 CVE 的分析。
四、 CVE-2018-8120 分析
1. 定位漏洞位置通过对比补丁前后,查看漏洞所在位置。
(可以使用工具BinDiff 或者Diaphora 来对比分析),这里就直接查看漏洞函数 win32k!SetImeInfoEx。打补丁前:
打补丁后:
2. 漏洞的原因分析 接下来从如下几个角度进行审视,进而确定它为漏洞而不是 Bug。
(1) 漏洞位置是否可控?这个自我感觉是很重要的,因为之前有点和 rootkit 混淆,漏洞是利用系统留给用 户的接口来进行攻击,有点像用正当的手法做最不正当的事情,而 rootkit 这种就是大 肆的破坏。通过使用 IDA 对 SetImeInfoEx 函数进行交叉引用后,会发现第一个参数是来自于 NtUserSetImeInfoEx 函数中的 GetProcessWindowStation 函数的返回值,因此这个值是可控的。
(2) 是否可以扩大影响?对于扩大影响,我们则需要查看 SetImeInfoEx 这个函数的漏洞点以下的部分。通过查看,我们发现了此处有个 qmemcpy 函数,并且目的参数与漏洞点的指针有些间接的关系,如果可以控制这个位置,那么就可以达到任意读写从而使其 bug 变为漏洞。
(3) 为什么会存在这样的问题?站在代码编写的角度来想,编写者可以潜意识的认为这个地址不可能为 NULL,所以没 有进行更深层次的检验。这可能是因为是结构与代码组合时,没有想到过多个 WindowStation 的问题,因为我查看了当前桌面的 WindowStation,发现它并不是为 NULL,但是如果是新建 一个,默认是为 NULL 的。
3. 漏洞的 POC 编写
(1) 分析用户层到漏洞点的路径 通过 IDA 一路交叉引用,找到最终的 Nt 开头的函数,它的执行路径如下:Ntdll!NtUserSetImeInfoEx --> Win32k!NtUserSetImeInfoEx --> Win32k!SetImeInfoEx
(2) 代码编写
1. 创建自己的 WindowStation,并且查看(tagWINDOWSTATION*)pwinsta->spklList 是否为 NULL。通过调用 CreateWindowStationA 创建的 WindowStation,pwinsta->spklList 默认是为 NULL。
2. 调用 Ntdll!NtUserSetImeInfoEx,这一步,我们可以模仿 NTDLL 中的残根函数写法,自己调用。完整代码如下:
4. 漏洞的利用编写 POC 的编写是研究如何走到漏洞点,exploit 的编写是从漏洞点如何走下去。
(1) 分析当前可利用的区域
通过查看 qmemcpy 第一个参数的来源,我们发现它来自于 v3 的一个指针,由此可知 ,我们可控的区域就是 4 个字节,控制了这 4 个字节,就可以进行任意的读写。
(2) 如何利用这个区域 1 池喷(Pool Spraying)的可行性。查看 qmemcpy 的第一个参数,可以发现该地址是位于内存池的,因此,我们可以通过池喷射的技术来利用漏洞。
可以发现这个地址来自于池里面,所以我们利用池喷进行内存句柄,进而控制住指针。
2. 池喷射
先研究一下池喷射,这个就根据 HEVD 的池溢出漏洞来进行研究。
1) 漏洞点构造
2) 临界值观察 a. 首先我们将利用的代码中 Size 修改为和缓冲区刚刚好的大小。查看 一下池的布局。这里设置为 0x1F8 个字节。
为什么上面的设定是 0x1F8 个字节呢?而不是别的。这是因为_POOL_HEAD 占 8 个 字节,所以总共加起来刚好 0x200 个字节,够一个 Lookaside 中的大小。
接下来,查看一下 pool 的布局,通过查看会发现如果用户输入的大小超过 0x1f8, 那么就会覆盖掉下一个池头。
3) 寻找利用点 a. 由上面可知,可以控制溢出到池头,那么必然我们的利用点得利用池 头中的某些成员来进行控制EIP。
通过查看,池头部和池配额结构体,都没有发现可以利用地方,但是考虑到这是非分页池,那么里面就存放了很多内核对象结构体。(这里思考的比较久,已经看很多网上的文章都是一路 dt 下去,找到了内核对象,都没有说这块内存是用来干嘛的。那是 不是如果是非分页池了,就不能这么使用了。)
于是我们可以尝试探索一下,查看里面是不是存放的是内核对象,由于某一个内核 对象都会有一个头部(_OBJECT_HEADER),所以可以试探性的搜寻是否可以找到对应的 BODY。
这也就是为什么国外的文章,直接使用 dt _OBJECT_HEADER 这个地址。
是不是内核对象,还得在通过其中的 TypeIndex 来进行验证。操作系统将所有的内核对 象定义成了一个全局数组 nt!ObTypeIndexTable,而 TypeIndex 就是数组的索引。这个数组的类型是 OBJECT_TYPE 类型,如果这个索引可以到这个数组中寻找到,那么必然是内核对象。
查看对象模板中的 _OBJECT_TYPE_INITIALIZER,会发现很多回调函数,都可以进行利用, 但是最好利用应该就是 CloseProcedure 了,只需要通过调用 closeHandle 函数来触发。
4) 内存布局 内存布局的话,可以通过申请大量的内核对象,这里选择的 Event 对象,这个 和这个对象的大小刚好为 0x40 个字节(释放 8 个就是 0x200),申请大量的 Event 对象将 Lookaside 和 ListHeads 占用了,然后通过申请新页来分配内存, 进而释放内存的时候可以进行合并成想要的大小。(因为分配 Pool 内存的时 候循序为,lookaside-->ListHeads -->申请新页)
这个漏洞可以利用的关键点还在于 Win7 系统可以申请 NULL 页面。所以将 TypeIndex 修改为 0(溢出的位置),然后将 NULL 页面 +0x60 偏移处修改为 shellcode 的位置,就可以在 CloseHandle 的时候触发 shellcode 代码。5) 攻击过程综述 a. 进行内存布局,制造出 0x200 的空隙,控制位置
b. 申请 0 页面,构造shellcode
c. 触发
3 研究 Bitmap 布局的可行性。