【漏洞分析】CVE-2017-0038:GDI32越界读漏洞从分析到Exploit

http://p3.qhimg.com/t01728de3df72a48a86.jpg

作者:k0shl

稿费:600RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

前言

前段时间我在博客发了一篇关于CVE-2017-0037 Type Confusion的文章,其中完成了在关闭DEP情况下的Exploit,当时提到做这个漏洞的时候,感觉由于利用面的限制,导致似乎无法绕过ASLR,也就是DEP也不好绕过。当时完成那篇文章后,我恰巧看到了Google Project Zero公开的另一个漏洞CVE-2017-0038,这是一个EMF文件格式导致的Out-of-bound read,众所周知,在漏洞利用中,越界读这种漏洞类型很容易能够造成信息泄露。

换句话说,当时我考虑也许可以通过这个EMF的Out-of-bound Read造成信息泄露,然后获取某个内存基址,从而在CVE-2017-0037漏洞利用中构造ROP链来完成最后的利用(但事实证明好像还是不行2333)。

关于这个漏洞曝光之后,有人利用0patch对这个漏洞进行了修补。

PJ0关于CVE-2017-0038漏洞说明地址(含PoC EMF):

https://bugs.chromium.org/p/project-zero/issues/detail?id=992 

0patch修补CVE-2017-0038原文地址:

https://0patch.blogspot.jp/2017/02/0patching-0-day-windows-gdi32dll-memory.html 

安全客翻译地址:

http://bobao.360.cn/learning/detail/3578.html 

在这篇文章中,我将首先对CVE-2017-0038这个GDI32.dll的Out-of-bound Read漏洞进行分析;随后,我将首先将分享用JS编写一个浏览器可用的Exploit,随后我将和大家一起来看一下这么做的一个限制(为什么无法用浏览器进行Info leak),也是在0patch文章中提到的0xFF3333FF到底是怎么回事;然后,我将和大家分享用C语言完成Exploit,一起来看看真正泄露的内存内容;最后我将把JS和C的Exploit放在github上和大家分享。

调试环境是:

Windows 10 x86_64 build 10240
IE 11
GDI32.dll Version 10.0.10240.16384

http://p6.qhimg.com/t014607b4e06356eaea.png

请大家多多交流,感谢阅读!


CVE-2017-0038 PoC与漏洞分析

如我在第一章分享的测试环境的图片,同样在0patch的文章中也能看到,在浏览器每次加载poc.emf的时候,都会产生不同的图片,这张图片只有左下角的一个小红点是固定不变的,其实除了左下角四个字节,其他的内容都是泄露的内存,这点在后面的分析中我们可以获得。

那么现在我们需要解决的一个问题就是如何加载这个图片,这样我们需要用JS的画布功能来完成对PoC的构造,并且成功加载PoC.emf。

首先,我们定义一个canvas画布,之后通过js的getElementById来获得画布对象,之后我们通过Image()函数来初始化image对象,加载poc.emf,最后,我们通过drawImage来读取poc.emf,将poc.emf打印在画布上。drawImage后两个参数是image在canvas画布上的坐标,这里我们设置为0,0。

http://p9.qhimg.com/t01deaa2f8dc1368c11.png

完成构造后,我们稍微修改一下poc.emf的文件结构,然后打开IE11浏览器,通过Windbg附加进程,并且在gdi32!MRSETDIBITSTODEVICE::bPlay函数位置下断点,这个过程会在poc.emf映射在画布上时发生,允许js执行之后,Windbg命中函数入口。

