正在做的项目需要复现Windows栈溢出的漏洞,CVE-2010-2883是Adobe Reader的CoolType.dll库在解析字体文件SING表中的uniqueName项时产生的漏洞。
调试环境和工具
1 | Adobe Reader:9.3.4 |
PDF文件结构
PDF的基本结构由以下四部分组成:
1 | ----------- |
其中Header是PDF文件的第一行,指明了使用的PDF规范的版本号,可以使用十六进制编辑器查看一个PDF文件的版本号。比如打开一个PDF可以看到它的版本号为1.7,“%”是PDF格式中的注释。
Body用来保存向向户显示的所有文档数据。通常包含文本流、图像及其他多媒体元素。
xref Table是交叉引用表, 包含对文档中所有文档的引用,交叉引用表的存在使得可以随机访问文件中的对象,而不必读取整个PDF文件来定位特定对象。引用表中的每个条目表示一个对象。每个条目的长度为20个字节,说明了对象在文件中的偏移、对象的使用状态等。
Trailer指定读取PDF文件的应用程序应该如何找到交叉引用表和其他特殊对象。在读取PDF文件时,所有的PDF Readers都应该从文件末尾来解析文件。PDF文档的最后一行是以%EOF.作为结尾,在该字符串前面有一个startxref的字符串指明了交叉引用表在文件中的偏移,如图这个文件中交叉引用表的偏移是331814=0x51026字节。
TTF文件格式
因为这个漏洞是TTF字体文件SING表时导致的栈溢出漏洞。TrueType Font(TTF)是苹果和微软在20世纪八十年代后期开发的轮廓字体标准,目前已成为操作系统上最常用的字体格式。
后续补充TTF的组成及包含。
使用PdfStreamDumper导出PDF样本中的TTF文件,这里使用的PDF样本是《漏洞战争》里使得Adobe Reader崩溃的msf.pdf样本。TTF中关于SING表的TableEntry结构数据如下图所示,选中Object,右键选择“Save Decompressed Stream”导出TTF文件。
每一个表都有一个TableEntry的结构,其中SING(Smart INdependent Glyphlets)表的TableEntry结构如下:
1 | typedef struct_SING{ |
SING表的数据结构如下:
1 | typedef struct |
通过上图我们得知TableEntry中的offset为0x11c,从文件起始偏移为0x11c处是SING表的真实数据,然后偏移0x10处就是uniqueName域:
溢出点
使用IDA打开CoolType.dll,搜索字符串“SING”:
1 | .rdata:0819DB4C aSing db 'SING',0 ; DATA XREF: sub_8015AD9+D2↑o |
交叉引用定位到该库对SING表格的解析方式:
1 | .text:0803DCF9 ; __unwind { // loc_8184A54 |
由上面分析可知,是因为Adobe Reader在调用strcat时,没有对uniqueName域的长度进行检查,直接将其复制到固定大小的栈空间,从而导致栈溢出。
漏洞复现
运行AcroRd32.exe,OD附加,首先在0x0803dd74上下断点,但是在调试环境中程序加载的基址不是0x08001000,因此可以根据OD中列出的所有模块找到CoolType.dll,然后转到该模块:
该模块的加载的基址为0x6af60000,根据相对偏移计算需要下断点的地址为0x0803dd74 - 0x08001000 + 0x6af60000 = 0x6AF9DD74。
程序执行完0x0803dd82后,eax指向SING表的入口,根据eax的值我们可以得到SING表入口地址为0X0765C454。
在内存窗口查看SING表的内容,与我们在PdfStreamDumper里看到的内容是一致的,偏移0x10处事uniqueName域。
执行完0x0803DD8D后,ecx为SING表的前4个字节,这里是0x00010000。
执行完strcat后,会将“98 66 51 E6”起始的部分复制到ebp所指向的地址处0x0025E13C,直至遇到‘\x00’结束:
因为我这里调试的只是让程序崩溃的样本msf.pdf,继续跟进可以看到程序崩溃的过程,因为有地址随机化,下面栈的地址可能与上面不一样,但是程序指令地址是一样的,可以对应上,因为实在不想再截一遍图了。
崩溃过程
1.在0x6AF9DEAF中崩溃,call 0x6AF76BDE,单步步入
2.在0x6AF76C56中崩溃,call 0x6AF7BB21,单步步入
3.在0x6AF7BB41中崩溃,call dword ptr ds:[eax],单步步入 -> 0x6AFEB116
4.在0x6AFE6B2D中崩溃,call dword ptr ds:[eax+0x70]单步步入-> 0x6AFE9937
5.在0x6AFEB308处,call dword ptr ds:[eax] -> icucnv36.4A80CB38
程序跳转到icucnv36.4A80CB38,在0x4A80CB38处的指令如下图所示,其中对add ebp,0x794之后,ebp我这里是变成了0x00EFDDC0,前面将uniqueName里的内容拷贝到了ebp中,这里对ebp增加之后ebp指向的正好是uniqueName的第5个字节:
查看栈里的内容可以看到ebp的内容,可以看到ebp-4的内容是uniqueName的前4个字节0xE6516698,ebp是0xE78B53AB。
0x4A80CB3E处的leave指令分解为如下指令:
1 | mov esp,ebp |
栈变成了下图的样子,程序执行retn指令返回到0x4A82A714,也就是uniqueName的第九字节处的内容,这个地址也位于icucnv36.dll的空间,然后栈顶就变为了uniqueName第13个字节0x0c0c0c0c。
继续跟进,程序运行到0x4A82A714之后,OD里显示的0x4A82A712为起始地址的指令call dword ptr ds:[eax+0x5c],可以把前两个字节nop掉,就看到0x4A82A714的指令其实是pop esp,因此esp变为了0x0c0c0c0c。
栈顶为0x0c0c0c0c,0x0c0c0c0c里的内容也是0x0c0c0c0c:
然后执行retn指令,程序跳转到0x0c0c0c0c处执行,0x0c0c0c0c处的内容仍然为0x0c0c0c0c,之后程序退出。
windows异常处理机制
- 当程序发生异常时,CPU捕获异常并进行内核态的异常处理。
- 内核处理结束后将控制权交给ring3用户态,用户态异常处理的第一个函数是ntdll.dll的KiUserExceptionDispatcher函数。
- 首先会调用程序是否在调试状态,如果程序正在被调试,会将异常处理交给调试器。
- 如果程序处于未调试状态,该函数将调用RtlDispatchException遍历线程的SEH链,SEH链的起始地址在TEB线程环境块的0字节偏移处。
- 如果线程的SEH无法处理该异常,会检查用户是否通过SetUnhandledExceptionFilter函数注册过进程的异常处理函数。
- 如果用户曾经注册过,则调用该异常处理函数;如果用户自己注册的异常处理函数未能处理异常,或用户从未注册过自己的异常处理函数,则会交给系统默认的异常处理UnhandledExceptionFilter(),根据注册表键值决定是直接关闭程序还是弹出程序崩溃的对话框。
在调试中可以看到,当发生异常时,程序首先调用了ntdll.dll的KiUserExceptionDispatcher函数。
利用过程
参考
《漏洞战争》
https://resources.infosecinstitute.com/pdf-file-format-basic-structure/
https://firmianay.github.io/2018/04/13/adobe_reader_2010_2883.html