ms17-017提权漏洞
Microsoft Windows Transaction Manager 权限许可和访问控制漏洞
- CNNVD编号:CNNVD-201703-757
- 危害等级: 中危
- CVE编号: CVE-2017-0101
- 漏洞类型: 缓冲区溢出
- 发布时间: 2017-03-20
- 威胁类型: 远程
- 更新时间: 2017-03-20
- 厂 商: microsoft
受影响实体
- Microsoft Windows_10
- Microsoft Windows_10:1511
- Microsoft Windows_10:1607
- Microsoft Windows_7:SP1
- Microsoft Windows_8.1
- Microsoft Windows_Rt_8.1
- Microsoft Windows_server_2008:SP2
- Microsoft Windows_server_2008:R2
- Microsoft Windows_server_2012:R2
- Microsoft Windows_vista:SP2
详细分析
模块对比
实验环境为win7 32bit SP1,下载ms17-017对应的kb4012212补丁包,使用expand命令提取补丁文件
1 | expand -F:* Windows6.1-KB4012212-x86.cab c:\path |
找到补丁包中对应更新的win32k.sys程序,查看版本为6.1.7601.23677,查找最接近该模块的历史版本为6.1.7601.23591

使用BinDiff分析漏洞补丁,找到存在漏洞的函数EngRealizeBrush

函数分析
IDA分析EngRealizeBrush函数,对比新旧版本差异,新版本的win32k.sys内增加了ULongLongToULong和ULongAdd函数防止PALLOCMEM申请tag为”Gebr”的内存时产生整型溢出,PALLOCMEM函数第一个参数为申请内存的大小