0:022> x gdi32!MRSETDIBITSTODEVICE::bPlay
00007ff8`2a378730 GDI32!MRSETDIBITSTODEVICE::bPlay = <no type information>
0:022> bp gdi32!MRSETDIBITSTODEVICE::bPlay
Breakpoint 0 hit
GDI32!MRSETDIBITSTODEVICE::bPlay:
00007ff8`2a378730 48895c2408      mov     qword ptr [rsp+8],rbx ss:00000034`93cef6f0={GDI32!MRMETAFILE::bPlay (00007ff8`2a320950)}
0:027> kb
RetAddr:ArgstoChild: Call Site
00007ff8`2a2ff592 : 00007ff8`2a320950 00007ff8`2a2f8ed1 00000000`00000008 ffffffff`8f010c40 : GDI32!MRSETDIBITSTODEVICE::bPlay//到达目标断点
00007ff8`2a2ff0a8 : 0000002c`8f00be8c 00000000`00000000 00000000`00000000 00000000`00000000 : GDI32!PlayEnhMetaFileRecord+0xa2
00007ff8`2a327106 : 00000034`92acee00 00007ff8`2a2ff8f3 00000034`90380680 00000034`93cef988 : GDI32!bInternalPlayEMF+0x858
00007ff8`08650a70 : 00007ff8`08061010 00007ff8`08061010 00000034`93cefa10 00000000`000000ff : GDI32!PlayEnhMetaFile+0x26//关键函数调用PlayEnhMetaFile

现在我们开始单步跟踪这个关键函数的执行流程,之后我放出整个函数的伪代码,相应的注释,我已经写在//后面,首先单步执行,会到达一处参数赋值,在64位系统中,参数是靠寄存器传递的。

0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x1b:
00007ff8`2a37874b 488bd9          mov     rbx,rcx
0:027> r rcx
rcx=0000002c8f00be8c
0:027> dt vaultcli!EMRSETDIBITSTODEVICE 0000002c8f00be8c
   +0x000 emr              : tagEMR
   +0x008 rclBounds        : _RECTL
   +0x018 xDest            : 0n0
   +0x01c yDest            : 0n0
   +0x020 xSrc             : 0n0
   +0x024 ySrc             : 0n0
   +0x028 cxSrc            : 0n1
   +0x02c cySrc            : 0n1
   +0x030 offBmiSrc        : 0x4c
   +0x034 cbBmiSrc         : 0x28
   +0x038 offBitsSrc       : 0x74
   +0x03c cbBitsSrc        : 4
   +0x040 iUsageSrc        : 0
   +0x044 iStartScan       : 0
   +0x048 cScans           : 0x10

这里rcx传递的指针是MRSETDIBITSTODEVICE::bplay的第一个参数,这个参数是一个非常非常非常重要的结构体EMRSETDIBITSTODEVICE,正是对这个结构体中几个成员变量的控制没有进行严格的判断,从而导致了越界读漏洞的发生。

首先我们来看一下EMF文件格式。

http://p1.qhimg.com/t01f953a06bd0c3a2ea.png

这里,我对poc.emf进行了修改,修改了cxSrc和cySrc的值,这样在最后向HDC拷贝图像的时候,就不会读取多余的内存信息,这个结构体的变量我们要记录,因为接下来在跟踪函数内部逻辑的时候,会涉及到很多关于这个结构体成员变量的偏移,关于这个结构体变量的解释,可以参照MSDN。

https://msdn.microsoft.com/en-us/library/windows/desktop/dd162580(v=vs.85).aspx 

接下来我们继续单步跟踪,首先函数会命中一个叫做pvClientObjGet的函数,这个函数会根据Handle获取EMF头部section的一个标识并对标识进行判断。

0:027> p
GDI32!pvClientObjGet+0x2a:
00007ff8`2a301d5a 488d0daf141400  lea     rcx,[GDI32!aplHash (00007ff8`2a443210)]
0:027> p
GDI32!pvClientObjGet+0x31:
00007ff8`2a301d61 83e07f          and     eax,7Fh
0:027> p
GDI32!pvClientObjGet+0x34:
00007ff8`2a301d64 48833cc100      cmp     qword ptr [rcx+rax*8],0 ds:00007ff8`2a443218=0000003492a60080//获取特殊handle object
0:027> p
GDI32!pvClientObjGet+0x39:
00007ff8`2a301d69 488d3cc1        lea     rdi,[rcx+rax*8]
0:027> p
GDI32!pvClientObjGet+0x70://获得当前EMF头部section标识  ENHMETA_SIGNATURE
00007ff8`2a301da0 488b4718        mov     rax,qword ptr [rdi+18h] ds:00000034`92a60098=0000003492ac5280
0:026> r rax
rax=00000296033d3d30
0:026> dc 296033d3d30 l1
00000296`033d3d30  0000464d                             MF..

可以看到,最后函数会读取一个名为MF的标识,这个标识就是EMF文件的头部。这里会识别的就是ENHMETA_SIGNATURE结构。

http://p9.qhimg.com/t015b2f12bef923fe40.png

返回之后继续单步跟踪,接下来会命中bCheckRecord函数,这个函数主要负责的就是检查EMRSETDIBITSTODEVICE中成员变量的一些信息是否符合要求。

0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x3f:
00007ff8`2a37876f 498bd6          mov     rdx,r14
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x42:
00007ff8`2a378772 488bcb          mov     rcx,rbx
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x45:
00007ff8`2a378775 e8a6dbffff      call    GDI32!MRSETDIBITSTODEVICE::bCheckRecord (00007ff8`2a376320)
0:027> r rcx
rcx=0000002c8f00be8c//检查vaultcli!EMRSETDIBITSTODEVICE结构

可以看到,EMRSETDIBITSTODEVICE结构保存在rcx中,会作为第一个参数传入函数,接下来跟入函数中。

0:027> p//接下来检查tagEMR的nSize
GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x6:
00007ff8`2a376326 448b4104        mov     r8d,dword ptr [rcx+4] ds:0000002c`8f00be90=00000078
0:027> dt tagEMR 0000002c8f00be8c
vaultcli!tagEMR
   +0x000 iType            : 0x50
   +0x004 nSize            : 0x78
0:026> p
gdi32full!MRSETDIBITSTODEVICE::bCheckRecord+0x2a:
00007ffd`cf88a56a 39442430        cmp     dword ptr [rsp+30h],eax ss:000000bb`06b9f650=00000078
0:027> p//与0x78比较检查nSize
GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x6:
00007ff8`2a376326 448b4104        mov     r8d,dword ptr [rcx+4] ds:0000002c`8f00be90=00000078
……
0:027> p
GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x16://获得vaultcli!EMRSETDIBITSTODEVICE的cbBmiSrc成员变量值
00007ff8`2a376336 8b4934          mov     ecx,dword ptr [rcx+34h] ds:0000002c`8f00bec0=00000028
0:027> p
GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x19:
00007ff8`2a376339 bab0ffffff      mov     edx,0FFFFFFB0h
0:027> p
GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x1e://与0x0FFFFFFB0作比较,检查cbBmiSrc的上限
00007ff8`2a37633e 3bca            cmp     ecx,edx
0:027> p
GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x20:
00007ff8`2a376340 7343            jae     GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x65 (00007ff8`2a376385) [br=0]

因为代码片段较长,这里我列举了一些片段,主要就是对结构体中的一些成员变量进行检查,比如头部的tagEMR,会检查tagEMR中的nSize,后续还会检查cbBmiSrc(BitmapInfo大小)等等。

随后继续单步跟踪,会到达bClipped这个函数,这个函数的主要功能就是对EMRSETDIBITSTODEVICE结构体偏移0x8位置的成员,也就是_RECTL进行检查,_RECTL主要是负责这个图像的上下左右边界。

0:026> p//传递rbx+8地址值,是个_RECTL对象
gdi32full!MRSETDIBITSTODEVICE::bPlay+0x4e:
00007ffd`cf88dfae 488d5308        lea     rdx,[rbx+8]
0:026> p
gdi32full!MRSETDIBITSTODEVICE::bPlay+0x52:
00007ffd`cf88dfb2 488bcd          mov     rcx,rbp
0:026> p
gdi32full!MRSETDIBITSTODEVICE::bPlay+0x55:
00007ffd`cf88dfb5 e822caffff      call    gdi32full!MF::bClipped (00007ffd`cf88a9dc)
0:026> r rdx
rdx=0000029606a4a0e4
0:026> dt _RECTL 0000029606a4a0e4
vaultcli!_RECTL
   +0x000 left             : 0n0
   +0x004 top              : 0n0
   +0x008 right            : 0n15
   +0x00c bottom           : 0n15
