学习内核提权相关知识,拿CVE-2017-0101这个漏洞练习,于是有了这篇笔记。CVE-2017-0101是位于Win32K中的一个整形溢出漏洞,通过利用可实现内核提权。学习过程中,查阅了很多资料,尤其是xiaodao师傅的博客文章,给予了莫大的帮助。阅读xiaodao师傅博客,深感到师傅内功的深厚,无比佩服。本篇笔记以初学者的视角,尽量避开太多的Windows内核GDI内部实现逻辑相关知识,对该漏洞进行分析,调试,最终POC内核提权。
原xiaodao师傅最终pool fengshui 最终使用Bitmap+Palette实现,关键内存布局为0xDF8-0x1F0-0x18。我对其进行修改,使用Btimap+Bitmap实现,关键布局方式为0xD88-0x260-0x18。
环境:
x86 win7 sp1
定位漏洞代码位置可选择找到对应补丁修复后,使用diff工具对比其函数差异定位到对应函数代码片段,阅读相关公开信息,我们可知漏洞发生在Win32k-EngRealizeBrush函数内,此处直接贴出该部分代码,后续所有讨论都基于该函数代码。
通过分析Win32k-EngRealizeBrush可知,函数内由于存在一处整形溢出。该函数内在对要使用的ENGBRUSH对象进行内存申请时(即下图中PALLOCMEM申请pool tag为”Gebr”的对象内存,该对象类型即为ENGBRUSH),由于其使用到的申请大小相关变量v12可溢出、可控,从而导致我们可以利用该溢出点,通过构造小于其标准对象大小的ENGBRUSH对象,进而在其后续对象成员初始化时,可越界操作到其对象内存以外区域,进一步有机会通过布局内存,达到利用目的。
通过下图(只会注释,不会画图)注释图可理解,如果想要精确的攻击到指定的内存布局对象,首先需要搞清楚的问题是漏洞处代码在ENGBRUSH对象申请内存前的大小计算过程,该过程可通过动态调试结合静态分析来完成。
首先我们静态分析漏洞函数处相关代码可知,ENGBRUSH对象申请的大小的决定因素为v12变量,v12最终的计算过程为通过下述代码获得(另外此处可看到有gpCachedEngbrush缓存策略,调试时有如有合适缓存大小,将不申请内存直接使用缓存,故代码测试调试前可注销系统等操作避开此处判断):
接下来分析v12不包括自身初始值的影响部分((v15 >> 3) * v14),从后向前经历以下流程:
首先其影响来自v15,v14:
v12 += (v15 >> 3) * v14;
v15影响来自v13或v15为定值0x20:
v15 = (v13 + 0x3F) & 0xFFFFFFE0
v13影响来自v61:
v13 = *((_DWORD *)v61 + 8);
V14影响来自v61:
v14 = *((_DWORD *)v61 + 9);
递进整理下前后关系流程:
(v15 >> 3) * v14;
v15=(v13 + 0x3F) & 0xFFFFFFE0
V13=*((_DWORD *)v61 + 8);
v14=*((_DWORD *)v61 + 9);
V14=*((_DWORD *)v61 + 9);
分析到此处可知,影响该部分计算结果的成员为*((_DWORD *)v61 + 8),*((_DWORD *)v61 + 9)处数值。接下来继续分析v61的由来,回溯到函数头部,可看出,v61来自v7,而v7通过SURFOBJ_TO_SURFACE(a4)得来,a4为EngRealizeBrush函数形参,一个SURFOBJ对象。
查看SURFOBJ_TO_SURFACE()函数代码可知其本质即为指向参数SURFOBJ(a1)-0x10处,而查看SURFACE对象结构也可进一步确认,SURFOBJ就是SURFACE对象其0x10处的一个成员。回到关注点,此时我们关心v61 + 8/9处的值,即指向v61偏移0x20和0x24,也就是指向SURFACE(v7)对象偏移0x20和0x24处,SURFOBJ(a4)对象0x10和0x14处,查看结构相应说明即为SURFOBJ->sizlBitmap成员,该成员保存了一个Bitmap图像的像素宽高。
最后梳理上述分析到的部分公式:
(v15 >> 3) * v14;
=((v13 + 0x3F) & 0xFFFFFFE0或 0x20)>>3*v14
=(((v13 + 0x3F) & 0xFFFFFFE0) 或 0x20)>>3*v14
=((((*((_DWORD *)v61 + 8)) + 0x3F) & 0xFFFFFFE0) 或 0x20)>>3*(*((_DWORD *)v61 + 9))
=(((a4对象的像素宽+ 0x3F) & 0xFFFFFFE0)或 0x20)>>3* a4对象的像素高
接下来继续分析v12值的初始化部分过程,过程同上述分析流程得到以下递进关系:
v12 = v60 * v68 + 0x44;
v60 = (unsigned int)(v11 * v8) >> 3;
v11取决于局部变量a3(switch)//注意此a3不是函数传参对象a3
a3 = (struct _SURFOBJ *)*((_DWORD *)v58 + 0xF)
v58 = SURFOBJ_TO_SURFACE(a2)
v8 = *((_DWORD *)v6 + 8);
v6 = SURFOBJ_TO_SURFACE(a3)
v68 = *((_DWORD *)v6 + 9);
v6 = SURFOBJ_TO_SURFACE(a3);
梳理流程可得以下公式:
v12 = v60 * v68 + 0x44;
v12 = v60 * (*((_DWORD *)v6 + 9)) + 0x44;
v12 = v60 * (*((_DWORD *)SURFOBJ_TO_SURFACE(a3) + 9)) + 0x44;
v12 = v60 * (a3对象的像素高) + 0x44;
v12 = ((v11 * v8) >> 3) * (a3对象的像素高) + 0x44;
v12 = ((v11 * (a3对象的像素宽)) >> 3) * (a3对象的像素高) + 0x44;
v12 = ((取决于 a2.iBitmapFormat -switch数值* (a3对象的像素宽)) >> 3) * (a3对象的像素高) + 0x44;
v12+部分:
(((a4对象的像素宽+ 0x3F) & 0xFFFFFFE0)或 0x20)>>3* a4对象的像素高
v12 初始部分:
v12 = ((取决于 a2.iBitmapFormat * (a3对象的像素宽)) >> 3) * (a3对象的像素高) + 0x44;
总结静态分析结论可知影响最终的v12可控溢出因素均来自EngRealizeBrush函数参数
a2,a3,a4
,类型为SURFOBJ,具体影响来自以下成员:
1.a4(SURFOBJ)对象的像素宽和高,offset:0x10
2.a2(SURFOBJ)对象的iBitmapFormat 成员,offset:0x2c
3.a3(SURFOBJ)对象的像素宽和高,offset:0x10
通过静态分析,我们已经大概的了解到影响溢出的关键公式流程,接下来还要通过动态分析,来进一步确认静态分析的结论,同时对静态分析过程中的未确认部分进行进一步探索(例如a2.iBitmapFormat最终switch后的分支到达结果)。
首先在漏洞代码处关键位置下断点,通过栈回溯找到可以触发漏洞的3环代码路径(此处注意,实际测试下断点后操作系统内窗口短时间并未断下,可以尝试通过打开浏览器,注销,锁定机器等拥有较多复杂UI操作过程的动作,快速让系统断到我们希望调试的代码处)。通过观察断下的堆栈情况,我们可知通过使用gdi32!PolyPatBlt可触发到达EngRealizeBrush过程ENGBRUSH对象初始化处。
接下来我们还需要了解如何编写3环代码使用PolyPatBlt,来进行下一步的验证调试工作。由于该函数未文档化,我尝试通过在reactos 系统源代码中寻找一些答案(xiaodao师傅已经直接给了使用方法,但还是要了解过程和方法),下图为系统源码中其正确的调用方式和所需参数的定义,通过参考系统代码的方法,可较快速分析,掌握该api相关用法。
经过查看reactos,结合xiaodao师傅给出的答案,了解3环测试代码写法后,接下来直接使用下述代码调试。
通过在EngRealizeBrush头下断,调试观察我们关心的EngRealizeBrush参数a2,a3,a4,断下后首先观察堆栈中的函数参数(下图红框从左到右分别为EngRealizeBrush参数a1-a4)
经过反复调试,可以发现a4始终为空,经过上述静态分析可知,当a4为空时,v12+部分不参与运算。也就是说上述的关键两部分计算,由于a4对象未使用,现在只需要关心v12初始计算过程:
v12 初始计算过程关键取决于a2.iBitmapFormat(offset:0x2c)和a3对象的像素宽高(offset:0x10),经过多次调试,我们可知a2.iBitmapFormat始终为0x6,而a3对象的像素宽高即为我们测试代码中指定与Brush绑定的bitmap宽高。
接下来,需要关心的是,当a2.iBitmapFormat值为6时,最终执行的switch将影响的公式中v11关键数值,还有当前3环代码最终影响0环生成ENGBRUSH对象的大小。调试可知,当a2.iBitmapFormat为6时,switch最终影响v11值为32(0x20)。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!