當前位置:
首頁 > 新聞 > 50天收穫50個CVE:針對Adobe Reader的模糊測試

50天收穫50個CVE:針對Adobe Reader的模糊測試

一、概述

2017年,是漏洞領域的一個轉折點,在當年報告的新增漏洞數量約為14000個,是2016年的兩倍。其中的一個可能的原因,是自動漏洞查找工具(也被稱為模糊測試工具,Fuzzer)越來越受歡迎。

模糊測試工具的出現並不是新聞,這些工具已經存在了二十多年。但是,目前的模糊測試工具已經長大,他們的能力更強、更容易使用、整體更加成熟。儘管如此,使用模糊測試工具在某種程度上仍然被認為是一種「黑暗藝術」,許多研究人員並不樂於使用模糊測試工具,因為他們認為這些工具非常麻煩。

考慮到上述情況,我們很自然的會產生一些問題:儘管有更多的研究人員正在使用模糊測試工具來發現更多的漏洞,但所有研究人員都會使用模糊測試工具嗎?有多少漏洞還靜靜掛在那裡,只等待著第一個研究人員按下「FUZZ」的紅色大按鈕?

為了找到答案,我們創建了一個最為普通的實驗環境。我們使用最常見的Windows模糊測試框架之一——WinAFL,並針對世界最受歡迎的軟體產品之一——Adobe Reader進行測試。我們預先設置了50天的測試時限,首先對代碼進行逆向工程,尋找潛在的易受攻擊的庫,然後編寫Harness,最後運行模糊測試工具。

測試的結果讓我們大開眼界。在50天之內,我們在Adobe Reader中找到了50多個新漏洞。平均而言,每天都能發現1個漏洞,這種速度對於人工研究來說是難以實現的。

在本文中,我們詳細說明了完整的測試過程。同時,向大家介紹一種新穎的方法,能夠擴大搜索範圍,並改進WinAFL。最後,我們還會分享在此過程中得到的經驗和體會。

二、背景介紹

2.1 關於WinAFL

AFL是一種引導式的通用模糊測試工具,具有非常可靠的實現和巧妙的啟發式方法,已經被證明能夠在真實軟體中非常成功的發現有效的漏洞。

WInAFL是AFL在Windows上的分支版本,有Ivan Fratric(Google Project Zero)創建和維護。Windows版本使用不同風格的檢測方式,使我們能夠針對封閉的源二進位文件進行檢查。

我們建議各位讀者閱讀AFL技術論文,其中詳細介紹了AFL的工作原理。同時,其中還指出了該工具的缺點,並幫助用戶在出現問題時進行調試。

我們發現,WinAFL在查找文件格式錯誤方面非常有效,特別是針對壓縮的二進位格式(圖像、視頻、壓縮包)。

2.2 針對Acrobat Reader DC的攻擊

要測試Acrobat Reader DC,最簡單的一個起點就是核心可執行文件AcroRd32.exe。這是一個相對較小的AcroRd32.dll的包裝器(Wrapper),大小約為30MB。AcroRd32.dll中有很多代碼,其中包含PDF對象的解析器,但其中也有很多GUI代碼(這部分代碼,通常不是我們要尋找漏洞的位置)。

我們知道,WinAFL在二進位格式方面的處理更好,因此我們決定集中精力,只攻擊特定的解析器。這裡的挑戰在於,我們如何找到解析器,並為其編寫Harness。在稍後,我們會詳細解釋什麼是Harness。

我們希望一個具有最小依賴關係的二進位格式解析器,我們可以載入這些解析器,而不再需要載入整個Reader進程。

我們探索了Acrobat文件夾中的DLL,發現其中的JP2KLib.dll適合所有類別:

JP2KLib.dll是JPEG2000格式的解析器,它是一種複雜的二進位格式文件(753 KB),並且具有非常重要的導出函數。

我們的研究是在以下版本進行的:

·Acrobat Reader DC 2018.011.20038及更早版本

·JP2KLib.dll 1.2.2.39492版本

2.3 關於目標函數

目標函數(Target Function)是WinAFL中用於描述作為模糊測試過程入口點的函數的術語。該函數在fuzz_iterations循環中被調用,每一次都會改變磁碟上的輸入文件。該函數必須:

1、打開輸入文件,讀取文件,解析輸入並關閉文件。

2、能正常返回,不拋出C++異常或調用TerminateProcess。

要尋找到這樣的函數,是非常困難的。所以,在針對複雜軟體進行模糊測試時,我們通常需要編寫Harness。

2.4 關於Harness