0:027> r rcx
rcx=0000003492ac5280
0:027> dt _RECTL 0000003492ac5280+8c//这里偏移+8c是由于pvClientObjGet获取的对象偏移+8c存放的是比较值,具体在函数里体现
vaultcli!_RECTL
   +0x000 left             : 0n-1
   +0x004 top              : 0n-1
   +0x008 right            : 0n17
   +0x00c bottom           : 0n17

在rcx寄存器,也就是第一个参数中存放的是上下左右的界限,而第二个参数则是我们当前图像的RECTL,我们来看一下bClipped检查的伪代码。

__int64 __fastcall MF::bClipped(MF *this, struct ERECTL *a2)//this指针是pvClientObjGet对象,a2是RECTL对象,这两个对象对应偏移之间会有一个检查,检查当前RECTL对象是否在符合条件的范围内
{
  v2 = ERECTL::bEmpty(a2);//先判断要判断的地址非空
  v5 = 0;
  if ( v2 )
  {
    result = 0i64;
  }
  else
  {
    if ( *(_DWORD *)(v4 + 140) > *(_DWORD *)(v3 + 8)//检查上下左右是否符合要求
      || *(_DWORD *)(v4 + 148) < *(_DWORD *)v3
      || *(_DWORD *)(v4 + 144) > *(_DWORD *)(v3 + 12)
      || *(_DWORD *)(v4 + 152) < *(_DWORD *)(v3 + 4) )
    {
      v5 = 1;
    }
    result = (unsigned int)v5;
  }
  return result;
}

在函数中当RECTL对象不为空时,会和标准对象的上下左右进行比较,看是否超出界限大小(在else语句逻辑中),随后如果满足在标准大小范围内,返回为1,程序会继续执行。

接下来程序会分别进入三个函数逻辑,这三个函数逻辑和HDC相关。详细请参照我的注释。

