2017-03-23 10:11:44
来源:安全客 作者:k0shl
阅读:386次
点赞(0)
收藏
作者: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上和大家分享。
调试环境是:
Windows10x86_64build10240 IE11 GDI32.dllVersion10.0.10240.16384请大家多多交流,感谢阅读!
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。
完成构造后,我们稍微修改一下poc.emf的文件结构,然后打开IE11浏览器,通过Windbg附加进程,并且在gdi32!MRSETDIBITSTODEVICE::bPlay函数位置下断点,这个过程会在poc.emf映射在画布上时发生,允许js执行之后,Windbg命中函数入口。
0:022>xgdi32!MRSETDIBITSTODEVICE::bPlay 00007ff8`2a378730GDI32!MRSETDIBITSTODEVICE::bPlay=<notypeinformation> 0:022>bpgdi32!MRSETDIBITSTODEVICE::bPlay Breakpoint0hit GDI32!MRSETDIBITSTODEVICE::bPlay: 00007ff8`2a37873048895c2408movqwordptr[rsp+8],rbxss:00000034`93cef6f0={GDI32!MRMETAFILE::bPlay(00007ff8`2a320950)} 0:027>kb RetAddr:ArgstoChild:CallSite 00007ff8`2a2ff592:00007ff8`2a32095000007ff8`2a2f8ed100000000`00000008ffffffff`8f010c40:GDI32!MRSETDIBITSTODEVICE::bPlay//到达目标断点 00007ff8`2a2ff0a8:0000002c`8f00be8c00000000`0000000000000000`0000000000000000`00000000:GDI32!PlayEnhMetaFileRecord+0xa2 00007ff8`2a327106:00000034`92acee0000007ff8`2a2ff8f300000034`9038068000000034`93cef988:GDI32!bInternalPlayEMF+0x858 00007ff8`08650a70:00007ff8`0806101000007ff8`0806101000000034`93cefa1000000000`000000ff:GDI32!PlayEnhMetaFile+0x26//关键函数调用PlayEnhMetaFile现在我们开始单步跟踪这个关键函数的执行流程,之后我放出整个函数的伪代码,相应的注释,我已经写在//后面,首先单步执行,会到达一处参数赋值,在64位系统中,参数是靠寄存器传递的。
0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x1b: 00007ff8`2a37874b488bd9movrbx,rcx 0:027>rrcx rcx=0000002c8f00be8c 0:027>dtvaultcli!EMRSETDIBITSTODEVICE0000002c8f00be8c +0x000emr:tagEMR +0x008rclBounds:_RECTL +0x018xDest:0n0 +0x01cyDest:0n0 +0x020xSrc:0n0 +0x024ySrc:0n0 +0x028cxSrc:0n1 +0x02ccySrc:0n1 +0x030offBmiSrc:0x4c +0x034cbBmiSrc:0x28 +0x038offBitsSrc:0x74 +0x03ccbBitsSrc:4 +0x040iUsageSrc:0 +0x044iStartScan:0 +0x048cScans:0x10这里rcx传递的指针是MRSETDIBITSTODEVICE::bplay的第一个参数,这个参数是一个非常非常非常重要的结构体EMRSETDIBITSTODEVICE,正是对这个结构体中几个成员变量的控制没有进行严格的判断,从而导致了越界读漏洞的发生。
首先我们来看一下EMF文件格式。
这里,我对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`2a301d5a488d0daf141400learcx,[GDI32!aplHash(00007ff8`2a443210)] 0:027>p GDI32!pvClientObjGet+0x31: 00007ff8`2a301d6183e07fandeax,7Fh 0:027>p GDI32!pvClientObjGet+0x34: 00007ff8`2a301d6448833cc100cmpqwordptr[rcx+rax*8],0ds:00007ff8`2a443218=0000003492a60080//获取特殊handleobject 0:027>p GDI32!pvClientObjGet+0x39: 00007ff8`2a301d69488d3cc1leardi,[rcx+rax*8] 0:027>p GDI32!pvClientObjGet+0x70://获得当前EMF头部section标识ENHMETA_SIGNATURE 00007ff8`2a301da0488b4718movrax,qwordptr[rdi+18h]ds:00000034`92a60098=0000003492ac5280 0:026>rrax rax=00000296033d3d30 0:026>dc296033d3d30l1 00000296`033d3d300000464dMF..可以看到,最后函数会读取一个名为MF的标识,这个标识就是EMF文件的头部。这里会识别的就是ENHMETA_SIGNATURE结构。
返回之后继续单步跟踪,接下来会命中bCheckRecord函数,这个函数主要负责的就是检查EMRSETDIBITSTODEVICE中成员变量的一些信息是否符合要求。
0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x3f: 00007ff8`2a37876f498bd6movrdx,r14 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x42: 00007ff8`2a378772488bcbmovrcx,rbx 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x45: 00007ff8`2a378775e8a6dbffffcallGDI32!MRSETDIBITSTODEVICE::bCheckRecord(00007ff8`2a376320) 0:027>rrcx rcx=0000002c8f00be8c//检查vaultcli!EMRSETDIBITSTODEVICE结构可以看到,EMRSETDIBITSTODEVICE结构保存在rcx中,会作为第一个参数传入函数,接下来跟入函数中。
0:027>p//接下来检查tagEMR的nSize GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x6: 00007ff8`2a376326448b4104movr8d,dwordptr[rcx+4]ds:0000002c`8f00be90=00000078 0:027>dttagEMR0000002c8f00be8c vaultcli!tagEMR +0x000iType:0x50 +0x004nSize:0x78 0:026>p gdi32full!MRSETDIBITSTODEVICE::bCheckRecord+0x2a: 00007ffd`cf88a56a39442430cmpdwordptr[rsp+30h],eaxss:000000bb`06b9f650=00000078 0:027>p//与0x78比较检查nSize GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x6: 00007ff8`2a376326448b4104movr8d,dwordptr[rcx+4]ds:0000002c`8f00be90=00000078 …… 0:027>p GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x16://获得vaultcli!EMRSETDIBITSTODEVICE的cbBmiSrc成员变量值 00007ff8`2a3763368b4934movecx,dwordptr[rcx+34h]ds:0000002c`8f00bec0=00000028 0:027>p GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x19: 00007ff8`2a376339bab0ffffffmovedx,0FFFFFFB0h 0:027>p GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x1e://与0x0FFFFFFB0作比较,检查cbBmiSrc的上限 00007ff8`2a37633e3bcacmpecx,edx 0:027>p GDI32!MRSETDIBITSTODEVICE::bCheckRecord+0x20: 00007ff8`2a3763407343jaeGDI32!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`cf88dfae488d5308leardx,[rbx+8] 0:026>p gdi32full!MRSETDIBITSTODEVICE::bPlay+0x52: 00007ffd`cf88dfb2488bcdmovrcx,rbp 0:026>p gdi32full!MRSETDIBITSTODEVICE::bPlay+0x55: 00007ffd`cf88dfb5e822caffffcallgdi32full!MF::bClipped(00007ffd`cf88a9dc) 0:026>rrdx rdx=0000029606a4a0e4 0:026>dt_RECTL0000029606a4a0e4 vaultcli!_RECTL +0x000left:0n0 +0x004top:0n0 +0x008right:0n15 +0x00cbottom:0n15 0:027>rrcx rcx=0000003492ac5280 0:027>dt_RECTL0000003492ac5280+8c//这里偏移+8c是由于pvClientObjGet获取的对象偏移+8c存放的是比较值,具体在函数里体现 vaultcli!_RECTL +0x000left:0n-1 +0x004top:0n-1 +0x008right:0n17 +0x00cbottom:0n17在rcx寄存器,也就是第一个参数中存放的是上下左右的界限,而第二个参数则是我们当前图像的RECTL,我们来看一下bClipped检查的伪代码。
__int64__fastcallMF::bClipped(MF*this,structERECTL*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=(unsignedint)v5; } returnresult; }在函数中当RECTL对象不为空时,会和标准对象的上下左右进行比较,看是否超出界限大小(在else语句逻辑中),随后如果满足在标准大小范围内,返回为1,程序会继续执行。
接下来程序会分别进入三个函数逻辑,这三个函数逻辑和HDC相关。详细请参照我的注释。
0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x68://获取xDest,x轴值 00007ff8`2a3787988b4318moveax,dwordptr[rbx+18h]ds:0000002c`8f00bea4=00000000 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x6b: 00007ff8`2a37879b488d9424a0000000leardx,[rsp+0A0h] 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x73: 00007ff8`2a3787a3898424a0000000movdwordptr[rsp+0A0h],eaxss:00000034`93cef700=00000008 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x7a: 00007ff8`2a3787aa41b801000000movr8d,1 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x80://获取yDest,y轴值 00007ff8`2a3787b08b431cmoveax,dwordptr[rbx+1Ch]ds:0000002c`8f00bea8=00000000 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x83: 00007ff8`2a3787b3898424a4000000movdwordptr[rsp+0A4h],eaxss:00000034`93cef704=00000000 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x8a://获取hdc 00007ff8`2a3787ba488b8dd8020000movrcx,qwordptr[rbp+2D8h]ss:00000034`92ac5558=ffffffffe90107a2 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x91://LPtoDP把x、y坐标发给hdc 00007ff8`2a3787c1e8ba87faffcallGDI32!LPtoDP(00007ff8`2a320f80) …… 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0x9a://SetWorldTransform函数为指定的设备上下文在全局空间和页空间之间设置一个二维线性变换。此变换可用于缩放,旋转,剪切或转换图形输出。 00007ff8`2a3787ca488d95c0020000leardx,[rbp+2C0h] 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xa1: 00007ff8`2a3787d141b804000000movr8d,4 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xa7: 00007ff8`2a3787d7498bcfmovrcx,r15 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xaa: 00007ff8`2a3787dae8815cf8ffcallGDI32!ModifyWorldTransform(00007ff8`2a2fe460) 0:027>rrdx rdx=0000003492ac5540 0:027>dtXFORM0000003492ac5540//线形变换参数 vaultcli!XFORM +0x000eM11:0.9870000482 +0x004eM12:0 +0x008eM21:0 +0x00ceM22:0.9893333316 +0x010eDx:0 +0x014eDy:0 …… 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xb3://获取cbBmiSrc成员值,负责BITMAP的大小 00007ff8`2a3787e3448b4b34movr9d,dwordptr[rbx+34h]ds:0000002c`8f00bec0=00000028 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xb7: 00007ff8`2a3787e7498bd6movrdx,r14 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xba://获得offBmiSrc成员变量值,负责BITMAP的偏移 00007ff8`2a3787ea448b4330movr8d,dwordptr[rbx+30h]ds:0000002c`8f00bebc=0000004c 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xbe: 00007ff8`2a3787ee488bcbmovrcx,rbx 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xc1://主要就是ChecktagBITMAPINFO 00007ff8`2a3787f1e852d6faffcallGDI32!MR::bValidOffExt(00007ff8`2a325e48) 0:027>rrdx rdx=0000002c8f00be8c 0:027>dttagBITMAPINFO2c8f00be8c+4c vaultcli!tagBITMAPINFO +0x000bmiHeader:tagBITMAPINFOHEADER +0x028bmiColors:[1]tagRGBQUAD 0:027>dttagBITMAPINFOHEADER2c8f00be8c+4c vaultcli!tagBITMAPINFOHEADER +0x000biSize:0x28 +0x004biWidth:0n16 +0x008biHeight:0n16 +0x00cbiPlanes:1 +0x00ebiBitCount:0x18 +0x010biCompression:0 +0x014biSizeImage:4 +0x018biXPelsPerMeter:0n0 +0x01cbiYPelsPerMeter:0n0 +0x020biClrUsed:0 +0x024biClrImportant:0如注释内容,这三个函数会分别对坐标,线性变换的参数,以及EMRSETDIBITSTODEVICE结构体的BITMAPINFO成员变量进行获取赋值和检查。这些赋值和检查如果成功,都会返回非0值,这样才能继续下面的逻辑,接下来,bPlay函数会为BITMAPINFO开辟地址空间。
0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xce: 00007ff8`2a3787feb8f8040000moveax,4F8h 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xd3://获得LocalAlloc的第一个参数nType 00007ff8`2a378803b940000000movecx,40h 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xd8://判断cbBmiSrc和4f8的大小 00007ff8`2a378808394334cmpdwordptr[rbx+34h],eaxds:0000002c`8f00bec0=00000028 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xdb://如果大于则将cbBmiSrc的大小作为开辟的空间大小,否则就开辟4f8 00007ff8`2a37880b0f474334cmovaeax,dwordptr[rbx+34h]ds:0000002c`8f00bec0=00000028 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xdf://获得要开辟空间的大小 00007ff8`2a37880f8bd0movedx,eax 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xe1://开辟4f8的bitmapinfo空间 00007ff8`2a378811ff15d9190300callqwordptr[GDI32!_imp_LocalAlloc(00007ff8`2a3aa1f0)]ds:00007ff8`2a3aa1f0={KERNELBASE!LocalAlloc(00007ff8`2810fe40)} 0:027>redx edx=4f8 0:027>p GDI32!MRSETDIBITSTODEVICE::bPlay+0xe7: 00007ff8`2a378817488bf0movrsi,rax 0:027>rrax rax=0000003492a637b0//开辟出4f8的空间,并且获取堆指针 0:027>dd3492a637b0l5 00000034`92a637b000000000000000000000000000000000 00000034`92a637c000000000在函数中,会对开辟的空间进行一个判断,如果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`2a378830e8d34cfbffcallGDI32!memcpy(00007ff8`2a32d508) 0:027>rr8 r8=0000000000000028 0:027>rrdx rdx=0000002c8f00bed8 0:027>dd2c8f00bed8 0000002c`8f00bed800000028000000100000001000180001 0000002c`8f00bee800000000000000040000000000000000 0000002c`8f00bef8000000000000000000ed1c240000000e可以看到,拷贝的内容正是我们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,//thisisimportantpointer,指向要拷贝的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`cf88e104ff1596ee0300callqwordptr[gdi32full!_imp_StretchDIBits(00007ffd`cf8ccfa0)]ds:00007ffd`cf8ccfa0={GDI32!StretchDIBits(00007ffd`d1143370)} 0:026>dd29606a4a0dc+74 00000296`06a4a15000ed1c240000000e0000001400000000 00000296`06a4a160000000100000001418abea8b90018400而本来图像的大小是取决于EMRSETDIBITSTODEVICE结构体的cbBitsSrc,大小只有0x4,也就是这里只有00ed1c24,但是在之前的一系列分析过程中,发现整个过程都没有进行判断,从而直接拷贝了cxSrc*cySrc*4大小的内容,也就是超过cbBitsSrc的大小,造成了内存泄露。
下面我贴出这个函数的伪代码,相关注释已经写在伪代码中。
__int64__fastcallMRSETDIBITSTODEVICE::bPlay(MRSETDIBITSTODEVICE*this,void*a2,structtagHANDLETABLE*a3) { HDCv3;//r15@1 MRSETDIBITSTODEVICE*v4;//rbx@1 structtagHANDLETABLE*v5;//r14@1 unsignedintv6;//edi@1 __int64v7;//rax@1 __int64v8;//rbp@1 signedintv10;//eax@9 BITMAPINFO*v11;//rax@11 constBITMAPINFO*v12;//rsi@11 signedintv13;//eax@12 intv14;//eax@14 unsigned__int32v15;//er9@16 char*v16;//r8@19 structtagPOINTpt;//[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||!(unsignedint)MRSETDIBITSTODEVICE::bCheckRecord(v4,v5))//满足v6是EMF,且bCheckRecord会对EMRSETDIBITSTODEVICE结构体成员变量的大小作检查(仅仅是对结构体各自成员变量大小,而没有检查bitmap整体size) return0i64; if(MF::bClipped((MF*)v8,(MRSETDIBITSTODEVICE*)((char*)v4+8)))//这个函数会checkMF和ERECTL的上下左右值,是否在满足范围内 return1i64; pt.x=*((_DWORD*)v4+6);//对EMF的xDest和yDest进行传递 pt.y=*((_DWORD*)v4+7); if(!LPtoDP(*(HDC*)(v8+728),&pt,1)//将逻辑坐标转换成HDC的坐标 ||!SetWorldTransform(v3,(constXFORM*)(v8+704))//建立用于转换,输出图形的二维线形变换 ||!(unsignedint)MR::bValidOffExt(v4,v5,*((_DWORD*)v4+12),*((_DWORD*)v4+13)))//检查bitmap信息正确性 { return0i64; } v10=1272; if(*((_DWORD*)v4+13)>0x4F8u) v10=*((_DWORD*)v4+13); v11=(BITMAPINFO*)LocalAlloc(0x40u,(unsignedint)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为负数,则转换成正数(防止IntegerOverflow?) v14=-v14; v12->bmiHeader.biHeight=v14; v12->bmiHeader.biSizeImage=*((_DWORD*)v4+15);//设定bitmapinfoheader中biSizeImage值为cbBitsSrc v15=*((_DWORD*)v4+15);//cbBitSize交给v15 if(!v15||(unsignedint)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); returnv6; }JS Exploit与Web Safe Color
到此,我们分析了这个漏洞的成因,可能读到这里大家都会有一些和我当时一样的疑问,就是为什么我们要打印的内容是0x00ED1C24,也就是说,这里不管内存如何泄露,固定不变的值应该是0x00ED1C24,但是像诸如0patch文章中所说的,固定的值却是0xFF3333FF呢。
首先我们一起来把这个PoC修改成Exploit,用JS在网页中打印泄露的内存地址,在之前我们将cxSrc和cySrc修改回泄露内存的PoC,接下来,通过getImageData的方法,来获得图像的像素,之后将这个值打印。打印的Length实际上我输出多了,这里只需要1024(0x10*0x10*4)足以表示整个poc.emf图像。
可以看到,我们泄露了内存地址的信息,这些信息其实在本质上是包含着很多内容的。比如一些关键的内存地址信息等等。但是在测试的过程中,我没有发现有关浏览器的一些信息,比如cookie之类的,不知道是不是因为我的浏览器比较干净,内存驻留的信息较少。
但这里有几个问题,第一个就是我多次调试之后,发现内存泄露的信息位置很不稳定,也就是说,它可以用来泄露浏览器信息,但是用来做稳定的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的像素值,来看看在浏览器中图像强制转换打印的内容。
所以可以看到这个过程不可逆,因此,至少在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数组里。
可以看到,我们的DWORD color[]数组读取到了更多的内存信息,但其实在我们当前的进程空间里,这样的内存信息还是太少了,换句话说当前进程太单纯2333,我们可以正常打印PJ0提供的一个poc.emf,由于这个GetDC是Null,图像将会打印在左上角。
可以看到,除了左下角的0x00241ced,其他的都是0x00(#00000000表示黑色),也就是初始化的内存空间(这里0x00241ced也是红色),到此我们完成了对于这个漏洞的分析和利用,如有不当之处,还望大家多多包含,多多交流,谢谢!
最后我把exploit地址放在末尾:
https://github.com/k0keoyo/CVE-2017-0038-EXP-C-JS
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/3644.html