原文标题:《用 Opatch 修补一个 0day:Windows gdi32.dll 内存信息泄露 (CVE-2017-0038)》
我第一个修复的 0day, (我甚至不是一个微软的开发人员)。
by Luka Treiber, 0patch Team
我正在在网上钓鱼以查找我下一个去修补的目标时,有东西被抓到了我的网里. 然后 Mitja 走进来并且问:
M:抓住了什么吗?
- 0...
M :哦,真遗憾...
- 是的,但不是那个意思,这是一个 0day
正如你可能已经知道的那样,上一个补丁日微软并没有修复这个漏洞。前段时间(Project Zero)连续公布了几个微软的 0day 漏洞, CVE-2017-0038 是里面的第一个。但是不用担心,即使在黑暗中也有一线光明。我上周有些空闲时间来查看该漏洞的问题所在,所以现在我可以给你一个非常第一手的对这个漏洞的 0patch 修补。
CVE-2017-0038 是因为 EMF 图片格式解析逻辑里,在解析一个图像文件提供的像素数量时,没有充分检查图像文件中指定的图像尺寸。如果图像大小足够大,解析器会被触发去读取正在被解析的 EMF 文件内存映射区域前面的内存内容。一个攻击者可以利用这个楼顶偷取应用程序存储在内存里面的敏感数据,或者作为对其他利用技术的辅助(只要突破 ASLR)
我想要感谢谷歌 Project Zero 团队的 Mateusz Jurczyk 对这个漏洞做的简介而准确的报告,这使我快速找出了 bug 的所在,并且跳转到补丁代码去修补它。
Mateusz's 的 POC 的作用原理就像描述的那样——通过启动一个 IE 11,并且拖入一个 poc.emf 进去,这是我得到的结果:
这张图片需要被放大到最大,以看清楚通过彩色像素泄露的惊人的堆数据。而且它确实泄露了:每一次重新加载这个 POC 都会导致一幅不一样的图片。
为了看到像素值,我将 poc.emf 放到了一个 html 画布里面,而且用 JavaScript 将像素值打印了出来。连续重新加载 PoC 后对比结果显示,唯一不会改变的像素值是那个位于左下角的像素(如果你真的很仔细看上面的屏幕截图的话,你会发现那里有一个红色的污点)
换句话说:
FF 33 33 FF
这与报告里面描述的一致(我们只要仔细看就行了)。所以这个实际来源于所提供的 EMF 文件里面数据的唯一像素值就是它(FF 33 FF 33)(用4个字节表示)——此外所有的数据都从图像像素数据外面的内存区域所读取。
现在让我们来修补它。在行动之前,我们需要理解漏洞代码的位置在哪里。幸运的是报告里面紧接着就揭露了漏洞函数的名称 MRSETDIBITSTODEVICE::bPlay。这是重要的,因为 PoC 触发的过程中既没有crash也没有任何可以被调试器捕获以进行深入分析的异常。手握这部分信息后,我们可以用windbg 附加到 iexplore.exe ,并且设置一个断点:
bp MRSETDIBITSTODEVICE::bPlay
根据报告,这就是所有问题的所在。报告里面说 cbBitsSrc 并没有检查“预期的 bitmap 字节数”-- 从 cxSrc 和cySrc 计算出来额图像大小。
我们需要一点来自 MSDN 的帮助:
- cxSrc
Width of the source rectangle, in logical units. (初始矩形的宽度,以逻辑单元表示)
- cySrc
Height of the source rectangle, in logical units.(初始矩形的高度,以逻辑单元表示)
- cbBitsSrc
Size of source bitmap bits.(初始 bitmap 比特的大小)
在为 windbg 加载 EMRSETDIBITSTODEVICE 的符号后(这个在后面有解释),报告里面的解释一下子变得清晰起来。
当处理 PoC 内的 EMF文件时,一个 EMRSETDIBITSTODEVICE 结构体作为第一参数(该结构体的详细信息在上面截图的右下角)被传入 MRSETDIBITSTODEVICE::Play 函数(rcx 的值 复制给 rbx 的值,标记为蓝色背景)。包含的 cbBitsSrc 值被设为 4,这没有匹配被 cxSrc 和 cySrc 匹定义的图像尺寸(256个像素或者是 1024 个字节)
所以,为了修复这个缺陷,应该在任何数据被窃取之前增加一层校验。原则上应该做的是,在任何图像数据被复制之前,检查一下 cbBitsSrc 的值是否小于 cxSrc * cySrc * 4。一个合理的点似乎就位于检查 EMRSETDIBITSTODEVICE 属性的第一轮校验内部。(参见上面代码截图中,任何失败的情况下,以一个 jmp 退出函数的 cmp - jg 对)。然而,选择实际的 patching 点不仅仅是逻辑上适合我们我们想要去应用的目的,patching 的工作原理是一个从原始点跳转到修补点的 jmp 和完成修补后的返回。在位于原始代码中的 patching 点,一个 jmp 指令被写入(这会消耗 5 字节的原始指令长度空间),然后将执行流重定向到一个设计用以存放 patch 指令的内存块,原来被 jmp 替换的指令被挪到 patch 代码的结尾处,随后,一个最终的 jmp 指令将执行流重新重定向到原始代码 patch 点后的下一句指令处。鉴于此,有一些特殊的要求需要被满足:
- patch 点的指令必须可重定位(例如,不能有短跳转)
- 在 patch 点,必须要有单条指令或多条指令属于同一执行流(在这之内不能有任何定向到别处的交叉引用)
脑海里有了这些后,我选择了一个实际的 patch 点。为了写入尽可能少的 patch 代码,我将 patch 点设为了跳转到函数退出处的第一个分支指令(这样绕开了图像数据的处理过程),所以我可以越过这里(这句建议看原文)。在上面的代码截图中,patch 点被标记为红色,同时相应的 jump 指令被标记为亮粉色。在 patch 点,对 pvClientObjGet 的调用结果会被检验是否为0,后面紧跟一个用于跳转到函数退出处的 jne 指令(如果 rax=0).在这处逻辑里同一个 jne 可以帮助我们的 patch 代码退出函数:如果 cbBitsSrc 的值小于 cxSrc * cySrc * 4 我们只需要将 rax 设为 0 即可。
第一眼来看,选择 000007fe`feeae3b8 作为 patch 点似乎是可取的,但是 test rax, rax 指令只占 3 个字节,所以 Patch 点也会覆写 jne 指令(这是不可重定位的,因为上面说了,必须要有满足条件的 5 个字节的空间可用)。而在当前(我)选择的位置, patching 影响两个 3字节 的指令 - mov rsi,rax 和 test rax,rax - 这两条指令都是可重定位的。
在选好了 patch 点后,我们最终可用经实际的 patch 代码写入。下面是 一个用来准备被 0patch Builder (包含在 0patch Agent for Developers - 对其的用法我们已经在前一篇博文里面讨论过了。)所编译的 .0pp patch 文件的内容。
在代码的开头部分,是对 cxSrc * cySrc * 4 的计算过程,结果被保存到 rcx 。这两个操作被紧跟着一个溢出检查。所以我们并不以看上去似乎在边界内但实际上被过大的因素所生成的产品结尾(这句建议看原文)。在例子里面,非法的 图像尺寸被检测到了,为了显示 “Exploit Blocked ” 对话框, 一个对位于我们的 0patch Agent 内的特殊函数的调用被执行,最重要的是,rax 寄存器被设为 0,所以接下来的 tes rax,rax 指令可以将条件断点的条件状态设为指向亮粉色处的跳转(这就导致了函数退出)
;target platform: Windows 7 x64
;
RUN_CMD C:\Program Files\Internet Explorer\iexplore.exe C:\0patch\Patches\ZP-gdi32\poc.emf
MODULE_PATH "C:\Windows\System32\gdi32.dll"
PATCH_ID 259
PATCH_FORMAT_VER 2
VULN_ID 2135
PLATFORM win64
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x0004e3b5
;注意:我们检查了 rcx 寄存器可以自由使用.
; 我们并不关心是否 rsi 寄存器被 blacok_it 影响,因为在这个例子里面
; 它不会再被任何人使用
code_start
imul ecx, dword[rbx+28h], 04h ; cxSrc * 4 (每个像素用4个字节表示)
jc block_it ; 是否溢出
imul ecx, dword[rbx+2ch] ; * cySrc
jc block_it ; 是否溢出
cmp ecx,dword[rbx+3ch] ; cbBitsSrc < cxSrc * cySrc * 4
jbe skip
block_it:
call PIT_ExploitBlocked ; 显示 "Exploit Blocked" 对话框
xor rax,rax ; 在修补完后,为原来的 jne 设置跳转条件
skip:
code_end
patchlet_end
顺带说明一下,通过 windbg 的 dt 命令,EMRSETDIBITSTODEVICE 结构体内的成员相对偏移可以被通过 dumping type 获取到):
0:034> dt symbollib!EMRSETDIBITSTODEVICE
+0x000 emr : tagEMR
+0x008 rclBounds : _RECTL
+0x018 xDest : Int4B
+0x01c yDest : Int4B
+0x020 xSrc : Int4B
+0x024 ySrc : Int4B
+0x028 cxSrc : Int4B
+0x02c cySrc : Int4B
+0x030 offBmiSrc : Uint4B
+0x034 cbBmiSrc : Uint4B
+0x038 offBitsSrc : Uint4B
+0x03c cbBitsSrc : Uint4B
+0x040 iUsageSrc : Uint4B
+0x044 iStartScan : Uint4B
+0x048 cScans : Uint4B
但是,既然只加载来自微软符号服务器的符号表并不能看到类型信息,需要使用下面的方法,我编译了一个只有 EMRSETDIBITSTODEVICE x; 的 bare-bone dll 工程代码,实例声明为自定义的结构体。然后在加载 iexplore.exe 之前,我们将这个 DLL 加入 AppInit_DLLs 的注册表值,所以它就可以被加载到进程了(感谢这种方法)
在用 0patch Builder 编译这个 .0pp 文件后,补丁起作用了。一个空图像取代了原来的彩虹图像显示在浏览器内,而且一个 "Exploit Blocked" 对话框被弹出。
这是一段我捕获上述描述过程的视频。
所以,这是我第一个修复的 0day,虽然不是最严重的问题。我以一种颤栗的思维在想,如果不是一张彩虹图,而是一个恶意页面,就可以从我的浏览器内存里面窃取我在线银行的证书或者获取一张我昨晚派对的照片。
如果你装了 0patch Agent ,从 ZP-258 到 ZP-264 的补丁都应当已经存在在你的电脑里。如果没有的话,你可以下载一个免拷贝版 0patch Agent 来保护你自己在微软的官方补丁发布之前免被这些利用攻击。需要指出的是,当微软发布关于这些漏洞的更新后,它会替换存在漏洞的 gdi32.dll,由于我们的软件紧密关联漏洞 dll 的版本,那时我们的补丁也就自然停止工作了。我们已经为如下平台提供了这个漏洞的补丁:Windows 10 64bit, Windows 8.1 64bit, Windows 7 64bit 和 Windows 7 32bit
如果你想要自己写补丁,请不要犹豫,马上下载我们的 0patch Agent for Developers 并且试一下(我们提供了 .0pp 文件的下载地址,所以你可以自己编译它们。
现在我要写下一个 0patch 的 0day 补丁了。
原文发表时间: 2017-02-28
原文链接:https://0patch.blogspot.jp/2017/02/0patching-0-day-windows-gdi32dll-memory.html
翻译 by 银雁冰
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开
发者可享99元/年,续费同价!