0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x68://获取xDest,x轴值
00007ff8`2a378798 8b4318          mov     eax,dword ptr [rbx+18h] ds:0000002c`8f00bea4=00000000
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x6b:
00007ff8`2a37879b 488d9424a0000000 lea     rdx,[rsp+0A0h]
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x73:
00007ff8`2a3787a3 898424a0000000  mov     dword ptr [rsp+0A0h],eax ss:00000034`93cef700=00000008
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x7a:
00007ff8`2a3787aa 41b801000000    mov     r8d,1
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x80://获取yDest,y轴值
00007ff8`2a3787b0 8b431c          mov     eax,dword ptr [rbx+1Ch] ds:0000002c`8f00bea8=00000000
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x83:
00007ff8`2a3787b3 898424a4000000  mov     dword ptr [rsp+0A4h],eax ss:00000034`93cef704=00000000
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x8a://获取hdc
00007ff8`2a3787ba 488b8dd8020000  mov     rcx,qword ptr [rbp+2D8h] ss:00000034`92ac5558=ffffffffe90107a2
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x91://LPtoDP把x、y坐标发给hdc
00007ff8`2a3787c1 e8ba87faff      call    GDI32!LPtoDP (00007ff8`2a320f80)
……
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x9a://SetWorldTransform函数为指定的设备上下文在全局空间和页空间之间设置一个二维线性变换。 此变换可用于缩放,旋转,剪切或转换图形输出。
00007ff8`2a3787ca 488d95c0020000  lea     rdx,[rbp+2C0h]
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xa1:
00007ff8`2a3787d1 41b804000000    mov     r8d,4
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xa7:
00007ff8`2a3787d7 498bcf          mov     rcx,r15
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xaa:
00007ff8`2a3787da e8815cf8ff      call    GDI32!ModifyWorldTransform (00007ff8`2a2fe460)
0:027> r rdx
rdx=0000003492ac5540
0:027> dt XFORM 0000003492ac5540//线形变换参数
vaultcli!XFORM
   +0x000 eM11             : 0.9870000482 
   +0x004 eM12             : 0 
   +0x008 eM21             : 0 
   +0x00c eM22             : 0.9893333316 
   +0x010 eDx              : 0 
   +0x014 eDy              : 0 
……
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xb3://获取cbBmiSrc成员值,负责BITMAP的大小
00007ff8`2a3787e3 448b4b34        mov     r9d,dword ptr [rbx+34h] ds:0000002c`8f00bec0=00000028
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xb7:
00007ff8`2a3787e7 498bd6          mov     rdx,r14
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xba://获得offBmiSrc成员变量值,负责BITMAP的偏移
00007ff8`2a3787ea 448b4330        mov     r8d,dword ptr [rbx+30h] ds:0000002c`8f00bebc=0000004c
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xbe:
00007ff8`2a3787ee 488bcb          mov     rcx,rbx
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xc1://主要就是Check tagBITMAPINFO
00007ff8`2a3787f1 e852d6faff      call    GDI32!MR::bValidOffExt (00007ff8`2a325e48)
0:027> r rdx
rdx=0000002c8f00be8c
0:027> dt tagBITMAPINFO 2c8f00be8c+4c
vaultcli!tagBITMAPINFO
   +0x000 bmiHeader        : tagBITMAPINFOHEADER
   +0x028 bmiColors        : [1] tagRGBQUAD
0:027> dt tagBITMAPINFOHEADER 2c8f00be8c+4c
vaultcli!tagBITMAPINFOHEADER
   +0x000 biSize           : 0x28
   +0x004 biWidth          : 0n16
   +0x008 biHeight         : 0n16
   +0x00c biPlanes         : 1
   +0x00e biBitCount       : 0x18
   +0x010 biCompression    : 0
   +0x014 biSizeImage      : 4
   +0x018 biXPelsPerMeter  : 0n0
   +0x01c biYPelsPerMeter  : 0n0
   +0x020 biClrUsed        : 0
   +0x024 biClrImportant   : 0

如注释内容,这三个函数会分别对坐标,线性变换的参数,以及EMRSETDIBITSTODEVICE结构体的BITMAPINFO成员变量进行获取赋值和检查。这些赋值和检查如果成功,都会返回非0值,这样才能继续下面的逻辑,接下来,bPlay函数会为BITMAPINFO开辟地址空间。

0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xce:
00007ff8`2a3787fe b8f8040000      mov     eax,4F8h
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xd3://获得LocalAlloc的第一个参数nType
00007ff8`2a378803 b940000000      mov     ecx,40h
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xd8://判断cbBmiSrc和4f8的大小
00007ff8`2a378808 394334          cmp     dword ptr [rbx+34h],eax ds:0000002c`8f00bec0=00000028
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xdb://如果大于则将cbBmiSrc的大小作为开辟的空间大小,否则就开辟4f8
00007ff8`2a37880b 0f474334        cmova   eax,dword ptr [rbx+34h] ds:0000002c`8f00bec0=00000028
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xdf://获得要开辟空间的大小
00007ff8`2a37880f 8bd0            mov     edx,eax
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xe1://开辟4f8的bitmapinfo空间
00007ff8`2a378811 ff15d9190300    call    qword ptr [GDI32!_imp_LocalAlloc (00007ff8`2a3aa1f0)] ds:00007ff8`2a3aa1f0={KERNELBASE!LocalAlloc (00007ff8`2810fe40)}
0:027> r edx
edx=4f8
0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0xe7:
00007ff8`2a378817 488bf0          mov     rsi,rax
0:027> r rax
rax=0000003492a637b0//开辟出4f8的空间,并且获取堆指针
0:027> dd 3492a637b0 l5
00000034`92a637b0  00000000 00000000 00000000 00000000
00000034`92a637c0  00000000

在函数中,会对开辟的空间进行一个判断,如果BITMAPINFO的size大于4f8,则开辟BITMAPINFO size大小空间,如果小于的话,则直接开辟4f8空间,开辟后,会将当前EMRSETDIBITSTODEVICE结构体的BITMAPINFO拷贝进去,在EMRSETDIBITSTODEVICE结构体中offBmiSrc是BITMAPINFO距离EMRSETDIBITSTODEVICE结构体的偏移,而cbBmiSrc则是BITMAPINFO的大小。

根据我们当前的情况,偏移是0x4c,大小是0x28,随后会执行memcpy拷贝BITMAPINFO。

0:027> p
GDI32!MRSETDIBITSTODEVICE::bPlay+0x100://拷贝bitmapinfo
00007ff8`2a378830 e8d34cfbff      call    GDI32!memcpy (00007ff8`2a32d508)
0:027> r r8
r8=0000000000000028
0:027> r rdx
rdx=0000002c8f00bed8
0:027> dd 2c8f00bed8
0000002c`8f00bed8  00000028 00000010 00000010 00180001
0000002c`8f00bee8  00000000 00000004 00000000 00000000
0000002c`8f00bef8  00000000 00000000 00ed1c24 0000000e

可以看到,拷贝的内容正是我们EMF文件中对应BITMAPINFO区域的内容,这里要注意,其实这里拷贝的只是BITMAP的信息,而并不是我们真正图像的内容,因此这里还不是造成内存泄露的原因。

接下来会进行一系列的变量判断,这些变量判断我将在下面的伪代码的注释中给大家讲解,但是这一系列的判断并没有判断引发这个漏洞最关键的部分,随后我们会看到一处关键函数调用。

      LOBYTE(v6) = StretchDIBits(
                     v3,
                     pt.x,
                     pt.y,
                     *((_DWORD *)v4 + 10),//
                     *((_DWORD *)v4 + 11),
                     *((_DWORD *)v4 + 8),
                     *((_DWORD *)v4 + 9) - *((_DWORD *)v4 + 17),
                     *((_DWORD *)v4 + 10),//宽和高,0x10
                     *((_DWORD *)v4 + 11),
                     v15,//this is important pointer,指向要拷贝的bitmap指针
                     v11,
                     *((_DWORD *)v4 + 16),
                     0xCC0020u) != 0;

这个函数调用,会拷贝当前的图像像素到目标的设备(画布)中,而这个过程拷贝的内容就是v15,这个v15变量是指向要拷贝内容的指针,而拷贝取决于之前我们定义的cxSrc和cySrc,拷贝的大小是cxSrc*cySrc*4,而当前v15的值是什么呢,这取决于EMRSETDIBITSTODEVICE结构体的offBitsSrc,这里就是当前结构体偏移+0x74的位置。

0:026> p
gdi32full!MRSETDIBITSTODEVICE::bPlay+0x1a4:
00007ffd`cf88e104 ff1596ee0300    call    qword ptr [gdi32full!_imp_StretchDIBits (00007ffd`cf8ccfa0)] ds:00007ffd`cf8ccfa0={GDI32!StretchDIBits (00007ffd`d1143370)}
0:026> dd 29606a4a0dc+74
00000296`06a4a150  00ed1c24 0000000e 00000014 00000000
00000296`06a4a160  00000010 00000014 18abea8b 90018400

