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->iBitmapFormat
a3->sizlBitmap
a4->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
fightingman
https://www.anquanke.com/post/id/205870
Evil Bit Blog
http://theevilbit.blogspot.com/2017/10/abusing-gdi-objects-bitmap-objects-size.html