首页
社区
课程
招聘
[原创]CVE-2016-0165提权漏洞学习笔记
发表于: 2022-5-6 23:25 16379

[原创]CVE-2016-0165提权漏洞学习笔记

2022-5-6 23:25
16379

该漏洞是一个整数溢出漏洞,存在于win32k中的RGNMEMOBJ::vCreate函数中。该函数在分配内存的时候,没有对申请的内存大小是否会发生整型溢出进行校验,这将导致分配的内存大小将远小于所期望的大小。但是,在后续的操作中,函数依然以所期望的大小来对内存进行操作,就会对申请的内存块相邻的内存块进行写入操作,最终在释放申请的内存块的时候会因为破坏了相邻内存块的POOL_HEADER产生BSOD。而通过适当的内存布局,在申请的内存块后跟着的是BitMap对象,越界写入操作修改BitMap中的关键成员可以实现任意地址读写最终可以实现提权。

操作系统:Win7 x86 sp1 专业版

编译器:Visual Studio 2017

调试器: IDA Pro,WinDbg

该漏洞的产生和vCreate函数的第二个参数有关,该参数是一个EPATHOBJ结构指针,该结构体的定义如下:

第一个成员po是一个PATHOBJ结构体,该结构体的定义如下,其中第二个成员cCurves代表的是当前EPATHOBJ对象的曲线数目。

以下是vCreate函数中与本次漏洞有关的关键代码。首先函数会调用ExAllocatePoolWithTag申请内存,而申请的内存的大小只验证了是否大于0,却没有验证是否发生了整型溢出,这就将导致当大小发生整型溢出的时候,ExAllocatePoolWithTag申请的内存大小会远小于期望的大小。

接着,函数将申请到的内存地址作为第三个参数传递给vContructGET函数,在这个函数中将会对申请到的内存进行读写操作,可是整型溢出,导致申请到的实际内存非常小,而vContructGET函数却以期望申请到的大小来读写内存,导致将会越界写入其他内存空间而产生了错误。

最后,如果内存申请成功,vCreate函数会调用ExFreePoolWithTag函数来释放内存。

函数vConstructGET通过结构体EDGE根据路径建立全局边表,结构体EDGE的定义如下。该结构共有0x28个字节,由此可以得知上面申请内存的时候是要申请可以容纳uiCurves+1个EDGE结构体的内存。

函数vConstructGET通过AddEdgeToGET来实现边表的建立,这里要注意的是第三个参数pEdge将作为第二个参数传入AddEdgeToGET函数,也就是将申请的内存作为第二个参数来调用AddEdgeToGET。

函数中AddEdgeToGET实现添加的代码如下,代码有两个内容,首先会判断要加入的边中的结束点的坐标的Y值和起始坐标的Y值是否相同,如果相同则返回。如果不相同,就会通过EDGE结构体中的pNext实现添加,最终会返回pEdge中的下一个EDGE结构体,然后会在函数vConstructGET中继续添加。

其中的几个需要使用的结构体定义如下:

由上面分析可以知道,这个漏洞产生的原因是vCreate函数没有对uiCurves+1和0x28相乘的值是否发生整型溢出导致的。所以,要验证该漏洞就需要解决两个问题,首先是要调用vCreate函数,其次是要在调用这个函数的时候第二个参数中保存的uiCurves可以让后面的计算产生整型溢出。

可以通过函数PathToRegion来调用vCreate函数,该函数定义如下。

该函数只有设备句柄一个参数,该设备句柄对应的对象中的uiCurves将会被用于申请内存。在用户层,可以通过GetDC,BeginPath,EndPath这三个参数来实现获取设备句柄,将设备与当前上下文绑定或解绑,函数定义如下:

对于GetDC,可以通过传入NULL来获取桌面窗口的设备句柄,通过BeginPath将设备与当前上下文绑定就可以对桌面窗口的设备对象进行操作。接下来就可以通过PolylineTo函数来增加获取的桌面窗口设备句柄的uiCurvers,该函数定义如下:

函数第一个参数即使要操作的设备句柄,第二个参数是POINT指针,指向要增加的曲线的坐标,第三个参数为坐标个数。其中,POINT结构体定义如下:

