1. 前言
官方的漏洞通报中,关于这个漏洞的信息其实很少:
Integer overflow in Adobe Reader and Acrobat 9.x before 9.5.1 and 10.x before 10.1.3 allows attackers to execute arbitrary code via a crafted TrueType font.
只有几个关键点:构造的TTF文件,Adobe Reader版本,整数溢出漏洞。
因为这是一个很老的漏洞,网上能搜到的很多分析文章都是基于《漏洞战争》这本书完成的,并且其中的大多数只是在进行书中内容的复述。在阅读书中内容的过程中,作者提到使用TrueType Font Analyzer对ttf文件进行解析时出错,由此判断问题出现在glyf表中。虽然我找到了这个工具,但实在是太小众了,是一个日本博客中提供的,而用010editor对TTF文件进行解析的过程中没有得到什么有用的输出信息。所以漏洞分析的一开始,最困扰我的就是,如果没有《漏洞战争》这本书,我要如何确定异常数据的位置。
以下的分析内容有些做的其实是无用功,但是体现了针对该漏洞我的整个思考思路以及分析流程,因此全部保留下来。
2. 文件格式分析理解
2.1 利用010editor初步分析TTF文件结构
如果不看书,我能想到的就是用010editor打开TTF文件。
使用PdfStreamDumper将poc.pdf中的TTF文件提取出来(之前分析过一次Adobe Reader中的字体漏洞,所以知道该怎么做),命名为poc.ttf,用010editor打开。软件自动用Template进行解析,Output中显示:
1 2 | Executing template 'C:\Users\test\Documents\SweetScape\010 Templates\Repository\TTF.bt' on 'D:\Myfiles\vul_study\ldzz\2012-0774\poc.ttf' ...
* WARNING Line 158 : Variable 'glyphIdArray' not generated since array size is zero.
|
双击这个警告信息,会直接打开用于解析TTF文件的bt文件,定位到出现问题的结构体中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | typedef struct tcmap_format4 {
cmap_subtable = FTell();
USHORT format ; / / Format number is set to 4.
USHORT length; / / This is the length in bytes of the subtable.
USHORT language; / / Please see "Note on the language field in 'cmap' subtables" in this document.
USHORT segCountX2; / / 2 x segCount.
USHORT searchRange; / / 2 x ( 2 * * floor(log2(segCount)))
USHORT entrySelector; / / log2(searchRange / 2 )
USHORT rangeShift; / / 2 x segCount - searchRange
USHORT endCount[segCountX2 / 2 ]; / / End characterCode for each segment, last = 0xFFFF .
USHORT reservedPad; / / Set to 0.
USHORT startCount[segCountX2 / 2 ]; / / Start character code for each segment.
SHORT idDelta[segCountX2 / 2 ]; / / Delta for all character codes in segment.
USHORT idRangeOffset[segCountX2 / 2 ]; / / Offsets into glyphIdArray or 0
USHORT glyphIdArray[(length - (FTell() - cmap_subtable)) / 2 ]; / / Glyph index array (arbitrary length) !!!就是这里出现了问题!!!
};
|
那么为什么会出现这个警告信息呢?
搜索tcmap_format4
字段会定位到tcmap
结构体,也就是TTF文件中的cmap
表。在010editor中找到cmap
表,其中包含了两个子表,第二个子表中就包含了出现问题的tcmap_format4
,点开之后可以发现它的length
字段是64,如果你选中整个tcmap_format4
结构,会发现它的长度也是64,所以计算(length-(FTell()-cmap_subtable))/2
得到的值是0。因此出现了警告信息。
不过我也不知道警告信息有什么用,但是既然这里出现了警告,那么至少说明这个文件中的结构是有一些问题的,再加上template的解析结果其实比较乱,我将结果导出到文档中,并进行了整理:
根据之前的漏洞分析经验,已经知道TTF文件中都是由一个个表组成的,这里汇总的就是不同表的位置以及大小数据,其中用(head)
标注的数据指的是文件开头的Table Directory中记录的各个表的偏移及大小,而没有使用(head)
标注的数据则是template整理出来的实际的位置和大小。
注意到Table Directory中记录的表的大小信息有3处与实际不符,但是由于对表的具体功能不了解,所以还要继续查资料。
2.2 通过文档详细了解TTF文件格式
通过TTF中template的输出结果,已经对poc.ttf文件有了一个初步的了解,但是由于对于每个表的具体功能并不了解,因此仍旧是一头雾水,所以接下来开始直接阅读文档。
注:以下内容之所以会注意到那么多细节的内容是因为后面调试阶段遇到了相关问题,所以又回过头来补充的。所以可以先看下面的调试,再回过头来看这里的文件格式分析。
2.2.1 name表
name表中包含的是一些关于字体的可读信息,可以被其他表引用,从而向用户提供有用的信息。它的结构是这样的:
注意到其中的char name[35]
了吗,它的Start
数据是0xFBE
,这里其实就是上面统计的数据中,name表Size
中未包含的部分。准确的说010editor的template并没有把这部分数据包含在name表的Size
中,因此出现了和Table Directory中不符的情况,但是实际上没有任何问题。
2.2.2 cmap表
所谓cmap,其实就是character mapping的缩写,它用来将字符编码映射成实际的字形。由于存在多种平台环境,多种编码形式,因此就对应了多种编码表,因此cmap表中也就可能包含多个子表,每个子表对应一个编码形式。在实际使用的时候会根据情况选择使用哪个子表。
根据文档中的描述,对poc.ttf文件中的cmap进行解释:
其中没有展开的两个tcmap_format
结构就是具体的映射表了,注意它们的Start
信息,会发现这两个映射表其实就占据了上面tamplate总结的Size信息未包含的那部分。因此虽然和Table Directory中的记录不符,但是也没有问题。
不过在2.1小节中,我们提到了Variable 'glyphIdArray' not generated
的警告信息,这个警告信息就是tcmap_format4
中产生的,因此再具体的看一下tcmap_format4
结构:
format 4格式针对的是2字节编码格式,当字体编码位于多个连续区间之内的时候使用这种格式。上图中的segCount
表示的就是连续区间的个数,startCount
和endCount
可以用于确定编码落在哪个区间范围内,针对上图,六个区间分别是[32, 34]
、[77, 77]
、[100, 101]
、[114, 116]
、[160, 160]
、[-1, -1]
,其中最后一个区间不对应任何有效编码。
idDelta
和idRangeOffset
用于确认编码对应的glyph索引值,针对上图,由于idRangeOffset
为0,因此索引值的计算方法为:glyphIndex = idDelta[i] + c
。
索引值最后用于在glyphIndexArray
中索引,但是在此例中缺少了glyphIndexArray
。
看到现在,还是不确定glyphIndexArray
这个结构怎么对应到实际的字形上,先看下一个表。
2.2.3 maxp表
maxp表中的数据说明了字体的内存需求,这里只关注一个数值:numGlyphs
,保存了glyph的个数。在此例中,这个数值是271。
2.2.4 loca表
loca表中保存了字形数据相对于glyf表起始部位的偏移位置,这个表主要是为了对字形数据能够快速索引。里面就是一个USHORT的数组,一共由numGlyphs+1
项(还包括一个表示字符不存在的字形)。
在此例中,数组中有多项是重复的,因为下面分析glyf表时要用到,所以这里做一个整理:
2.2.5 glyf表
glyf表中保存了定义字体字形的数据信息,其中既包括定义字形轮廓的点信息,也包括填充字形的指令信息:
在检查这个表的时候,没有发现和name以及cmap表类似的数据索引的情况,因此需要搞清楚为什么glyf表后面会空余出一大块数据。
这里就要回头看一下loca表中的数据了,如果你将loca表中保存的偏移量*2,再加上glyf表的起始位置0x600
,就会得到各个SimpleGlyph
的Start值了。
注:关于为什么要2,head表中定义了一个indexToLocFormat
数值,如果该值为0
,代表short,单位就是2个字节,所以要2。
注意到loca表中重复的数据所对应的SimpleGlyph的Start值也是相同的,虽然它们在template的结果中表示成了不同的项。
但是template的结果中只显示到偏移为0x156
的字形数据,之后偏移的字形数据没有解析出来。
现在我们把后面的数据复制出来,然后手工按照SimpleGlyph的结果进行简单的解析:
后面的compressedFlags和contours有点复杂所以我没有进一步处理。
3. 漏洞调试
操作系统: Win7 sp1 32位
Adobe Reader 9.4 英文版
3.1 确定异常成因
打开poc.pdf文件之后,由于发生异常,windbg自动打开,程序中断:
1 2 3 4 5 6 7 | ( 158.f9c ): Access violation - code c0000005 (!!! second chance !!!)
eax = 632c622c ebx = 00000214 ecx = 632d0000 edx = 3fffd88a esi = 632d0004 edi = 00004141
eip = 630979ce esp = 0030cdf0 ebp = 0030ce84 iopl = 0 nv up ei pl nz na po nc
cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00010202
* * * ERROR: Symbol file could not be found. Defaulted to export symbols for C:\Program Files\Adobe\Reader 9.0 \Reader\CoolType.dll -
CoolType + 0x79ce :
630979ce 8919 mov dword ptr [ecx],ebx ds: 0023 : 632d0000 = 00001000
|
看一下这个地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 0 : 000 > !address 632d0000
Failed to map Heaps (error 80004005 )
Usage: Image
Allocation Base: 63090000
Base Address: 632d0000
End Address: 632ef000
Region Size: 0001f000
Type : 01000000 MEM_IMAGE
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
More info: lmv m CoolType
More info: !lmi CoolType
More info: ln 0x632d0000
|
发现这是一个具有只读权限的地址,而现在程序在尝试写入,因此出现异常。
由于CoolType.dll每次加载的基地址都不一样,所以为了方便在IDA中定位,直接将CoolType.dll在IDA中的基地址修改成0,然后根据偏移定位到发生漏洞的位置,并将其函数命名为vulFunc
。
先看一下IDA中的伪代码分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | int __cdecl vulFunc( int a1) {
if ( (a - 4 ) < * b
|| (high = * (b + 0x154 ), a - 4 > = high)
|| (end = (a - 4 ), len = * (a - 4 ), start = (a - 4 - 4 * len ), start < * b)
|| start > = high ) {
result = dword_232438;
dword_232434 = 0x1110 ;
} else {
v5 = * start;
if ( len > 0 ) {
do {
- - len ;
* start = start[ 1 ]; / / 异常发生位置
+ + start;
}
while ( len );
- - end;
}
* end = v5;
a = (end + 1 );
result = a1;
}
return result;
}
|
我对变量名进行了一些修改以使过程更加清晰,整个函数是在对一个范围的数据进行循环前移操作。a-4是范围的终点,终点位置保存了整个范围的长度。函数一开始对整个范围的地址进行了一个上下界的判断,符合要求后才会进行下一步循环前移操作。
确定了函数功能之后,重新加载调试,在vulFunc
的起始位置设置断点,然后单步调试,跟踪到计算start的语句start = (a - 4 - 4 * len)
的时候发现了问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 0 : 000 > p
eax = 6426622c ebx = 00000000 ecx = 03ec2e04 edx = 6426622c esi = 64266220 edi = 64266344
eip = 640379af esp = 0031cd90 ebp = 0031ce24 iopl = 0 nv up ei pl nz na po cy
cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000203
CoolType + 0x79af :
640379af 8b10 mov edx,dword ptr [eax] ds: 0023 : 6426622c = 40000001 / / 长度是 40000001
0 : 000 > p
eax = 6426622c ebx = 00000000 ecx = 03ec2e04 edx = 40000001 esi = 64266220 edi = 64266344
eip = 640379b1 esp = 0031cd90 ebp = 0031ce24 iopl = 0 nv up ei pl nz na po cy
cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000203
CoolType + 0x79b1 :
640379b1 8bda mov ebx,edx
0 : 000 > p
eax = 6426622c ebx = 40000001 ecx = 03ec2e04 edx = 40000001 esi = 64266220 edi = 64266344
eip = 640379b3 esp = 0031cd90 ebp = 0031ce24 iopl = 0 nv up ei pl nz na po cy
cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000203
CoolType + 0x79b3 :
640379b3 c1e302 shl ebx, 2 / / 长度 * 4
0 : 000 > p
eax = 6426622c ebx = 00000004 ecx = 03ec2e04 edx = 40000001 esi = 64266220 edi = 64266344
eip = 640379b6 esp = 0031cd90 ebp = 0031ce24 iopl = 0 ov up ei pl nz na po cy
cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000a03
CoolType + 0x79b6 :
640379b6 8bc8 mov ecx,eax
|
注意一开始读取到的长度是0x40000001
,执行完*4操作之后的ebx的值是00000004
,这里发生了溢出。画成图来看比较清晰:
由于范围判断的不严谨,程序没有发现发生了整数溢出,导致了异常的发生。
3.2 确定数据来源
3.2.1 TrueType指令系统分析
既然是长度信息有误,一开始自然会想到要确定这个长度信息来自哪里,扩展一点说,这里的循环前移操作想要操作的是什么数据。
根据windbg的输出确定长度信息来自于地址6426622c
,看一下这个地址前面的数据是什么(因为长度信息位于整个数据的末尾):
1 2 3 4 | 0 : 000 > dd 64266200 lc
64266200 00000000 00000000 00000000 00000000
64266210 00000000 00000000 00000000 00000000
64266220 00004141 00004141 00004141 40000001
|
其中的4141
吸引了我的注意力,这样的数据不太自然,有很大的可能性是人为设置的。目前已知是TTF文件有问题,所以到TTF文件中搜索一下0x4141
出现的位置:
只有这一个位置出现了连续的6个0x41
。
如果和2.2.5小节最后对于数据的解析结果来看,这部分数据位于glyf表中最后一个SimpleGlyph的指令部分:
如果查找TrueType文档中关于指令的介绍,可以看到指令0x41
是NPUSHW
操作,0x06
表示入栈个数,说明要入栈6个WORD,同时将其扩展为DWORD,这也就是内存中三个0x00004141
出现的原因,但是现在最关键的是要知道0x40000001
出现的原因。
我最初根据TrueType文档中的指令介绍,对glyf表中最后一个SimpleGlyph中的指令进行了解释:
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 | / / NPUSHW操作,入栈 6 个WORD,同时扩展为DWORD
41 , 06 , 41 41 , 41 41 , 41 41 , 00 03 , 00 00 , 00 40
/ / Write Store操作,弹出两个DWORD
42
/ / NPUSHW操作,入栈 2 个WORD,同时扩展为DWORD
41 , 02 , 7F FF, 7F FF
/ / MULtiply操作,弹出两个DWORD,入栈乘法结果
63
/ / ADD操作,弹出两个DWORD,入栈加法结果
60
/ / NPUSHW操作,入栈 4 个WORD,同时扩展为DWORD
41 , 04 , FF E8, 00 00 , 00 00 , 00 00
/ / Read Store操作,弹出一个读取位置DWORD,入栈一个读取结果DWORD
43
/ / PUSHB操作,入栈 1 个BYTE,同时扩展为DWORD
B0, 01
/ / SUBtract操作,弹出 2 两个DWORD,入栈减法结果
61
/ / Write Store操作,弹出两个DWORD
42
/ / Read Store操作,弹出一个读取位置DWORD,入栈一个读取结果DWORD
43
/ / Jump Relative On True 操作,弹出两个DWORD,并根据第一个DWORD决定指令要不要跳转
78
/ / NPUSHW操作,入栈 2 个WORD,同时扩展为DWORD
41 , 02 , 7F FF, 7F FF
/ / ADD操作,弹出两个DWORD,入栈加法结果
60
/ / ADD操作,弹出两个DWORD,入栈加法结果
60
/ / Move the INDEXed element to the top of the stack 从栈顶弹出一个元素k,循环移动栈中接下来的k个元素
26
/ / 注意这里就是vulFunc在执行的操作,所以不再向下分析
|
然后画出如下图的栈中数据变化情况,结果发现不太对劲:
执行到MINDEX
这个指令的时候就是在做vulFunc
中的循环移位操作,但是得到的长度并不是0x40000001
,如上图中所示,应该是执行到JROT
指令的时候做了跳转,EIP向前跳转24个字节,现在不知道24个字节对应于多少指令,再手工分析就有点丧心病狂了。
鉴于现在对于TrueType文件结构以及其中的指令系统有了更加深入的了解,我决定回到IDA和Windbg,通过动态调试的方法最终确定0x40000001
的数据来源。
3.2.2 代码分析及动态调试
还是回到IDA中vulFunc
的位置,在IDA中发现了两个交叉引用,分别位于偏移690E
和偏移6C605
,重新打开Adobe Reader,在这两个偏移位置下断点,然后加载POC文件,程序中断在了690E
的位置,说明异常发生在调用690E
之后,在IDA中看一下调用到了vulFunc
的那个语句:
1 2 3 4 5 6 7 | .text: 00006955
.text: 00006955 loc_6955:
.text: 00006955 51 push ecx
.text: 00006956 50 push eax
.text: 00006957 FF 14 8D D0 BE 21 00 call funcs_6409C64D[ecx * 4 ]
.text: 0000695E 59 pop ecx
.text: 0000695F 59 pop ecx
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | .data: 0021BED0 6A 6C 00 00 C5 6C 00 00 20 6D + funcs_6409C64D dd offset sub_6C6A, offset sub_6CC5, offset sub_6D20, offset sub_6D6D
.data: 0021BED0 00 00 6D 6D 00 00 BA 6D 00 00 + ; DATA XREF: sub_690E + 49 ↑r
.data: 0021BED0 F3 6D 00 00 2C 6E 00 00 2C 6E + ; sub_6C605 + 48 ↑r
.data: 0021BED0 00 00 6B 70 00 00 6B 70 00 00 + dd offset sub_6DBA, offset sub_6DF3, offset sub_6E2C, offset sub_6E2C
.data: 0021BED0 52 71 00 00 5F C6 06 00 CA 71 + dd offset sub_706B, offset sub_706B, offset sub_7152, offset sub_6C65F
.data: 0021BED0 00 00 1F 72 00 00 74 72 00 00 + dd offset sub_71CA, offset sub_721F, offset sub_7274, offset sub_729E
.data: 0021BED0 9E 72 00 00 95 75 00 00 D5 75 + dd offset sub_7595, offset sub_75D5, offset sub_7615, offset sub_76CC
.data: 0021BED0 00 00 15 76 00 00 CC 76 00 00 + dd offset sub_76CC, offset sub_76CC, offset sub_76CC, offset sub_7655
.data: 0021BED0 CC 76 00 00 CC 76 00 00 CC 76 + dd offset sub_7756, offset sub_7767, offset sub_751B, offset sub_995C
.data: 0021BED0 00 00 55 76 00 00 56 77 00 00 + dd offset sub_999F, offset sub_7558, offset sub_6C6C8
.data: 0021BED0 67 77 00 00 1B 75 00 00 5C 99 + dd offset sub_6C70D, offset sub_78A2, offset sub_7696
.data: 0021BED0 00 00 9F 99 00 00 58 75 00 00 + dd offset sub_6C815, offset sub_78FB, offset sub_6C826
.data: 0021BED0 C8 C6 06 00 0D C7 06 00 A2 78 + dd offset sub_7939, offset vulFunc, offset sub_6CBDA, offset sub_6C7A0
...
|
发现这里在通过ecx寄存器索引一个函数数组。
根据上面对指令系统的分析,已经知道vulFunc
是在执行MINDEX
指令,那么很自然的会想到这些函数对应于TrueType中的不同指令。vulFunc
在整个数组的偏移38
的位置,对应于十六进制就是0x26
,就是MINDEX
的指令码,b( ̄▽ ̄)d。
所以程序应该就是在偏移690E
的函数中调用不同的函数来处理不同的指令,我完全可以在.text:00006957 call funcs_6409C64D[ecx*4]
这里设置一个断点,然后通过查看ecx寄存器的值来确定每次执行的指令都是什么。
1 2 3 4 5 6 7 8 | Breakpoint 0 hit
eax = 00000000 ebx = 00000000 ecx = 00000000 edx = 00000000 esi = 03ec2e04 edi = 03ec2c54
eip = 6403690e esp = 0031cf08 ebp = 0031cf80 iopl = 0 nv up ei pl zr na pe nc
cs = 001b ss = 0023 ds = 0023 es = 0023 fs = 003b gs = 0000 efl = 00000246
CoolType + 0x690e :
6403690e 8b442404 mov eax,dword ptr [esp + 4 ] ss: 0023 : 0031cf0c = 03ec2f6c
0 : 000 > bc *
0 : 000 > bp CoolType + 0x6957 "r ecx;g"
|
最后得到了657条输出结果……但是不要着急,如果仔细检查,会发现其中0x78
指令起了很大的作用,一共出现了64次,也就是进行了63次指令跳转,直到最后一次没有跳转,继续往下执行,才到达了0x26
指令处。
重复的指令序列如下:
1 2 3 4 5 6 7 8 9 10 | ecx = 00000078
ecx = 00000041
ecx = 00000063
ecx = 00000060
ecx = 00000041
ecx = 00000043
ecx = 000000b0
ecx = 00000061
ecx = 00000042
ecx = 00000043
|
如果和上面3.2.1中的图相对应,就会发现程序已知在循环执行这部分指令:
相当于已知在0x00000003
的上面递增0x00FFFC00
,计算一下0x00000003 + 0x00FFFC00 * 0x40 = 0x3FFF0003
。
最后一次的读取结果是0,所以不再进行跳转,而是继续往下执行:
这次得到的长度结果正好就是之前调试看到的数值0x40000001
。
4. 总结
在此次的漏洞分析过程中,由于无法说服自己接受“通过TrueType Font Analyzer对于TTF文件的解析结果确定漏洞位于glyf表”中这一因果关系(因为这个工具过于小众,且信息太少),因此我完全放弃根据书中的步骤对漏洞进行分析,转而去查看TrueType的文档。在本文中花费了大量篇幅对TTF文件格式进行了介绍,正是通过对文件格式的理解,我确定了问题处在glyf表中,并进一步确定了问题数据0x40000001
的来源。
在我完成漏洞分析转而看书中的介绍时,发现两者殊途同归,最后竟然都对poc文件中的指令进行了分析,只不过我是从文件格式手工解析出发,转而通过调试验证,而书中是先通过调试确定了指令执行顺序,进而解析文件中的部分指令。
从我个人的角度来说,通过对文件格式的理解进而进行漏洞分析,整个逻辑过程会比较通顺,也易于理解。经过了此次漏洞分析,对于TTF文件格式也有了更深一步的认识。
5. 参考资料
- TrueType Reference Manual(关于各个表以及指令的介绍都在这里)
- 《漏洞战争》
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
最后于 2021-9-20 16:02
被LarryS编辑
,原因: 修改标题