该漏洞是存在于win32kfull.sys的bFill函数中的一个整型上溢漏洞,函数在申请内存的时候没有对申请的内存的大小是否发生整型溢出进行验证,导致申请的内存大小可以远小于期望申请的内存大小。随后函数又会根据期望申请的内存大小来对申请到的内存进行写入操作,这就引发了越界写入的问题,最终会在释放内存的时候因为破坏了相邻块的_POOL_HEADER而导致BSOD。通过适当的内存布局,可以利用这个越界写入操作修改BitMap对象的关键成员来实现任意地址读写入从而实现提权。
操作系统:Win10 1511 x64 企业版
编译器:Visual Studio 2017
调试器:IDA Pro, WinDbg
bFill函数的第一个参数是一个EPATHOBJ结构体,该结构体的定义如下,其中偏移0x4出保存的cCurves定义了曲线的数目,bFill函数用该值来计算要申请的内存大小。
以下是bFill函数的关键代码,函数会将EPATHOBJ中的cCurves取出,将其与0x30相乘得到的数值作为内存大小来申请内存。如果申请成功,就会在调用的bConstructGET函数中会对申请的内存进行写入操作,在bFill函数最后会将申请的内存释放。
以下是触发该漏洞的POC代码,该代码会通过调用PolyLineTo函数来增加EPATHOBJ的cCurves,共增加0x156次,每次增加0x3FE01。而在调用bFill函数前,cCurves还会被加1用来闭合曲线。因此,在bFill函数中申请的内存大小就会是(0x156 * 0x3FE01 + 1) * 0x30 = 0x5555557 * 0x30 = 0x1 0000 00050,溢出后就会是0x50。
在申请内存处下中断,编译运行POC,就可以看到因为整型溢出此时申请的内存大小就远小于期望的大小。
继续运行,就会越界写入操作修改了相邻内存块的_POOL_HEAEDER,从而在释放内存的时候产生BSOD错误,以下是部分错误信息:
该漏洞由于整型溢出可以越界写入相邻的内存,如果这些写入操作能修改BitMap对象的关键成员就可以实现任意地址写入。为了达到这个目标,需要特定的内存布局,在x64位操作系统上的池内存有以下的几个特点:
每一块申请的内存都会加上0x10字节大小的_POOL_HEADER
申请的内存大小如果超过0x808字节就会在新的内存页中申请该内存
连续的请求会从页的末尾开始分配,页尾的内存在释放的时候不会合并位于相邻内存页的下一内存块
本文的内存布局和参考链接中的不同,其中每一页的0x1000字节由0xBC0大小的剪切板对象,0x3E0大小的BitMap对象和0x60大小的加速表对象或空闲内存构成。这样,当触发漏洞的时候,申请的0x50(加上_POOL_HEADER为0x60)的内存就会占用其中位于页尾的空闲内存。这样,即使越界写入操作会修改相邻内存块的_POOL_HEADER,但此时的内存块位于下一内存页,所以在释放申请的0x50大小的内存块的时候,不会因为合并操作产生BSOD。此时,如果能利用越界写入操作修改相邻内存页中的BitMap对象中的关键成员,就可以实现任意地址读写。
想要创建上图的内存布局,同时让触发漏洞时候申请的内存刚好在中间的1000个内存页中0x60大的空闲内存中,需要通过以下几个步骤实现:
通过加速表对象创建0x60大小的内存来消耗内存池中的空闲内存
创建5000个0xFA0大小的BitMap对象,这样就会有5000个内存页中包含0xFA0大小的BitMap对象和0x60大小的空闲内存
通过加速表对象创建5000个0x60字节大小的加速表占用第2步中0x60大小的空闲内存
释放掉第2步中创建的BitMap对象,这样那5000个内存页中起始的0xFA0内存就会变成空闲内存
创建5000个0xBC0大小的剪切板对象,这些对象会占用第4步释放BitMap对象而产生的内存页中起始的0xFA0的空闲内存中的0xBC0,这些0xFA0的空闲内存就会剩余0xFA0 - 0xBC0 = 0x3E0的空闲内存
创建5000个0x3E0的BitMap对象,这些BitMap对象就会占用第5步中剩余的0x3E0的空闲内存
释放掉第3步创建的中间一部分加速表对象,这样就会在第5步和第6步中创建的剪切板对象和BitMap对象后产生0x60大小的空闲内存,这样触发漏洞时候申请的内存就会刚好占用这些0x60大小的空闲内存
经过以上7个步骤就会形成上图的内存布局,相应代码如下,此时要注意在x64系统中,BitMap对象的对象头占0x260字节,加速表对象的对象头占0x20字节,剪切板对象的对象头占0x14字节。
在申请内存处下断点,编译运行程序就可以看到此时申请的内存在内存页的最后0x60字节:
在x64系统中创建的BitMap对象的BASEOBJECT64占0x18字节,结构体定义如下:
利用BitMap对象实现任意地址写入的关键成员在对BASEOBJECT64之后的SURFOBJ64结构体中,结构体定义如下:
其中偏移0x38的pvScan0指定了通过SetBitmapBits和GetBitmapBits读写内存地址,正常情况下pvScan0指向的地址紧跟在BitMap对象头之后。偏移0x20的sizlBitmap中的cx和cy则决定了可以读写的字节数,如果可以修改这两个值就可以扩大读写的范围。
内存的修改发生在bConstructGET函数中,该函数通过调用AddEdgeToGET函数完成写入,AddEdgeToGET函数的第二个参数指向了要修改的内存地址,第三个和第四个参数为_POINT结构体,对应了POC代码中的POINT数组中的点,分别代表了上一个点的坐标和当前点的坐标,最后一个参数是RECTL结构体,相关的定义如下:
以下是AddEdgeToGET函数中的关键代码,其中rect的top和bottom通过动态调试可以获取它们的值分别是0x1F0和0。
这段代码可以获取以下的几个信息:
如果当前点的y值小于上一个点的y值,内存偏移0x28处的内存会被赋值为0xFFFFFFFF,否则赋值为1
如果当前点和上一个点的y值相等,则函数会将edge直接返回,也就是不进行内存偏移,直接返回原来的内存地址
如果第2步没返回,就会返回edge + 0x30处的内存,也就是取当前地址偏移0x30处的内存地址,下一次进入到该函数中就会操作这块新的内存,这里也可以看出,EDGE结构体的大小为0x30
在本文的内存布局中,要修改BitMap的中的sizlBitmap需要写入申请的0x50字节,页起始处0xBC0字节的剪切板对象,sizlBitmap成员相对于BitMap对象页的偏移0x10 + 0x18 + 0x20 = 0x48,共0x50 + 0xBC0 + 0x48 = 0xC58,sizelBitmap占8字节,所以一共需要操作0xC60字节,AddEdgeToGET函数每次会移动0x30字节的内存,所以一共要添加0xC60 / 0x30 = 0x42次。起始点和结束点会被当作特例添加进去,因此只需要将所有点设置一样,将其中一个点设为不同,在调用PolyLine函数前0x20次增加cCurves的时候,该点在作为上一个点和当前点的时候都会进行内存偏移,这样就会刚好写入0x20 * 2 + 2 = 0x42次,此时触发漏洞的代码如下:
此时在申请内存处下断点,编译运行程序,在内存写入前,BitMap的sizlBitmap是创建BitMap时指定的大小。
当越界写入操作完成时,sizlBitmap成员的大小就被扩大为0x1 * 0xFFFFFFFF = 0xFFFFFFFF,sizelBitmap成员的前一个成员,即hdev被修改为非0,而之后的成员,特别是pvScan0并没有被修改,。
此时的sizlBitmap成员被扩大,也就是通过SetBitmapBits和GetBitmapBits对该BitMap对象可读写的地址被扩大了,而pvScan0并没有被修改,所以此时就可以将该BitMap对象作为work,将相邻页中的BitMap对象作为manger,通过修改相邻页中的BitMap对象中的pvScan0来实现任意地址读写。
获取work的方法也很简单,当创建BitMap对象的时候,指定的可读写的内存大小为0x5C * 1 * 32 / 8 = 0x170,所以可以遍历BitMap句柄的数组,调用GetBitmapBits来判断可读写的内存大小是否超过这个值,如果超过,说明该BitMap被修改了,相应代码如下:
此时编译运行会出现BSOD错误,错误信息如下,可以看到是错误是在调用GetBitmapBits函数的时候,在bAllowShareAccess中会对rax偏移0x38处的内容进行读取操作,而此时该地址的内存是无效的。rax等于BitMap对象的hdev,所以这个错误是因为内存写入的时候,修改了hdev产生的。
在错误函数bAllowShareAccess中,会将hdev + 0x38的地址取出并判断其是否为1,如果为1,则函数返回0,GetMapbitsBits函数就将调用成功。
根据上面的调试可以看到,hdev的值被修改为0x1 0000 0000。所以要避开上面的BSOD错误,只要在0x1 0000 0000处申请内存,且将0x1 0000 0038的值修改为1即可,相应代码如下:
现在得到了可以修改0xFFFFFFFF大小的BitMap对象的hWorker,只需要用它修改相邻内存页的BitMap对象的pvScan0就可以实现任意地址读写。hWorker的pvScan0所指地址为hWorker的BitMap对象之后,所以相邻内存页的pvScan0相对于它的偏移就是0x1D0(0x1000 - 0xBC0 - 0x10 -0x260) + 0xBC0 + 0x10 + 0x18 + 0x38 = 0x1D0 + 0xC20 = 0xDF8。所以通过SetBitmapBits修改hWorker偏移0xDF8处的内存就可以修改相邻内存页BitMap对象的pvScan0,此时就可以完成任意地址读写,相应的代码如下:
在win 10 x64系统上,与提权相关的几个成员如下:
想要实现提权就要将System进程的Token赋值给本进程的Token,在ntoskrnl.exe内核文件中,全局变量PsInitialSystemProcess保存了System进程EPROCESS地址:
所以只要找到当前系统中运行的ntoskrnl.exe内核中的PsInitialSystemProcess地址就可以获取System进程的EPEROCESS地址,PsInitialSystemProcess相对于内核文件的偏移可以通过GetProceAddress获取,而当前系统中运行的内核文件的地址则需要以下函数获取:
第一个参数用来接收系统中运行的驱动文件的地址,该变量将会指向一个ULONG64的数组,其中第一个元素就是当前系统运行的ntoskrnl.exe的地址,因此,可以通过以下代码来获取System进程的EPROCESS地址:
有了System进程的EPROCESS地址,就可以通过上面实现的任意地址读写的功能来实现Token的替换,相应代码如下:
完整的exp地址保存在:https://github.com/LegendSaber/exp_x64/blob/master/exp_x64/CVE-2016-3309.cpp。运行程序,可以看到提权成功:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-8-6 10:39
被1900编辑
,原因: