正在做的项目需要复现Windows栈溢出的漏洞,CVE-2010-2883是Adobe Reader的CoolType.dll库在解析字体文件SING表中的uniqueName项时产生的漏洞。
调试环境和工具
1 | Adobe Reader:9.3.4 |
PDF文件结构
PDF的基本结构由以下四部分组成:1
2
3
4
5
6
7
8
9-----------
Header
-----------
Body
-----------
xref Table
-----------
Trailer
-----------
其中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
2
3
4
5
6typedef struct_SING{
char tag[4]; //标记
ULONG checkSum; //校验和
ULONG offset; //相对文件的偏移
ULONG length; //数据长度
} TableEntry;
SING表的数据结构如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15typedef struct
{
USHORT tableVersionMajor;
USHORT tableVersionMinor;
USHORT glyphletVersion;
USHORT embeddinginfo;
USHORT mainGID;
USHORT unitsPerEm;
SHORT vertAdvance;
SHORT vertOrigin;
BYTE[28] uniqueName; //长度为28字节
BYTE[16] METAMD5;
BYTE nameLength;
BYTE[] baseGlyphName;
} SINGTable;
通过上图我们得知TableEntry中的offset为0x11c,从文件起始偏移为0x11c处是SING表的真实数据,然后偏移0x10处就是uniqueName域:
溢出点
使用IDA打开CoolType.dll,搜索字符串“SING”:1
2.rdata:0819DB4C aSing db 'SING',0 ; DATA XREF: sub_8015AD9+D2↑o
.rdata:0819DB4C ; sub_803DCF9+7B↑o ...
交叉引用定位到该库对SING表格的解析方式: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
61
62
63
64
65.text:0803DCF9 ; __unwind { // loc_8184A54
.text:0803DCF9 push ebp
.text:0803DCFA sub esp, 104h ; 分配栈空间0x104
.text:0803DD00 lea ebp, [esp-4] ; 将地址为esp-4中的值赋值给ebp
.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] ; edi为arg0
.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 ; esi为0,后面判断是否为空会用到
.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 ; 值为arg0
.text:0803DD7A lea ecx, [ebp+108h+var_12C] ; 指向SING表的入口
.text:0803DD7D call sub_8021B06 ; 处理SING表,sub_8021B06(a1,"SING")
.text:0803DD82 mov eax, [ebp+108h+var_12C] ; eax指向SING表入口
.text:0803DD85 cmp eax, esi ; 判断SING表是否为空
.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] ; ecx赋值为SING表的前四个字节,这里是00 01 00 00,是字体资源版本号
.text:0803DD8F and ecx, 0FFFFh ; 计算结果为0x0
.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: sub_803DCF9+9C↑j
.text:0803DD9F add eax, 10h ; 相对于SING表偏移0x10处,uniqueName域
.text:0803DDA2 push eax ; char* 源地址,uniqueName域
.text:0803DDA3 lea eax, [ebp+108h+var_108]
.text:0803DDA6 push eax ; char *,目的地址,一段固定大小的栈空间
.text:0803DDA7 mov [ebp+108h+var_108], 0 ; 目的地址置空,strcat字符串连接相当于复制
.text:0803DDAB call strcat ; 这里导致溢出
.text:0803DDB0 pop ecx
.text:0803DDB1 pop ecx
.text:0803DDB2 lea eax, [ebp+108h+var_108]
.text:0803DDB5 push eax
.text:0803DDB6 mov ecx, ebx
.text:0803DDB8 call sub_8001243
.text:0803DDBD mov eax, [ebp+108h+var_12C]
由上面分析可知,是因为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
2mov esp,ebp
pop 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