作者:天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/8GOXFfQZOvGB7W-sD6osLA

0x00前言

Windows圖形組件DWrite庫是用于高質量文本呈現的用戶模式動態庫,DWrite庫存在遠程代碼執行漏洞。目前已有POC,POC可以實現任意地址寫任意內容。

基于此我們做了分析,分析后得知字體庫文件中的”maxp”表內容存在錯誤數據時,DWrite庫使用此數據計算字體所需內存,導致分配內存過小,程序讀取字體庫中的數據寫入數組并越界寫入 到另外一個數據結構中,后續將結構中數據作為指針操作其中內容,寫入數據和指針都可控。通過分析補丁,補丁修補方案為:取出”maxp”表的另一字段再加4,比較之前畸形數據得到較大值,以此計算需要分配內存大小。

文章包含如下內容:

定位chrome引擎的渲染進程

使用條件記錄斷點動態調試分析

使用IDA補丁比較靜態分析

0x01漏洞信息

漏洞簡述

漏洞名稱:(Windows圖形組件遠程執行代碼漏洞)

漏洞編號:(CVE-2021-24093)

漏洞類型:(數組越界寫)

漏洞影響:(遠程代碼執行)

CVSS評分:(8.8)

利用難度:不太容易利用

基礎權限:需要用戶訪問網頁

組件概述

Microsoft DirectWrite是用于高質量文本呈現的現代Windows API。它的大部分代碼位于DWrite.dll用戶模式庫中。它用作各種廣泛使用的桌面程序(例如Windows上的chrome,Firefox和Edge)的字體光柵化程序。

漏洞利用

使用chrome瀏覽器訪問Web頁面,渲染引擎進程異常,導致chorme頁面崩潰。

漏洞影響

漏洞主要影響Win10的某些版本及Windows Server 2016、2019、2004、20H2等系統。

0x02漏洞復現

CVE-2021-21087

環境搭建

靶機環境: Windows 1909專業版 x64、chrome 86.0.4240.193 (64位)

poc.html放在本機,漏洞環境也在本機。

復現過程

1.將POC文件與poc.ttf放在同一目錄下,使用chrome打開poc.html文件。

2.頁面打開,點擊確定按鈕加載ttf文件。

3.瀏覽器渲染引擎進程崩潰

4.定位進程崩潰地點

(1)打開Html頁面時會啟動多個chrome進程,首先需要定位到哪個是渲染引擎進程。

(2)先關閉chrome瀏覽器(否則會影響定位結果),使用火絨劍來定位渲染引擎進程,清空火絨劍記錄內容,開啟監控,設置動作過濾包括進程啟動和進程退出,點擊確定,然后開啟監控,如下:

(3)使用chrome打開poc.html,在彈出框上點擊確定,之后渲染引擎崩潰,關閉監控,查看監控到的內容,過濾監控內容,只需要包含chrome.exe的記錄,如下:

(4)可以看到最后一個進程退出,進程ID為4228,渲染引擎崩潰,進程應該也會退出,假設最后一個進程是渲染引擎進程,它創建記錄是在從上往下數第七個,再試一次,重復(2-3)的過程,這次打開poc.html之后不要點擊確定按鈕,查看火絨劍記錄如下:

定位到第七個進程,進程ID為4948,使用Windbg x64附加此進程,(如果chrome瀏覽器切換到后臺,比如我點擊回到桌面,chrome會自動點擊確定按鈕,導致渲染進程退出,可以先打開windbg,不用切回桌面再打開windbg,或者使用雙屏幕也可以。)

附加成功之后,輸入g繼續運行,然后回到瀏覽器中,點擊確定按鈕,windbg斷下,如下:

可以看到引用一個錯誤的內存地址,發生異常,可知已經定位到正確的渲染進程。

0x03漏洞分析

基本信息

漏洞文件:DWrite.dll

漏洞函數:fsg_ExecuteGlyph

漏洞對象:TrueType字體中的”maxp”表

背景知識

TrueType字體通常包含在單個TrueType字體文件中,其后綴為.TTF。TrueType中的所有數據都使用big-endian編碼,TTF文件中包含了字體的版本號和幾個表,每個表都有一個TableEntry結構項,TableEntry結構包含了資源標記、校驗和、偏移量和每個表的大小。下面是TrueType字體目錄的C語言定義:

typedef sturct

{

char  tag[4];

ULONG  checkSum;

ULONG  offset;

ULONG  length;

}TableEntry;

typedef struct

{

Fixed  sfntversion;  //0x00010000  for  version  1.0

USHORT  numTables;

USHORT  searchRange;

USHORT  entrySelector;

USHORT  rangeShift;

TableEntry  entries[1];//variable  number  of  TableEntry

}TableDirectory;