该函数通过POINT数组中的值来添加边,如果现在输入的点为(0,0), (1, 1), (2, 2)这三个点,PolylineTo函数就会将(0, 0)与(1, 1), (1, 1)与(2, 2)相连,这样就增加了两条边。而在vCreate函数中,又会将曲线闭合,也就是会将 (2, 2)与(0, 0)连起来,因此uiCurvers就会增加3,所以POINT数组输入几个点就会增加多少线条。根据上面申请内存时候通过(uiCurvers + 1) * 0x28可以算出,只要uiCurvers的值为0x6666665就会发生整型溢出(0xFFFFFFFF / 0x28 - 1)。

但是,在要触发漏洞还需要绕过三处限制,第一处限制是在通过PolylineTo来增加设备对象的uiCurvers的时候,需要注意在PolylineTo中有对cpt参数,也就是要添加的边的个数进行判断,判断代码如下

第二处限制是调用PathToRegion后,会调用win32k中的NtGdiPathToRegion函数,在该函数中,要注意和不同y值的点不能过大,否则会导致内存资源不足,代码如下:

第三处限制是在vCreate函数中,在该函数中会将不同y值大小的点与0x20相乘并加上0x1F8,最终的值与0x7FFFFFFF进行判断,如果大于该值则会跳转,而不调用vConstructGET函数。这部分就是在上述对vCreate函数分析的时候,调用vConstructGET函数前的if判断语句中省略的部分。

最终,绕过以上三处限制而写出的POC代码如下:

在申请内存的地址处下断点,编译运行POC,由WinDbg输出可以看到,在对申请的内存大小进行乘法运算之前,eax的值为0x6666667,将其与0x28进行相乘之后就会因为整型溢出导致eax为0x18,接着就申请0x18大小的内存空间,记录下此时申请到的内存为0xfe640ce8,并查看相邻内存块的数据。

接着在AddEdgeToGET最后的增加EDGE处下断点,多运行几次,不难看出现在该函数写入的内存地址已经超过了申请的内存空间。

当AddEdgeToGET函数指向完成,再次查看申请的内存中的数据,可以看到此时相邻内存块的数据已经发生了更改。

继续运行,由于此时已经越界写入了相邻内存块,破坏了相邻内存块的POOL_HEADER,在释放内存的时候,内存合并操作将会产生BSOD的错误。

根据上面内容可以知道,整数溢出漏洞的存在会导致申请的内存的相邻内存被修改,这里希望可以通过内存布局,让BitMap对象紧跟在申请的内存空间后,这样可以通过越界写入操作来修改BitMap对象中的关键数据来实现任意地址写入实现提权。由于0x18的内存过小,难以利用,因此将uiCurvers的值增加2,来增大申请的内存空间到0x68(0x18 + 0x28 * 2),算上POOL_HEADER则占用0x70大小的内存块所以此时的需要用来增加uiCurvers的点为0x6666667个。此外,因为期望的内存块过大,所以如果不进行限制,写入的内存的地址会过多,这里就需要用到函数AddEdgeToGET函数中点的y坐标值相等情况下跳过写入操作来省略掉多余的内存操作。

BitMap对象可以通过CreateBitmap函数创建,该函数定义如下:

该函数会在内存创建BitMap对象,该对象包含0x154字节大的SURFACE结构体的对象头和像素点数据,SURFACE的结构体定义如下:

第一个成员是BASEOBJECT结构体,共0x10字节,该结构体在所有的GDI对象头都会保存一份,结构体定义如下:

第二个成员是SURFOBJ结构体,该结构体保存了紧跟对象头后面的像素数据的相关信息,定义如下:

其中sizlBitmap中的cx和cy分别指定了像素数据的宽和高,由调用的CreateBitmap的第一和第二个参数决定。像素数据的大小则保存在了cjBits,当第三个参数cPlanes为1,第四个参数cBitPerPel为8时,该值就是cx和cy相乘得到,当第四个参数cBitPerPel为32时,该值就是cx * cy * 4。pvScan0则指向了像素数据的起始地址,该地址跟在对象头其后。关键的数据是sizlBitmap和pvScan0,此时查看上面的溢出后的数据情况可以看出,这些值并没有被覆盖为理想的值。

因此,需要通过垫片,也就是在申请的内存块和BitMap对象中间填入一些"垃圾"数据,来让漏洞点的写入操作可以将BitMap对象的关键成员修改为理想数据。这里通过设置剪切板的方式来增加垫片,设置剪切板的数据的函数为SetClipboardData函数,函数定义如下:

在不调用OpenCliboard并清空剪切板数据的前提下SetClipboardData函数分配的剪切板数据对象会一直存在于分页会话池中,所以不用担心因为覆盖操作而导致内存回收时产生错误。通过剪切板来实现垫片的代码如下,通过该代码会产生dwSize + 0xC + 0x8大小的内存块:

让申请的内存块后跟BitMap对象的思路是,申请大量0xF90大的BitMap对象,这样就会在很多新的内存页中留下0x70大小的内存,此时漏洞函数申请的0x70大小的内存块就会每个内存页的最后0x70的字节。此时,该内存的相邻内存就会是相邻内存页,内存页的起始保存的就是BitMap对象。但是,内存中会存在一些0x70大小的空闲内存,为了保证申请的内存页在BitMap对象剩余的0x70字节中就需要通过CreateAcceleratorTable函数来消耗这些空闲的0x70大小的内存,该函数定义如下:

该函数会创建加速表对象,每个对象占8字节,第二个参数cEntries指定创建的加速表对象的个数,当指定为0xD的时候就会创建0xD个加速表对象,占用0x68字节大小,加上POOL_HEADER的大小刚好0x70。这样,我们就可以首先使用加速表对象消耗空间内存,在用加速表对象占用0x70的内存块,释放掉其中的一部分,漏洞函数申请内存的时候就会刚好申请到相应的内存块。

上面提到过,为了让漏洞的写入操作可以将BitMap对象的关键成员覆盖为理想的值,需要加入剪切板数据,这里剪切板数据的大小为0xB70。在创建完加速表对象之后,需要释放掉BitMap对象,随后申请0xB70的剪切板数据,再次申请BitMap对象来填充0xF90大小的内存减去剪切板数据大小的内存空间。最终的内存布局要如下图所示:

创建上图内存布局的代码如下:

此时编译运行,在分配内存处下断点可以看到漏洞函数申请的内存刚好在剪切板数据和BitMap对象后,且这三个数据加一起共占一个页的大小。而下一个页的数据是剪切板数据,BitMap对象和加速表对象,所以此时的内存布局已经如上图所示。同时,也记录下下一内存页的BitMap对象的数据,以供之后比对。

继续运行到vConstruct函数结尾处,此时已经成功写入了数据,可以看到此时已经成功修改了BitMap对象中的数据。根据计算,0xCAFC5B9C处保存的是cy,此时cy已经被修改为0xFFFFFFFF,那么就可以通过该BitMap对象修改下一页中的BitMap对象的pvScan实现任意地址读写。

现在已经可以通过漏洞修改相邻页中的BitMap对象的cy,这样该对象可读写的内存空间会变得很大,可以利用这个特征修改该BitMap对象下一内存页中的BitMap对象中的pvScan,以此来实现任意地址读写。而要知道被修改的BitMap对象,只需要通过GetBitmapBits函数获取返回值,如果返回值大于创建BitMap对象时指定的可读写返回,那么就证明该BitMap被修改过。另外,因为下一页中的BitMap对象句柄未必其相邻,所以要通过SetBitmapBits修改下一内存页的BitMap对象的cy,通过一样的方法获取其句柄。相关代码如下:

有了这两个BitMap对象就可以通过以下代码实现任意地址读写:

在进行任意地址读写的时候,除了修改实现提权必要的数据以外,还需要修改由于漏洞对其申请的内存相邻的内存页的剪切板数据和BitMap对象中的数据。限于篇幅就不展开,具体看参考资料中的链接。


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2022-5-9 08:51 被1900编辑 ,原因:
收藏
免费 3
支持
分享
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
 
赞赏  Editor   +50.00 2022/06/20 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (4)
雪    币: 4105
活跃值: (5807)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
2
打破0回复
2022-5-8 13:20
0
雪    币: 22411
活跃值: (25361)
能力值: ( LV15,RANK:910 )
在线值:
发帖
回帖
粉丝
3
blck四 打破0回复
感谢捧场哈哈哈
2022-5-8 15:00
0
雪    币: 539
活跃值: (92)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
上传到Github
2022-5-8 23:35
0
雪    币: 22411
活跃值: (25361)
能力值: ( LV15,RANK:910 )
在线值:
发帖
回帖
粉丝
5
dp_grost 上传到Github
已改,真细节啊
2022-5-9 08:52
0
游客
登录 | 注册 方可回帖
返回
//