而本来图像大小是取决于BITMAPINFOHEADER和EMRSETIDIBITSTODEVICE共同决定,在BITMAPINFOHEADER中的很多因素决定了图像bitmap的性质,比如bicompression决定了图像的压缩算法或者是否压缩(本实例中为0x0,no compression,这样才能正常泄露内存),而图像内容的偏移和bitmap内容保存在EMRSETDIBITSTODEVICE,但在上述的分析过程中,bplay处理逻辑并没有对要拷贝的内容的大小和图像本身大小bBitsSrc进行比较,而直接拷贝了,导致拷贝了cxSrc*cySrc*3的内存空间,造成了内存泄露。这里为了4字节对齐,会对泄露的内存空间的3字节补上一个0xFF。(笔者注:感谢@程序人生对文章的质疑,这里笔者在进行分析的过程中,在最后这个过程直接参照了0patch文章中的漏洞成因,而没有详细分析StrechToDIB函数的逻辑)

下面我贴出这个函数的伪代码,相关注释已经写在伪代码中。

__int64 __fastcall MRSETDIBITSTODEVICE::bPlay(MRSETDIBITSTODEVICE *this, void *a2, struct tagHANDLETABLE *a3)
{
  HDC v3; // r15@1
  MRSETDIBITSTODEVICE *v4; // rbx@1
  struct tagHANDLETABLE *v5; // r14@1
  unsigned int v6; // edi@1
  __int64 v7; // rax@1
  __int64 v8; // rbp@1
  signed int v10; // eax@9
  BITMAPINFO *v11; // rax@11
  const BITMAPINFO *v12; // rsi@11
  signed int v13; // eax@12
  int v14; // eax@14
  unsigned __int32 v15; // er9@16
  char *v16; // r8@19
  struct tagPOINT pt; // [sp+A0h] [bp+18h]@6
  v3 = (HDC)a2;
  v4 = this;
  v5 = a3;
  v6 = 0;
  LODWORD(v7) = pvClientObjGet(a3->objectHandle[0], 4587520i64);//a3是头部,判断ENHMETA_SIGNATURE是不是EMF
  v8 = v7;
  if ( !v7 || !(unsigned int)MRSETDIBITSTODEVICE::bCheckRecord(v4, v5) )//满足v6是EMF,且bCheckRecord会对EMRSETDIBITSTODEVICE结构体成员变量的大小作检查(仅仅是对结构体各自成员变量大小,而没有检查bitmap整体size)
    return 0i64;
  if ( MF::bClipped((MF *)v8, (MRSETDIBITSTODEVICE *)((char *)v4 + 8)) )//这个函数会check MF和ERECTL的上下左右值,是否在满足范围内
    return 1i64;
  pt.x = *((_DWORD *)v4 + 6);//对EMF的xDest和yDest进行传递
  pt.y = *((_DWORD *)v4 + 7);
  if ( !LPtoDP(*(HDC *)(v8 + 728), &pt, 1)//将逻辑坐标转换成HDC的坐标
    || !SetWorldTransform(v3, (const XFORM *)(v8 + 704))//建立用于转换,输出图形的二维线形变换
    || !(unsigned int)MR::bValidOffExt(v4, v5, *((_DWORD *)v4 + 12), *((_DWORD *)v4 + 13)) )//检查bitmap信息正确性
  {
    return 0i64;
  }
  v10 = 1272;
  if ( *((_DWORD *)v4 + 13) > 0x4F8u )
    v10 = *((_DWORD *)v4 + 13);
  v11 = (BITMAPINFO *)LocalAlloc(0x40u, (unsigned int)v10);//开辟一个V10(4f8)大小的空间
  v12 = v11;
  if ( v11 )
  {
    memcpy(v11, (char *)v4 + *((_DWORD *)v4 + 12), *((_DWORD *)v4 + 13));//拷贝bitmapinfo到目标内存
    v13 = 248;
    if ( v12->bmiHeader.biSize < 0xF8 )判断bitmapinfoheader中bisize大小
      v13 = v12->bmiHeader.biSize;//小于f8则当前值
    v12->bmiHeader.biSize = v13;//大于f8则f8,限定bisize最大值
    v14 = *((_DWORD *)v4 + 18);//
    if ( v12->bmiHeader.biHeight <= 0 )//如果biHeight为负数,则转换成正数(防止Integer Overflow?)
      v14 = -v14;
    v12->bmiHeader.biHeight = v14;
    v12->bmiHeader.biSizeImage = *((_DWORD *)v4 + 15);//设定bitmapinfoheader中biSizeImage值为cbBitsSrc
    v15 = *((_DWORD *)v4 + 15);//cbBitSize交给v15
    if ( !v15 || (unsigned int)MR::bValidOffExt(v4, v5, *((_DWORD *)v4 + 14), v15) )
    {
      if ( *((_DWORD *)v4 + 15) )//如果cbBitSize不为0
        v16 = (char *)v4 + *((_DWORD *)v4 + 14);//则v16为EMRSETDIBSITODEVICE结构+偏移14,也就是offBitsSrc
      else
        v16 = 0i64;
      LOBYTE(v6) = StretchDIBits(//拷贝目标像素到指定矩形中,没有判断,产生漏洞
                     v3,
                     pt.x,
                     pt.y,
                     *((_DWORD *)v4 + 10),
                     *((_DWORD *)v4 + 11),
                     *((_DWORD *)v4 + 8),
                     *((_DWORD *)v4 + 9) - *((_DWORD *)v4 + 17),
                     *((_DWORD *)v4 + 10),
                     *((_DWORD *)v4 + 11),
                     v16,//指向要拷贝bitmap的指针
                     v12,
                     *((_DWORD *)v4 + 16),
                     0xCC0020u) != 0;
    }
  }
  LocalFree((HLOCAL)v12);
  MF::bSetTransform((MF *)v8, v3);
  return v6;
}