文件開頭為TableDirectory結構體, TableDirectory結構的最后一個字段是可變長度的TableEntry結構的數組,每個結構對應一個表。TrueType字體中的每個表都保存了不同的邏輯信息,其中”maxp”表的作用是描述字體中所需內存分配情況的匯總數據,”maxp”表的內容具體結構為:

typedef struct

{

Fixed version;// 0x00010000 for version 1.0

USHORT numGlyphs;

USHORT maxPoints;// 非復合字形中的最大點

USHORT maxContours;

USHORT maxCompositePoints;// 復合字形中的最大點

USHORT maxCompositeContours;

USHORT maxZones;

USHORT maxTwilightPoints;

USHORT maxStorage;

USHORT maxFunctionDefs;

USHORT maxInstructionDefs;

USHORT maxStackElements;

USHORT maxSizeOfInstructions;

USHORT maxComponentElements;// 任何復合字形在“頂級”處引用的最大組件數

USHORT maxComponentDepth;

}

詳細分析

1. 基礎分析

poc.ttf中數據:

圖中箭頭1指向的數據為”maxp”表的TableEntry結構,Offset字段為00000158為箭頭2所指向的地方,是”maxp”表內容的具體結構,maxPoints字段值為0(距離箭頭2偏移0x6),maxCompositePoints字段為3(距離箭頭2偏移0xA)。

AE標志符號的表條目中的x和y增量在運行時會覆蓋到另一個數據結構,TTF中內容如下:

異常觸發時指令為add word ptr [r8+56h],ax,ax為 0x9E9F(圖中1標記),r8為0x00007A7B00007879(圖中2標記)的地址處,78 79 7A 7B都是TTF中的數據,補0是因為在異常指令之前對x數組和y數組調用了memset初始化內存空間。

poc.html中如下:

正常情況下,ttf文件中maxPoints字段值為0x168maxCompositePoins字段值為0x2352,在poc.ttf文件中將”maxp”結構中maxPoints字段的值改為0,將maxCompositePoins值改為3,當加載并光柵化損壞的”maxp”表的數據時,會導致堆分配緩沖區過小,調用棧如下:

當復合字形?(AE,HTML實體Æ,U + 00C6)被柵格化時,函數DWrite! fsg_ExecuteGlyph崩潰,調用棧如下:

fsg_ExecuteGlyph函數內部對堆塊內部的兩個整數數組(對應于x和y坐標)進行操作,使用0x148這個長度調用memset來初始化兩個數組,但是數組的長度小于0x148,會將跟在數組后面的一個指針置0。

如果字體是一個變量且指定了軸值,它還將調用TrueTypeRasterizer :: Implementation::ApplyOutlineVariation-> GlyphOutlineVariationInterpolator :: ApplyVariation會從TTF中獲取數據賦值給數組內的成員,但是數組較小,所以將數組后面的指針置為TTF中的數據。之后會向這個指針指向的地址中寫入數據導致異常。

漏洞庫版本如下:

2.靜態分析

崩潰函數fsg_ExecuteGlyph分析

在大的堆塊中,有兩塊內存分別用于x數組和y數組,調用memset初始化,之后調用call cs:off_7FFF41C70D10 ,函數內部會調用DWrite!TrueTypeRasterizer::Implementation::ApplyOutlineVariation給x數組和y數組賦值,addr1指向的內存沒有0x148字節那么大,所以會寫到其它的數據對象上,接下里會引用被覆蓋的數據作為指針去寫數據: rsi+8中的數據被數組賦值時修改了,使用TTF中的數據覆蓋了[rsi+8]的數據,0x00007FFF41B341F6地址處調用的指令dd [r8+56],ax,其中ax中的數據也是可以控制的。

函數調用鏈

計算內存大小的函數調用鏈為

TrueTypeRasterizer::Implementation::Initialize-> fs_NewSfnt ->fsg_WorkSpaceSetOffsets函數fsg_WorkSpaceSetOffsets內部計算需要申請的內存空間大小并將結果傳出到fs_NewSfnt中。

fs_NewSfnt獲取需要申請的內存大小,之后調用calloc申請內存。

可以看到圖中調用完成fs_NewSfnt后,在下面的循環中獲取v39內存塊中的內容作為申請內存的大小。

fs_NewSfnt函數內容如下:

v2為傳入的第二個參數a2,*((_DWORD *)v2 + 3)fs_NewSfnt中取內存大小區域((_DWORD )(v14 + 4i64 * j),j為3時)是一致的。

補丁Diff

fsg_WorkSpaceSetOffsets函數補丁前后有修改。

查看補丁前fsg_WorkSpaceSetOffsets函數偽C代碼如下:

其中參數v3指向”maxp”表具體內容