Harness是一個小程序,可以觸發我們想要模糊測試的功能。Harness中包含將要作為目標函數的函數。以下是WinAFL存儲庫中gdiplus小型Harness的示例:

Main的第一個參數是一個路徑。在函數中,我們會調用Image::Image解析器,這是我們想要模糊的API。需要注意的是,在錯誤的情況下,我們不會終止進程。在最後,將會釋放所有資源。

對於有記錄的API,這一過程相對容易。我們可以使用藉助官方文檔,複製示例代碼或編寫簡單程序。但其中的樂趣在哪裡呢?

我們選擇的目標是Adobe Reader,這是一個封閉的二進位文件。針對這種類型的目標,要編寫Harness,其步驟如下所示:

1、找到我們想要模糊的功能。

2、對其進行逆向工程。

3、編寫一個調用反向API的程序。

4、重複上述步驟,直到獲得一個功能齊全的完整Harness。

在下一章中,我們會詳細描述如何對JP2KLib進行逆向工程,並為其編寫了一個Harness。此外,還在下一章中分享了一些經驗。如果讀者只對模糊測試的方法感興趣,可以跳過下一章的閱讀。

三、編寫JP2KLib.dll的Harness

在開始對JP2KLib.dll進行逆向工程之前,我們首先檢查了該庫是開源的,還是具有公共符號的,這樣做會節約大量的時間。但是,在我們的示例中,沒有那麼幸運。

由於我們希望我們的Harness與Adobe Reader使用JP2KLib的方式儘可能相似,那麼要做的第一件事就是找到一個觸發我們想要進行模糊測試的行為的PDF文件。這樣一來,我們就能夠輕鬆找到程序的相關部分。

實際上,我們找到了大量的PDF文件來測試產品。我們使用了字元串「/JPXDecode」,這是JPEG2000的PDF過濾器。當然,也可以使用Google搜索示例文件,或者使用Acrobat Pro / Phantom PDF生成測試用例。

提示1:閱讀器有一個沙箱,有時會進行非常煩人的調試或分類操作,但它可以被禁用。可以參考:https://forums.adobe.com/thread/2110951

提示2:我們打開PageHeap,以便輔助進行逆向工程,因為該功能有助於跟蹤分配的位置和大小。

我們從示例中提取了jp2文件,於是便可以將其用於沒有PDF包裝器的Harness中。該文件將用於Harness的測試輸入。

現在,我們有一個最小化的可用示例,接下來使用「sxe ld jp2klib」在JP2KLib.dll的load事件上放置一個斷點。當走到斷點時,我們在JP2KLib的所有導出函數上放置了一個斷點命令。該斷點命令記錄調用的棧、前幾個參數以及返回值:

bm /a jp2klib!* 「.echo callstack; k L5; .echo parameters:; dc esp L8; .echo return value: ; pt; 」

我們載入了示例PDF,並得到以下輸出:

JP2KLibInitEx是載入JP2KLib後調用的第一個函數。我們注意到,JP2KLibInitEx只接受一個參數,具體如下:

可以看到,它是一個大小為0x20的結構,其中包含指向AcroRd32.dll中函數的指針。當我們遇到未知函數時,我們並不急於對其進行逆向,因為我們不知道它是否會被目標代碼所使用。相反,我們將每個地址指向一個唯一的空函數,我們稱之為「nopX」(其中X是一個數字)。

我們現在有足夠的信息,開始編寫我們的Harness框架:

1、從命令行參數獲取輸入文件。

2、載入JP2KLib.dll。

3、獲取指向JP2KLibInitEx的指針,並使用8個nop函數結構來調用它。

我們使用LOAD_FUNC作為方便的宏。我們還有一個用於創建nop函數的NOP(x)宏。

接下來,編譯並運行sample.jp2,它可以成功工作!

讓我們繼續。然後,我們轉到下一個函數JP2KGetMemObjEx,該函數不帶任何參數,因此我們只需調用它並保存返回值即可。

接下來的函數JP2KDecOptCreate也不帶任何參數,我們採用同樣的方法,調用並保存返回值。但是,我們注意到JP2KDecOptCreate在內部會調用nop4和nop7,這意味著我們需要實現它們。

接下來,需要了解「nop4」的作用。我們在原始函數指針上放置了一個斷點AcroRd32!CTJPEGDecoderRelease+0xa992,並繼續執行:

接下來會流向:

經過幾個步驟後:

事實證明,nop4是malloc的一個輕便包裝器。我們可以在Harness中實現它,並使用「nop4」替換掉它。接下來,再次對nop7重複上述的過程,最終發現它是memset。經過仔細分析,發現nop5和nop6分別是free以及memcpy。