JS Exploit与Web Safe Color

到此,我们分析了这个漏洞的成因,可能读到这里大家都会有一些和我当时一样的疑问,就是为什么我们要打印的内容是0x00ED1C24,也就是说,这里不管内存如何泄露,固定不变的值应该是0x00ED1C24,但是像诸如0patch文章中所说的,固定的值却是0xFF3333FF呢。

首先我们一起来把这个PoC修改成Exploit,用JS在网页中打印泄露的内存地址,在之前我们将cxSrc和cySrc修改回泄露内存的PoC,接下来,通过getImageData的方法,来获得图像的像素,之后将这个值打印。打印的Length实际上我输出多了,这里只需要1024(0x10*0x10*4)足以表示整个poc.emf图像。

http://p9.qhimg.com/t0164d1f8c1a9c21080.png

可以看到,我们泄露了内存地址的信息,这些信息其实在本质上是包含着很多内容的。比如一些关键的内存地址信息等等。但是在测试的过程中,我没有发现有关浏览器的一些信息,比如cookie之类的,不知道是不是因为我的浏览器比较干净,内存驻留的信息较少。

http://p1.qhimg.com/t01b488e610a67adb80.png

但这里有几个问题,第一个就是我多次调试之后,发现内存泄露的信息位置很不稳定,也就是说,它可以用来泄露浏览器信息,但是用来做稳定的info leak来bypass ASLR似乎不太可行,其次,我们来对比一下泄露内存的内容和我之前在浏览器中打印的图像泄露的内容是不同的。