PALLOCMEM函数内容如下,因此可以尝试从v12(形参中Size的来源)变量切入分析
1 | void * Pallocmem(DWORD Size, char *szFileName, DWORD nLine) |
在ReactOS中查看EngRealizeBrush函数原型
1 |
|
EngRealizeBrush参数中SURFOBJ结构体内容如下
1 | typedef struct _SURFOBJ { |
此时根据变量v12赋值过程向上查找,发现最终影响v12的值有三个,即EngRealizeBrush参数中的如下内容
a2->iBitmapFormata3->sizlBitmapa4->sizlBitmap

动态调试
建立windbg双机调试,加载符号后在win32k!EngRealizeBrush尝试下硬件执行断点,尝试触发断点
1 | 0: kd> ba e1 win32k!EngRealizeBrush |
断点触发后查看调用栈信息发现由用户态进入内核态的函数为GDI32!NtGdiPatBlt,函数原型如下:
1 | BOOL PatBlt( |
在实验环境中多次尝试触发断点并观察后续函数调用,PALLOCMEM函数并不会每次都得到执行,查看EngRealizeBrush函数存在变量gpCachedEngbrush用来缓存上次申请的内存空间并判断如果满足当前条件不再进行内存申请

尝试编写POC代码触发断点进入EngRealizeBrush函数:
1 |
|
在win32k!EngRealizeBrush下断后查看函数参数如下
a1–>fe476d68
a2–>fe522d18
a3–>fde02198
a4–>00000000
可以看到EngRealizeBrush的实参列表中a4总是为0,因此不再需要考虑a4->sizlBitmap对v12的影响(不需要考虑v12变量二次赋值的情况),着重分析a2->iBitmapFormat、a3->sizlBitmap与v12的对应关系即可

查看a2->iBitmapFormat对应值为6h,查看a3->sizlBitmap对应值(11h,111h)

此时在win32k!PALLOCMEM函数下硬件执行断点并触发,PALLOCMEM申请缓存区的大小为0x4908h

梳理v12计算公式如下
1 | v12=v59 * v68 + 0x44; |
那么v12应该为((v11 * v8) >> 3)*v68 + 0x44=48c8h,与动态调试结果一致(4908h,这是由于最终申请内存为v12+40h)

优化公式为PALLOCMEM函数所申请内存的计算公式
Size = ((20h*a3->sizlBitmap.cx)>>3)*a3->sizlBitmap.cy+44h+40h
在调试时发现PALLOCMEM申请的内存转换为ENGBRUSH结构体,对照windows老版本操作系统源码发现IDA此处确实识别有差异

通过对比老版本系统源码查看内容有偏差,IDA中添加自定义结构体PENGBRUSH并将PALLOCMEM函数申请到的内存转换为PENGBRUSH结构体类型
1 | typedef struct _ENGBRUSH |
EngRealizeBrush函数后续代码会对PALLOCMEM分配的ENGBRUSH结构体内存进行赋值,意味着整型溢出会导致申请到ENGBRUSH内核缓冲区过小,在赋值时会淹没到其他内核对象

v12是无符号整型,因此要溢出并保证能申请到内存需要使
1 | ((20h*a3->sizlBitmap.cx)>>3)*a3->sizlBitmap.cy+44h+40h>0xFFFFFFFF+2 |
以下值满足条件
1 | sizlBitmap.cx=0x23 |
修改POC代码并执行,此时溢出导致申请内存大小为0x10,实际申请了0x18个字节,这是因为其中包含8个字节POOL_HEADER

为了观察整型溢出淹没的数据内容,尝试对比ENGBRUSH内存赋值前后的不同,这里可以看到sizlBitmap.cx覆盖了下一块内存的POOL_HEADER,POOL_HEADER被破坏,会触发名为BAD_POOL_HEADER 的BSOD

继续执行代码时果然触发BAD_POOL_HEADER导致蓝屏,因此在构造EXP时需要绕过POOL_HEADER校验

构造EXP
梳理思路,由于Windows不会对内存页尾的对象相邻的POOL_HEADER进行校验,因此需要利用池风水技术构造内存布局,使得ENGBRUSH处于某一页内存的尾部,PALLOCMEM函数申请的内存被转换为ENGBRUSH结构体,因此我们有机会构造一个越界写的ENGBRUSH结构体,在溢出后需要在溢出的下一块内存布局一个可以方便3环程序利用的对象,如bitmap对象,可以在3环使用Get/SetBitmapBits操作。
查看bitmap对象内核结构SURFACE
1 | typedef struct _SURFACE |
CreateBitmap函数原型如下
1 | HBITMAP CreateBitmap( |
bitmap读写能力范围计算公式为:nWidth * nHeight * nBitCount / 8。ENGBRUSH的iFormat淹没SURFACE对象的sizlBitmap.cy,而sizlBitmap.cy就是CreateBitmap参数中的nHeight,因此当nHeight值为1时可以使其读写范围扩大6倍

拥有了扩展读写能力的bitmap对象,发现ENGBRUSH溢出导致bitmap对象的读写能力扩展了6倍范围(iFormat覆盖sizlBitmap.cy导致),但这仍然不能满足需求,因此需要将bitmap对象拆分成两个合适的对象,之后通过使用同一页内存中第一个bitmap的6倍读写能力修改第二个对象的读写能力来达到任意地址读写的目的(可以在其后再部署一个bitmap对象,扩展其读写范围,也可以部署其他对象,例如PALETTE),本例实现采用bitmap对象结合调色板PALETTE对象,通过篡改当前 PALETTE 对象的成员域 cEntries 值,即可获得相对内存地址读写的能力。
PALETTE结构体如下
1 | typedef struct _PALETTE |
采用这种方式,需要先将用来占位的bitmap位图对象释放掉,然后分配较小的位图SURFACE对象,在其后再申请适当大小的PALETTE对象。漏洞触发前使用UnregisterClassA释放掉一部分用来占位的0x18字节的窗口类菜单名称字符串,以便漏洞触发时能够申请到我们希望的位置

梳理EXP构造步骤
- 第一步,内核在释放内存块时会对相邻的内存块有效性进行校验,这是构造0x18字节的溢出时产生蓝屏的原因,而内核在释放内存块时,如果内存块位于所在内存页的末尾,则不会对相邻的内存块头部结构进行校验,因此我们构造溢出时申请的0x18个字节需要保证在内存页尾才不会触发
BSOD,0x1000-0x18=0xFE8,尝试喷射大量占用FE8大小的内存,使用CreateBitmap,而在需要分配小于0x1000字节的内存时,CreateBitmap分配的内存总大小为SURFACE对象大小+位图大小+POOL_HEADER,SURFACE对象的大小为0x154字节,那么所需要创建的位图大小为FE8-154-8=E8C
1 | static HBITMAP bitmaps[2000]; |
- 第二步,创建大量占用0x18个字节的对象进行占位,可以使用
windows窗口类创建lpszMenuName为四个字符的窗口,创建窗口类时内核会调用ExAllocatePoolWithQuotaTag,申请的内存大小为lpszMenuName*2+2+POOL_HEADER=0x12(内存对齐后为0x18)
1 | char strName[0x20]; |
- 第三步,释放掉第一步申请的内存,为了后续对大小为0xFE8的内存再进行二次分割(第一部分扩展读写能力,第二部分实现任意读写)
1 | for (int s = 0; s < 2000; s++) { |
- 第四步,创建2000个大小0x7F8的
Bitmap对象进行内存占位,此时内存中会出现大量的0x7F0的内存间隙
1 | for (int k = 0; k < 2000; k++) { |
- 第五步,创建2000个大小0x7F0(FE8-0x7F8)的调色板
Palette对象进行内存占位
1 | HPALETTE hps; |
- 第六步,释放一部分创建的0x18对象
1 | TCHAR strName[0x32]; |
- 第七步,触发漏洞并定位目标位图对象并获取调色板成员数据,之前喷射的
Bitmaps位图大小为0x698,而ENGBRUSH越界导致其大小变成了0x698*6=0x2790,可以通过查找大于0x698的位图找到溢出点
1 | HRESULT res; |
- 第八步,执行触发漏洞函数溢出申请
ENGBRUSH对象,溢出导致下一个内存头部的Bitmap对象越界读写,随后从7F8大小的Bitmap对象修改7F0大小的Palette对象成员cEntries,构造出内存任意读写
1 | UINT cEntries = * (UINT *)(&bits[0x698 + 0x8 + 0x18]);//1E3 |
- 第九步,可以指定两个
Palette对象,一个命名为hManager,另一个命名为hWorker,使用SetPaletteEntries方法操作hManager对象相对偏移达到修改hWokrer对象pFirstColor指向的地址的目的,使用SetPaletteEntries/GetPaletteEntries操作hWokrer实现任意读写。
1 | for (int i = 0; i < 2000; i++) |
- 第十步,提权,加载
ntkrnlpa.exe计算到全局变量PsInitialSystemProcess的偏移,在ntkrnlpa.exe中PsInitialSystemProcess代表了EPROCESS结构的地址,通过EPROCESS相对偏移取到双向链表ActiveProcessLinks的地址,通过PID对比找到当前进程的EPROCESS结构,最终从ntkrnlpa.exe的EPROCESS中取得Token替换到当前进程中
1 | 1: kd> dt _EPROCESS |
总结
该漏洞是由win32k中申请ENGBRUSH对象时未对大小进行校验从而使得可以使用整型溢出的方式申请到一块极小的内存,产生ENGBRUSH覆盖其他内核对象扩展其读写能力的行为,再使用被扩展的Bitmap对象修改其后续Palette对象的读写能力范围达到任意地址读写的特权,通过任意读写来修复被溢出的POOL_HEADER最后使用Get\Set方法替换进程Token实现权限提升,利用方法比较苛刻,需要严格构造内存布局才能达到任意地址读写的目的。
EXP代码
1 |
|

参考资料
小刀师傅https://xiaodaozhi.com/exploit/70.html
先知社区https://xz.aliyun.com/t/2919
看雪学院https://zhuanlan.zhihu.com/p/102121772
fightingmanhttps://www.anquanke.com/post/id/205870
Evil Bit Bloghttp://theevilbit.blogspot.com/2017/10/abusing-gdi-objects-bitmap-objects-size.html