接下來的函數,JP2KDecOptInitToDefaults需要使用一個參數調用。該參數是JP2KDecOptCreate的返回值,所以我們將這個值傳遞給它。

下一個函數JP2KImageCreate不帶參數,因此我們對其進行調用,並保存返回值。

目前,我們的Harness看起來像是這樣:

下一個函數是JP2KImageInitDecoderEx,需要5個參數。

我們找到了5個參數中的3個,分別是JP2KImageCreate、JP2KDecOptCreate和JP2KGetMemObjEx的返回值。

在這裡,注意到第三個參數指向vtable。於是,採用了與之前相同的技巧,我們創建一個具有相同大小的結構,指向「nop」函數。

第二個參數指向另一個結構,只是這次它似乎不包含函數指針,因此我們決定發送常數值0xbaaddaab。

此時,代碼如下所示:

我們跑完了Harness,很快就到達了nop10。在Adobe Reader的相應函數上設置了一個斷點,並進入以下調用棧:

在IDA上查看JP2KCodeStm::IsSeekable,具體如下:

通過查看WinDbg,我們可以看到偏移量為0x24的JP2KCodeStm中包含我們的vtable,偏移量0x18的位置包含0xbaaddaab。我們可以看到,JP2KCodeStm::IsSeekable從vtable調用函數傳遞了0xbaaddaab作為第一個參數,所以它基本上是我們的vtable函數#7的一個精簡包裝器。

按道理,每個解析器之間都會有一些區別,但通常它們的輸入流可能都位於熟悉的文件界面(例如FILE / ifstream)。通常,它是一種抽象底層輸入流(網路/文件/內存)的自定義類型。因此,當我們看到如何使用JP2KCodeStm時,一切就都非常清晰了。

回到我們的例子,0xbaaddaab是流對象,vtable函數在流對象上運行。

我們打開IDA,並查看了所有其他JP2KCodeStm::XXX函數。

它們都非常相似,所以我們繼續創建自己的文件對象,並實現了所有必要的方法。最終代碼如下所示:

我們會對JP2KImageInitDecoderEx的返回值進行檢查,從而確保不會出現錯誤。在我們的案例中,JP2KImageInitDecoderEx成功返回了0。我們嘗試了幾次,試圖正確實現流函數,最終得到了我們想要的返回值。

下一個函數JP2KImageDataCreate不帶函數,其返回值傳遞給JP2KImageGetMaxRes函數。我們對這兩個函數進行調用,並繼續前進。

接下來,到達JP2KImageDecodeTileInterleave函數,它居然需要7個參數!其中的3個參數分別是JP2KImageCreate、JP2KImageGetMaxRes和JP2KImageDataCreate的返回值。

在xref之後,使用IDA查看AcroRd32內部,我們發現第二個和第六個參數為空。

接下來,就只剩下第四個和第五個參數了。我們認為,這兩個參數取決於顏色深度(8/16),所以我們決定使用恆定的深度值來模糊處理。

最終,我們得到了:

最後,我們調用函數JP2KImageDataDestroy、JP2KImageDestroy和JP2KDecOptDestroy來釋放我們創建的對象,並避免內存泄漏。當模糊測試的迭代次數(fuzz_iterations)較多時,這一步對於WinAFL來說至關重要。

大功告成,現在我們就有一個可以使用的Harness了。

在最後一次調整中,我們分離了初始化代碼,載入JP2KLib並從解析代碼中查找函數。這樣能夠提高性能,因為我們就不必在每次模糊迭代中為初始化過程浪費時間。我們將新的函數稱為「fuzzme」,並且將會導出「fuzzme」(可以在exe文件中導出函數),因為它比在二進位文件中找到相關的偏移量要相對簡單一些。

在WinAFL測試Harness時,我們發現WinAFL會生成重複的文件。經過深入探究,我們發現Adobe使用了不同於libc中定義的SEEK常量,導致我們混用了SEEK_SET和SEEK_CUR。

四、模糊測試的方法論

1、對Harness進行基本測試:穩定性、路徑、超時值

2、模糊測試前環境部署

3、初始語料庫

4、初始行覆蓋範圍

5、模糊循環:模糊測試、檢查覆蓋/崩潰、cmin與重複

6、分類

4.1 對Harness進行基本測試

在開始大規模模糊測試之前,我們首先會進行一些健全性測試,以確保我們沒有做無用功。要檢查的第一件事,是模糊測試工具是否能夠藉助我們的Harness實現新的路徑,也就是總路徑的數量是否能夠穩步上升。

