殊途同歸的CVE-2012-0774 TrueType字體整數溢出漏洞分析
介紹
官方的漏洞通報中,關于這個漏洞的信息其實很少:
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.1 利用010editor初步分析TTF文件結構
如果不看書,我能想到的就是用010editor打開TTF文件。
使用PdfStreamDumper將poc.pdf中的TTF文件提取出來(之前分析過一次Adobe Reader中的字體漏洞,所以知道該怎么做),命名為poc.ttf,用010editor打開。軟件自動用Template進行解析,Output中顯示:
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文件,定位到出現問題的結構體中:
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的數組,一共由numGlyhs+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有點復雜所以我沒有進一步處理。
漏洞調試
操作系統:Win7 sp1 32位
Adobe Reader 9.4 英文版
3.1 確定異常成因
打開poc.pdf文件之后,由于發生異常,windbg自動打開,程序中斷:
(158.f9c): Access violation - code c0000005 (!!! second chance !!!)eax=632c622c ebx=00000214 ecx=632d0000 edx=3fffd88a esi=632d0004 edi=00004141eip=630979ce esp=0030cdf0 ebp=0030ce84 iopl=0 nv up ei pl nz na po nccs=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
看一下這個地址:
0:000> !address 632d0000 Failed to map Heaps (error 80004005)Usage: ImageAllocation Base: 63090000Base Address: 632d0000End Address: 632ef000Region Size: 0001f000Type: 01000000 MEM_IMAGEState: 00001000 MEM_COMMITProtect: 00000002 PAGE_READONLYMore info: lmv m CoolTypeMore info: !lmi CoolTypeMore info: ln 0x632d0000
發現這是一個具有只讀權限的地址,而現在程序在嘗試寫入,因此出現異常。
由于CoolType.dll每次加載的基地址都不一樣,所以為了方便在IDA中定位,直接將CoolType.dll在IDA中的基地址修改成0,然后根據偏移定位到發生漏洞的位置,并將其函數命名為vulFunc。
先看一下IDA中的偽代碼分析:
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)的時候發現了問題:
0:000> peax=6426622c ebx=00000000 ecx=03ec2e04 edx=6426622c esi=64266220 edi=64266344eip=640379af esp=0031cd90 ebp=0031ce24 iopl=0 nv up ei pl nz na po cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000203CoolType+0x79af:640379af 8b10 mov edx,dword ptr [eax] ds:0023:6426622c=40000001 // 長度是400000010:000> peax=6426622c ebx=00000000 ecx=03ec2e04 edx=40000001 esi=64266220 edi=64266344eip=640379b1 esp=0031cd90 ebp=0031ce24 iopl=0 nv up ei pl nz na po cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000203CoolType+0x79b1:640379b1 8bda mov ebx,edx0:000> peax=6426622c ebx=40000001 ecx=03ec2e04 edx=40000001 esi=64266220 edi=64266344eip=640379b3 esp=0031cd90 ebp=0031ce24 iopl=0 nv up ei pl nz na po cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000203CoolType+0x79b3:640379b3 c1e302 shl ebx,2 // 長度*40:000> peax=6426622c ebx=00000004 ecx=03ec2e04 edx=40000001 esi=64266220 edi=64266344eip=640379b6 esp=0031cd90 ebp=0031ce24 iopl=0 ov up ei pl nz na po cycs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000a03CoolType+0x79b6:640379b6 8bc8 mov ecx,eax
注意一開始讀取到的長度是0x40000001,執行完*4操作之后的ebx的值是00000004,這里發生了溢出。畫成圖來看比較清晰:

由于范圍判斷的不嚴謹,程序沒有發現發生了整數溢出,導致了異常的發生。
3.2 確定數據來源
3.2.1 TrueType指令系統分析
既然是長度信息有誤,一開始自然會想到要確定這個長度信息來自哪里,擴展一點說,這里的循環前移操作想要操作的是什么數據。
根據windbg的輸出確定長度信息來自于地址6426622c,看一下這個地址前面的數據是什么(因為長度信息位于整個數據的末尾):
0:000> dd 64266200 lc64266200 00000000 00000000 00000000 0000000064266210 00000000 00000000 00000000 0000000064266220 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中的指令進行了解釋:
// NPUSHW操作,入棧6個WORD,同時擴展為DWORD41, 06, 41 41, 41 41, 41 41, 00 03, 00 00, 00 40// Write Store操作,彈出兩個DWORD42 // NPUSHW操作,入棧2個WORD,同時擴展為DWORD41, 02, 7F FF, 7F FF // MULtiply操作,彈出兩個DWORD,入棧乘法結果63// ADD操作,彈出兩個DWORD,入棧加法結果60 // NPUSHW操作,入棧4個WORD,同時擴展為DWORD41, 04, FF E8, 00 00, 00 00, 00 00 // Read Store操作,彈出一個讀取位置DWORD,入棧一個讀取結果DWORD43 // PUSHB操作,入棧1個BYTE,同時擴展為DWORDB0, 01 // SUBtract操作,彈出2兩個DWORD,入棧減法結果61// Write Store操作,彈出兩個DWORD42 // Read Store操作,彈出一個讀取位置DWORD,入棧一個讀取結果DWORD43// Jump Relative On True操作,彈出兩個DWORD,并根據第一個DWORD決定指令要不要跳轉78// NPUSHW操作,入棧2個WORD,同時擴展為DWORD41, 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的那個語句:
.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 .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寄存器的值來確定每次執行的指令都是什么。
Breakpoint 0 hiteax=00000000 ebx=00000000 ecx=00000000 edx=00000000 esi=03ec2e04 edi=03ec2c54eip=6403690e esp=0031cf08 ebp=0031cf80 iopl=0 nv up ei pl zr na pe nccs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246CoolType+0x690e:6403690e 8b442404 mov eax,dword ptr [esp+4] ss:0023:0031cf0c=03ec2f6c0:000> bc *0:000> bp CoolType+0x6957 "r ecx;g"
最后得到了657條輸出結果……但是不要著急,如果仔細檢查,會發現其中0x78指令起了很大的作用,一共出現了64次,也就是進行了63次指令跳轉,直到最后一次沒有跳轉,繼續往下執行,才到達了0x26指令處。
重復的指令序列如下:
ecx=00000078ecx=00000041ecx=00000063ecx=00000060ecx=00000041ecx=00000043ecx=000000b0ecx=00000061ecx=00000042ecx=00000043
如果和上面3.2.1中的圖相對應,就會發現程序已知在循環執行這部分指令:

相當于已知在0x00000003的上面遞增0x00FFFC00,計算一下0x00000003 + 0x00FFFC00 * 0x40 = 0x3FFF0003。
最后一次的讀取結果是0,所以不再進行跳轉,而是繼續往下執行:

這次得到的長度結果正好就是之前調試看到的數值0x40000001。
總結
在此次的漏洞分析過程中,由于無法說服自己接受“通過TrueType Font Analyzer對于TTF文件的解析結果確定漏洞位于glyf表”中這一因果關系(因為這個工具過于小眾,且信息太少),因此我完全放棄根據書中的步驟對漏洞進行分析,轉而去查看TrueType的文檔。在本文中花費了大量篇幅對TTF文件格式進行了介紹,正是通過對文件格式的理解,我確定了問題處在glyf表中,并進一步確定了問題數據0x40000001的來源。
在我完成漏洞分析轉而看書中的介紹時,發現兩者殊途同歸,最后竟然都對poc文件中的指令進行了分析,只不過我是從文件格式手工解析出發,轉而通過調試驗證,而書中是先通過調試確定了指令執行順序,進而解析文件中的部分指令。
從我個人的角度來說,通過對文件格式的理解進而進行漏洞分析,整個邏輯過程會比較通順,也易于理解。經過了此次漏洞分析,對于TTF文件格式也有了更深一步的認識。