可以看到圖中從maxCompositePointsmaxPoints字段中取較大值,并且與常量1比較取較大值,得到值為3,之后加8,得到0xb,作為第一個參數傳入fsg_GetOutlineSizeAndOffsets中,這個函數看名稱應該是獲取輪廓的大小和偏移值。

補丁后fsg_WorkSpaceSetOffsets函數偽C代碼有點問題,所以下面貼出匯編代碼如下:

打過補丁后,程序是先從maxCompositePointsmaxPoints字段中取較大值,得到3,再與1比較得較大值仍然為3,3+8得到0xb,再取出maxp表中maxComponentElements字段,POC中此值為0x0062,相對”maxp”表具體內容偏移為0x1C

0x62+4=0x66,比較0x66與0xb得到較大值作為第一個參數傳入fsg_GetOutlineSizeAndOffsets函數中。

函數fsg_GetOutlineSizeAndOffsets沒有變化,只是因為傳入的第一個參數不同,所以最終計算出的結果也不同。

漏洞函數分析

fsg_ExecuteGlyph函數對堆塊內部的兩個整數數組,對應于x坐標和y坐標進行操作,實際上數組的較小,fsg_ExecuteGlyph函數先調用了兩次memset將數組清零,如果字體是一個變量且指定了軸值,還會調用TrueTypeRasterizer::Implementation::ApplyOutlineVariation->GlyphOutlineVariationInterpolator::ApplyVariation將字體中的數據賦值到坐標數組,這樣就會破壞到后續的結構成員。

3. 動態分析

計算所需內存的過程可以通過條件斷點的方式來調試,附加到調試器后,可以設置如下斷點命令,

bp DWrite!TrueTypeRasterizer::Implementation::Initialize "r $t0=$t0+1; .printf \"Initialize times:%d\n\",@$t0;.echo;gc"

可以看到第13次調用TrueTypeRasterizer::Implementation::Initialize函數之后就會進入崩潰。

重新啟動,下斷點:

bp DWrite!TrueTypeRasterizer::Implementation::Initialize "r $t0=$t0+1; .printf \"Initialize times:%d\n\",@$t0;.echo;.if(@$t0 == 0x0D){}.else{gc}"

運行可以看到:

在調用fs_NewSfnt之前下斷點,查看傳入參數內容:

單步步過,再次查看內存,可以看到需要申請的內存大小為0x6fa4。

繼續往下運行:

可以看到申請的內存地址為0x00000196bc484b60。

在崩潰函數中下斷點,運行:

堆塊起始地址為上面的申請的0x00000196bc484b60,調用memset使用的大小為0x148,上面是內存塊1,addr1為0x00000196bc4850fc

查看堆塊大小以及addr1相對堆塊起始地址的偏移大小如下:

最終調用fsg_ExecuteGlyph+0x772處的指令add [r8+56h], ax時,a8來源于[rsi+8]

查看rsi+8相對于堆塊的偏移

rsi+8相對于堆塊起始地址偏移0x6c8,而addr1相對于堆塊起始地址偏移0x59c,0x59c+0x148=0x6E4>0x6C8,所以操作addr1中的數據時會覆蓋rsi+8處的數據,從圖上可以看到,調用memsetrsi+8中的數據初始化為0。

調用ApplyOutlineVariation函數之后,rsi+8處數據被修改為ttf文件中的數據。(重新啟動,內存地址與上面不一樣)

繼續運行,如下:

可以看到ax為0x9e9f,r8為0x00007a7b00007879,這兩個數據都在poc.ttf文件中的數據,所以這個指針數據可控(where),要寫入的內容(what)也可控。

打過補丁之后查看DWrite!fsg_ExecuteGlyph函數沒有變化,調試發現整個堆塊的大小變得更大了,x數組和y數組的大小還是0x148字節,ESI對象距離堆塊起始地址的距離變大,所以在x數組和y數組的賦值過程中沒有覆蓋到ESI對象,如下: 可以看到堆塊大小為0x7d24,之前為0x6fa4。

addr1結束地址為0x0000015fd070427c<0x0000015fd0704868,所以數組使用0x148的大小不會覆蓋到后面的數據。

0x04總結

TTF文件的”maxp”表描述字體中所需內存分配情況的匯總數據,DWrite庫沒有校驗數據,在ttf中寫入畸形數據時,程序申請內存過小,導致溢出到其它的數據結構,實現了任意地址寫任意數據。補丁在計算所需內存時讀取ttf中另一個值+4,與之前畸形數據比較得較大值,申請足夠的內存。

0x05解決方案

微軟已經更新了官方補丁,下載鏈接:

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-24093

0x06參考文獻

1.https://bugs.chromium.org/p/project-zero/issues/detail?id=2123

2.https://docs.microsoft.com/en-us/typography/opentype/spec/maxp

3.https://blog.csdn.net/blueangle17/article/details/23750999


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1526/