如果路徑計數為0,或者幾乎為0,我們可以檢查是否出現了如下問題:

1、目標函數由編譯器內聯,導致WinAFL錯過目標函數的入口,並導致WinAFL終止程序終止(Abort)。

2、如果參數數量(-nargs)不正確,或調用約定(Calling Convention)不是默認值,也可能發生這種情況。

3、超時值:有時超時值太低,也會導致模糊測試工具過快地終止Harness。解決方案就是提高超時值。

我們讓模糊測試工具運行幾分鐘,然後檢查模糊測試工具的穩定性。如果穩定性很低(低於80%),那麼我們應該嘗試去繼續調試。Harness的穩定性非常重要,因為它會影響模糊測試工具的精度和性能。

常見的陷阱:

1、檢查隨機元素。例如,一些哈希表的實現會使用隨機數來防止衝突,但這對於保證準確性來說是不利的。我們需要做的,是將隨機種子修改為常數值。

2、有時,軟體具有某些全局對象的緩存。我們通常只是在調用目標函數之前執行nop,從而減少這一方面的影響。

3、對於Windows 10 x64環境上的32位應用程序,棧對齊並不總是8個位元組。這意味著有時memcpy和其他AVX優化代碼的行為會有所不同。這種情況,會直接影響覆蓋率。一種解決方案就是在Harness中添加代碼,從而對齊棧。

如果上述嘗試都失敗了,我們可以使用DynamoRIO對Harmess進行指令跟蹤,並對輸出進行差異化處理。

4.2 模糊測試前環境部署

我們要部署的環境是Windows 10 x64的虛擬機,其中包含8-16個內核以及32GB內存。

我們使用ImDisk工具包在RAM磁碟驅動器上進行模糊測試。我們發現,對於某些注重測試速度的目標,將測試用例寫入磁碟上是一個性能瓶頸。

在這裡,考慮到性能問題,應該禁用Windows Defender,因為WinAFL生成的一些測試用例是由Windows Defender發現的已知漏洞(漏洞利用:Win32/CVE-2010-2889)。

禁用Windows索引服務(Windows Indexing Service),從而提高性能。

此外,還需禁用Windows Update,因為它會干擾模糊測試(重新啟動計算機,並替換模糊的DLL)。

我們為Harness進程啟動頁堆(Page Heap),因為事實證明,啟用後可以發現我們原本無法檢測到的漏洞。

下面是運行adobe_jp2k工具的示例命令:

afl-fuzz.exe -i R:jp2kin -o R:jp2kout -t 20000+ -D c:DynamoRIO-Windows-7.0.0-RC1in32 -S Slav02 — -fuzz_iterations 10000 -coverage_module JP2KLib.dll -target_module adobe_jp2k.exe -target_method fuzzme -nargs 1 -covtype edge — adobe_jp2k.exe @@

4.3 初始語料庫

一旦我們有了可以使用的Harness,就可以為其創建一個初始語料庫,其來源包括:

1、在線語料庫(afl語料庫、openjpeg-data)。

2、來自開源項目的測試套件。

3、從Google或DuckDuckGo抓取的內容。

4、以往模糊測試項目的語料庫。

4.4 語料庫最小化

如果使用相同覆蓋範圍的大量文件,將會影響模糊測試工具的性能。在AFL中,通過使用afl-cmin最小化語料庫來解決這一問題。在WinAFL中,有一個名為winafl-cmin.py工具的埠。

我們獲取收集到的所有文件,並通過winafl-cmin.py來運行它們,這樣就能產生最小的語料庫。

我們可以運行winafl-cmin至少兩次,並查看是否得到了相同的文件集。如果得到了兩個不同的集合,通常就意味著我們的Harness中存在著不確定性。這是我們嘗試使用afl-showmap或其他工具進行調查的內容。

成功完成最小化後,我們將文件集保存為初始語料庫。

4.5 初始行覆蓋範圍

現在,我們有一個最小的語料庫,我們想看一下行覆蓋範圍(Line Coverage)。行覆蓋表示我們實際執行的彙編指令。為了獲得行覆蓋,我們針對每個測試用例都使用DynamoRIO:

[dynamoriodir]in32drrun.exe -t drcov -- harness.exe testcase

接下來,使用Lighthouse將結果載入到IDA:

我們注意到初始行覆蓋範圍,因為這有助於我們評估模糊測試Session的效果。

4.6 模糊循環

接下來的一步非常簡單:

1、運行模糊測試工具。

2、檢查覆蓋範圍和崩潰情況。

3、調查覆蓋率、cmin,並重複這一過程。

運行模糊測試工具前,並不需要任何特殊的設置,只需在上述配置環境中運行模糊測試工具。

我們有一個具有以下功能的自動工具:

1、所有模糊測試工具的狀態(使用winafl-whatsapp.py)

2、每個模糊測試工具的路徑圖(使用winafl-plot.py)

3、對崩潰進行分類,並生成報告(將在下一節詳細闡述)

4、重新啟動掛掉的模糊測試工具。

這些任務的自動化是非常重要的,否則模糊測試就是一個單調乏味並且容易出現錯誤的過程。

我們要每隔幾個小時檢查一次模糊測試工具的狀態,以及隨著時間推移的路徑變化情況。如果發現圖表平穩,我們就會嘗試分析覆蓋範圍。

我們複製所有模糊測試工具的所有隊列,並通過cmin運行它們,最後查看IDA中的結果。我們尋找相對較大且覆蓋範圍非常小的函數,嘗試了解與此功能相關的功能,並主動查找會觸發此功能的示例。在JP2K中,這並不是很有幫助,但在其他目標中,特別是文本格式中,會非常有效。

這個階段非常重要。在一次測試中,我們添加了一個樣本,經過幾個小時的模糊測試後,將行覆蓋率上升至1.5%,並成功發現了3個新的安全漏洞。

然後,我們重複這個循環,直到時間用盡,或者沒有看到任何覆蓋範圍的改變。這就意味著我們必須改變目標,或嘗試改進Harness。

4.7 分類

一旦出現了一組導致崩潰的測試用例,我們就會手動檢查崩潰以及導致每次崩潰的輸入內容。很快,我們就改變了策略,因為有很多重複的內容。於是,我們開始使用BugId自動查找重複項,並使其最小化。這一過程同樣使用自動工具來完成。

五、成果

通過上述策略,我們最終得以在Adobe Reader和Adobe Pro中找到53個關鍵漏洞。

我們針對不同的解析器重複了這一過程,例如圖像、流解碼器和xslt模塊,最終發現了如下的CVE:

CVE-2018-4985、CVE-2018-5063、CVE-2018-5064、CVE-2018-5065、CVE-2018-5068、CVE-2018-5069、CVE-2018-5070、CVE-2018-12754、CVE-2018-12755、CVE-2018-12764、CVE-2018-12765、CVE-2018-12766、CVE-2018-12767、CVE-2018-12768、CVE-2018-12848、CVE-2018-12849、CVE-2018-12850、CVE-2018-12840、CVE-2018-15956、CVE-2018-15955、CVE-2018-15954,CVE-2018-15953、CVE-2018-15952、CVE-2018-15938、CVE-2018-15937、CVE-2018-15936、CVE-2018-15935、CVE-2018-15934、CVE-2018-15933、CVE-2018-15932 、CVE-2018-15931、CVE-2018-15930 、CVE-2018-15929、CVE-2018-15928、CVE-2018-15927、CVE-2018-12875、CVE-2018-12874 、CVE-2018-12873、CVE-2018-12872,CVE-2018-12871、CVE-2018-12870、CVE-2018-12869、CVE-2018-12867 、CVE-2018-12866、CVE-2018-12865 、CVE-2018-12864 、CVE-2018-12863、CVE-2018-12862、CVE-2018-12861、CVE-2018-12860、CVE-2018-12859、CVE-2018-12857、CVE-2018-12839、CVE-2018-8464。

我們在jp2k中發現的一個漏洞,實際上在我們發現之前不久已經被Adobe接收,因為這一漏洞疑似已在野外發現被利用。

當然,Adobe Reader是以沙箱模式運行的,而閱讀器保護模式(Reader Protected Mode)極大增加了將沙箱內可進行漏洞利用的崩潰轉變成攻陷系統的方法的複雜性,這通常需要額外的PE漏洞利用,例如上面所說的野外利用就是如此。

我們非常喜歡WinAFL,並希望該工具能夠被大家更廣泛地利用。

在使用WinAFL的過程中,我們還遇到了許多漏洞或功能缺失。我們添加了對這些新功能的支持,並對漏洞進行了修復。其中包括:在Windows 10中增加對App驗證程序的支持、對工作程序的CPU關聯、修復漏洞、增加一些GUI功能。

各位讀者可以在這裡查看新的提交版本:

Netanel的提交版本:https://github.com/googleprojectzero/winafl/commits?author=netanel01

Yoava的提交版本:https://github.com/googleprojectzero/winafl/commits?author=yoava333


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

通過.NET實現Gargoyle

TAG:嘶吼RoarTalk |