这也是为什么无论是我调试还是0patch文章中都会在浏览器打印0xFF3333FF的原因,Web Safe Color!Web Safe Color是一种安全颜色的表示,开发者认为在256种颜色中,只需要216种颜色就能保证浏览器稳定输出图像,而不会让图像产生抖动,而这216种颜色用0x00,0x33,0x66,0x99,0xcc,0xff组合就能表示了,也就是说我们的内存在图像打印的时候,会被web safe color强制转换。

下面我修改poc.emf中bitmap的像素值,来看看在浏览器中图像强制转换打印的内容。

http://p9.qhimg.com/t01c5280e58c351f757.png

http://p1.qhimg.com/t019bd38cb5040f6496.png

所以可以看到这个过程不可逆,因此,至少在IE11浏览器,由于Web Safe Color导致我们内存泄露方法获取一些敏感数据的思路似乎不太可行,接下来,我们通过C语言来写一个Exploit,来看一下真正的内存泄露。


CVE-2017-0038 Out-of-bound Read Exploit

重新回过头看一下之前的bplay函数断点,实际上,这里调用了一个GDI32的API,叫做PlayEnhMetaFile,正是这个API内层函数调用到了bplay,因此我们在C中通过PlayEnhMetaFile来调用MRSETDIBITSTODEVICE::bplay。这个过程会将EMF文件转储到hdc上,这样,我们就构建了一个基本的Exploit思路:

通过GetDC函数来获取一个HDC

通过GetEnhMetaFile来加载poc.emf

通过PlayEnhMetaFile来将hemf转储到hedc中

通过GetPixel来从hedc中读取像素值,这个像素值泄露了内存信息,保存到一个DWORD数组里。

http://p5.qhimg.com/t01ad1db62de8de8a08.png

可以看到,我们的DWORD color[]数组读取到了更多的内存信息,但其实在我们当前的进程空间里,这样的内存信息还是太少了,换句话说当前进程太单纯2333,我们可以正常打印PJ0提供的一个poc.emf,由于这个GetDC是Null,图像将会打印在左上角。

http://p7.qhimg.com/t01ade368589df47845.png

可以看到,除了左下角的0x00241ced,其他的都是0x00(#00000000表示黑色),也就是初始化的内存空间(这里0x00241ced也是红色),到此我们完成了对于这个漏洞的分析和利用,如有不当之处,还望大家多多包含,多多交流,谢谢!

最后我把exploit地址放在末尾:

https://github.com/k0keoyo/CVE-2017-0038-EXP-C-JS 

(完)