殊途同归的CVE-2012-0774 TrueType字体整数溢出漏洞分析

370 阅读17分钟

安全漏洞 的图像结果

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中显示:

12Executing 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文件,定位到出现问题的结构体中:

12345678910111213141516typedef 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表示的就是连续区间的个数,startCountendCount可以用于确定编码落在哪个区间范围内,针对上图,六个区间分别是[32, 34][77, 77][100, 101][114, 116][160, 160][-1, -1],其中最后一个区间不对应任何有效编码。

 

idDeltaidRangeOffset用于确认编码对应的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.2 确定数据来源

3.2.1 TrueType指令系统分析

既然是长度信息有误,一开始自然会想到要确定这个长度信息来自哪里,扩展一点说,这里的循环前移操作想要操作的是什么数据。

根据windbg的输出确定长度信息来自于地址6426622c,看一下这个地址前面的数据是什么(因为长度信息位于整个数据的末尾):

12340 : 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文档中关于指令的介绍,可以看到指令0x41NPUSHW操作,0x06表示入栈个数,说明要入栈6个WORD,同时将其扩展为DWORD,这也就是内存中三个0x00004141出现的原因,但是现在最关键的是要知道0x40000001出现的原因。

 

我最初根据TrueType文档中的指令介绍,对glyf表中最后一个SimpleGlyph中的指令进行了解释:

123456789101112131415161718192021222324252627282930313233/ /   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的那个语句:

1234567.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
1234567891011121314.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寄存器的值来确定每次执行的指令都是什么。

12345678Breakpoint  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指令处。

 

重复的指令序列如下:

12345678910ecx = 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文件格式也有了更深一步的认识。

参考文献

表情包 的图像结果

网络安全学习攻略