許 樸,舒 輝,于穎超
(信息工程大學(xué) 數(shù)學(xué)工程與先進(jìn)計(jì)算國家重點(diǎn)實(shí)驗(yàn)室,河南 鄭州 450001)
隨著硬件性能的提高,人們對于軟件功能的需求也越來越高。這使得軟件程序更加復(fù)雜,公開發(fā)現(xiàn)的軟件漏洞也日趨增多[1]。雖然通過人工來檢測軟件漏洞有著不可替代的優(yōu)勢。但是對于日益復(fù)雜的軟件,僅僅依靠人工來進(jìn)行檢測已經(jīng)遠(yuǎn)遠(yuǎn)不夠。其一,人的精力有限。要發(fā)現(xiàn)軟件缺陷,首先需要深入了解軟件框架,而目前軟件實(shí)現(xiàn)愈加趨于復(fù)雜,要理解消化軟件代碼耗時(shí)巨大。其二,軟件缺陷種類多樣[2],人在檢查軟件缺陷時(shí)往往難以做到面面俱到。
因此利用自動化的方法檢測軟件漏洞十分必要。在工業(yè)界,對于挖掘軟件漏洞比較常見的方法是采用模糊測試,例如Google的OSS-Fuzz項(xiàng)目。模糊測試的英文稱作Fuz-zing,屬于動態(tài)執(zhí)行灰盒測試框架,幾乎不需要對被測試的程序做深入分析,一部分的實(shí)現(xiàn)也可以無需程序源碼。這兩點(diǎn)使其應(yīng)用范圍廣,任何接受輸入的程序都能被用來做測試。然而模糊測試由于缺乏對程序的感知能力,難以高效生成魔數(shù)等校驗(yàn)字段,對初始樣本的依賴程度較高。目前關(guān)于模糊測試的研究主要集中在提高樣本生成質(zhì)量以達(dá)到提高代碼覆蓋率的目的。
傳統(tǒng)Fuzzing通過既定的策略(比特翻轉(zhuǎn),數(shù)值運(yùn)算,固定數(shù)值填充等)以及一些隨機(jī)化(隨機(jī)數(shù)值填充)的方式來修改輸入樣本,使生成的不同輸入樣本能夠觸發(fā)程序不同的執(zhí)行流程,以提高代碼覆蓋率來觸發(fā)軟件漏洞。比較有代表性的Fuzzing工具有AFL[3],honggfuzz,libFuz-zer[4]等。這些工具采用較為簡單的灰盒方式,僅依靠代碼覆蓋率信息作為反饋,以此進(jìn)行種子選擇來輔助樣本生成。但是它們忽略程序代碼本身以及輸入的數(shù)據(jù)流信息,并不能有效產(chǎn)生高質(zhì)量輸入,只能依賴于優(yōu)秀的初始種子樣本以及不斷的嘗試次數(shù)。而且AFL為了運(yùn)行效率,其搜集的覆蓋率信息犧牲了一定的精度[5],這也導(dǎo)致其無法篩選出某些優(yōu)質(zhì)樣本。因此,目前對于模糊測試的研究主要在于采用一些輔助分析的手段,來提高生成樣本質(zhì)量,從而提高代碼覆蓋率。
基于樣本生成而非樣本修改的方式,文獻(xiàn)[6]實(shí)現(xiàn)了以skyfire命名的工具。該工具通過爬蟲收集互聯(lián)網(wǎng)上目標(biāo)格式的輸入樣本文件,然后通過基于ANTLR的開源語法解析器對這些文本進(jìn)行解析,將這些文件解析成語法樹格式,從而獲得概率語法推導(dǎo)公式以及語法樹中每個(gè)詞法單元對應(yīng)的具體值。獲得這些信息之后,其通過概率語法推導(dǎo)公式導(dǎo)出一棵新的語法樹,然后在相應(yīng)詞法單元中選取收集到的不同數(shù)據(jù)填入,最終生成新的樣本。該方法能夠在保持生成樣本多樣性的同時(shí),保證輸入的語法格式,使其通過程序的結(jié)構(gòu)化校驗(yàn),從而能夠通過程序的語法校驗(yàn)。但是該方法適用性較窄,首先其需要有巨大的樣本量,其次,需要針對程序所處理樣本的格式編寫語法解析器。
基于符號執(zhí)行[7]手段,將程序運(yùn)行中的數(shù)據(jù)進(jìn)行符號化。這樣在執(zhí)行到分支路徑中就能通過求解這些表達(dá)式得到不同的值,從而執(zhí)行不同路徑。文獻(xiàn)[8]基于angr[9],開發(fā)了一款名為Drill的工具,該工具基于具體符號執(zhí)行,收集執(zhí)行過程中的變量約束,利用符號求解器,求解出不同的執(zhí)行路徑所需要的約束值,從而生成對應(yīng)該執(zhí)行路徑樣本。文獻(xiàn)[10]實(shí)現(xiàn)的Qsym,其通過直接作用于x86匯編指令進(jìn)行具體符號執(zhí)行,而非KLEE[11]和angr基于中間語言形式,提高符號執(zhí)行速度;采用具體執(zhí)行,只求解單個(gè)執(zhí)行路徑中的分支,提高符號執(zhí)行可用性。雖然符號執(zhí)行概念很早就提出來,但是其面臨幾個(gè)重要的難題,約束其應(yīng)用范圍。首先是與其它外部環(huán)境代碼交互的問題,雖然能夠通過模擬執(zhí)行,將程序代碼本身進(jìn)行符號化執(zhí)行,但是程序的運(yùn)行還會涉及到與其它庫,系統(tǒng)調(diào)用交互等問題,而符號執(zhí)行無法對系統(tǒng)調(diào)用代碼進(jìn)行模擬。其次,由于符號執(zhí)行可以執(zhí)行不同的分支,每個(gè)分支都需要維護(hù)執(zhí)行時(shí)的相關(guān)符號表達(dá)式信息,同時(shí)分支狀態(tài)數(shù)隨著分支數(shù)量呈幾何級數(shù)增長,這些海量的分支不僅考驗(yàn)計(jì)算能力,也考驗(yàn)存儲能力。最后,為了確認(rèn)是否能夠執(zhí)行某條分支,需要求解符號表達(dá)式。然而表達(dá)式求解屬于NP問題,因此并不能確保能夠求解出需要的值。這些問題都限制了符號執(zhí)行的應(yīng)用。
通過對程序代碼本身的分析,獲得程序的受控輸入點(diǎn)以及關(guān)鍵脆弱點(diǎn)。文獻(xiàn)[12]通過先對程序進(jìn)行詞法語法分析,以此搜集程序的受控點(diǎn)和脆弱點(diǎn),在此基礎(chǔ)上再來進(jìn)行有指導(dǎo)性的模糊測試,提高測試效率。文獻(xiàn)[13]設(shè)計(jì)了一種針對路由器程序的漏洞挖掘方法,其通過靜態(tài)語義分析出脆弱路徑,然后再利用動態(tài)污點(diǎn)分析獲取能夠影響這些路徑的關(guān)鍵輸入字段,在后續(xù)模糊測試過程中對這部分字段進(jìn)行重點(diǎn)變異。然而這種方法需要對被測程序進(jìn)行一定程度的建模,不同類型的程序模型存在差別,因此通用性不高。
本文提出的方法是基于細(xì)粒度污點(diǎn)分析,來獲取輸入中影響程序執(zhí)行的部分來進(jìn)行針對性樣本修改。相比于符號執(zhí)行,其開銷較小,執(zhí)行效率高,適用程序廣泛。文獻(xiàn)[14]實(shí)現(xiàn)的VUzzer,其包含的一個(gè)模塊也是基于污點(diǎn)分析的,但是其將污點(diǎn)分析所獲得的所有信息一次性針對樣本進(jìn)行修改,如果其中某個(gè)修改破壞了前置校驗(yàn),則無法執(zhí)行到后面的校驗(yàn),錯(cuò)過部分執(zhí)行路徑,并且也未考慮通過不斷反饋形成更加優(yōu)質(zhì)的樣本。本文則為污點(diǎn)分析中獲取的每個(gè)不同位置的不同比較各生成一個(gè)樣本,然后再將這些生成樣本作為反饋輸入,進(jìn)行下一輪的樣本生成工作,減少不同字段校驗(yàn)間的干擾。同時(shí)VUzzer使用的污點(diǎn)分析引擎dtracker是基于libdft的實(shí)現(xiàn),通過分析其代碼,發(fā)現(xiàn)其同樣不支持浮點(diǎn)寄存器。然而在現(xiàn)實(shí)代碼中,memcpy、memmove等內(nèi)存拷貝調(diào)用比較常見,而這些函數(shù)在glibc的實(shí)現(xiàn)中都使用浮點(diǎn)寄存器來加快執(zhí)行速度,不支持浮點(diǎn)寄存器十分影響污點(diǎn)分析的精度。本文為了解決這個(gè)問題,實(shí)現(xiàn)了一款輕量級細(xì)粒度污點(diǎn)分析模塊,專注內(nèi)存轉(zhuǎn)移類指令的污點(diǎn)傳播,支持浮點(diǎn)寄存器。雖然VUzzer也是基于細(xì)粒度污點(diǎn)的分析,但是其在考慮生成樣本時(shí)未考慮到大小端轉(zhuǎn)換問題,本文通過分析影子內(nèi)存中污點(diǎn)的來源位置自動識別該字段是否存在字節(jié)轉(zhuǎn)序。另一方面VUzzer其通過識別error handling函數(shù),來為路徑權(quán)重賦值,挑選執(zhí)行路徑權(quán)重和高的種子來進(jìn)行樣本修改。而本文通過利用污點(diǎn)分析時(shí)獲得輸入數(shù)據(jù)的使用頻度信息,同時(shí)考慮控制流信息和數(shù)據(jù)流信息來進(jìn)行種子選擇。
基于路徑覆蓋反饋的模糊測試工具,通過覆蓋率信息來指導(dǎo)樣本修改以及種子選擇。但是其面臨一個(gè)重要的問題,即目標(biāo)樣本需要如何修改才能觸發(fā)不同路徑。傳統(tǒng)的模糊測試工具因此十分依賴于初始種子樣本集合,好的種子樣本集合能夠覆蓋被測程序中大多數(shù)處理輸入的正常路徑,在此基礎(chǔ)上,樣本生成只需依靠比特翻轉(zhuǎn),數(shù)值運(yùn)算等修改策略來觸及錯(cuò)誤處理分支。
在現(xiàn)實(shí)測試中樣本集合的選擇主要考慮目標(biāo)程序接受的輸入格式,根據(jù)格式選擇初始樣本集。然而這種測試方式卻忽視了程序本身的實(shí)現(xiàn)代碼。實(shí)際代碼中可能會支持處理其它格式輸入樣本,也可能實(shí)現(xiàn)一些自定義的格式解析方式。單純的根據(jù)經(jīng)驗(yàn)選擇的方法會錯(cuò)過一些測試點(diǎn)。而且某些冷門的數(shù)據(jù)格式也并不容易收集全面。如果能夠通過對程序進(jìn)行分析,依據(jù)程序處理輸入的方式來針對性進(jìn)行樣本生成,實(shí)現(xiàn)對被程序的敏感,既能減少人工的干預(yù),也能夠生成更符合程序需要的輸入。
基于以上思路,本文實(shí)現(xiàn)的方案是通過污點(diǎn)分析取得的輸入的數(shù)據(jù)流信息,分析出輸入中能夠直接影響程序執(zhí)行路徑的關(guān)鍵數(shù)據(jù)。對樣本中這些數(shù)據(jù)進(jìn)行針對性修改,生成觸發(fā)新路徑的樣本。同時(shí),在生成樣本中按照對輸入使用的頻度,挑選出頻度更高的輸入樣本。通過多次反饋執(zhí)行,使得生成的樣本通過程序的部分校驗(yàn),觸及測試程序深層次邏輯,達(dá)到對程序敏感的目的。以圖1為例,說明程序?qū)斎胛募M(jìn)行的常見校驗(yàn)。
圖1 格式解析型程序示例
首先,程序?qū)ξ募M(jìn)行魔數(shù)校驗(yàn)(magic check),該校驗(yàn)是檢驗(yàn)輸入文件固定位置是否是某個(gè)固定值,初步判斷輸入文件所屬的文件格式類型。然后,通過對輸入文件中的數(shù)據(jù)部分使用哈希算法算出一個(gè)哈希值,與文件中存儲的原始哈希值進(jìn)行比較,來判斷輸入是否受損。當(dāng)這些校驗(yàn)通過后,程序就判定輸入文件符合目標(biāo)格式要求,開始處理目標(biāo)文件,根據(jù)文件當(dāng)中的元數(shù)據(jù)進(jìn)行相應(yīng)數(shù)據(jù)處理。在示例代碼中,由于沒有對長度字段進(jìn)行正確的校驗(yàn),由此觸發(fā)了內(nèi)存越界操作。傳統(tǒng)的模糊測試方法為了通過這些校驗(yàn),需要在正確的校驗(yàn)位置進(jìn)行修改,即使湊巧找到了需要修改的位置,根據(jù)比較字節(jié)的長度,最壞需要28*bytes嘗試次數(shù)。對于圖1中5個(gè)字節(jié)的魔數(shù),即使每秒能嘗試1000次,最壞情況下需要約12 726天(28*5/24*60*60*1000)。而像示例中對數(shù)據(jù)長度大小的比較,雖然傳統(tǒng)模糊測試采用比特翻轉(zhuǎn)的方法能夠很容易觸發(fā)不同分支,但是漏洞的觸發(fā)依賴于取得的最值,通過覆蓋率反饋的方式,無法將取得最值的情況單獨(dú)區(qū)分出來。而針對文件格式解析型的程序,對魔數(shù)和元數(shù)據(jù)使用時(shí),幾乎不會進(jìn)行數(shù)值運(yùn)算,因此,對于這部分的數(shù)據(jù)分析完全可以依賴于污點(diǎn)分析而非借助開銷極大的符號執(zhí)行。
2.1.1 通過監(jiān)控系統(tǒng)調(diào)用引入source點(diǎn)
算法1:系統(tǒng)調(diào)用監(jiān)控的偽代碼
輸入:二進(jìn)制程序,程序的輸入文件
輸出:插樁后二進(jìn)制程序
(1)inspectfds=?//被監(jiān)視文件的打開句柄列表
(2)proceduresys_open_hook(path, flag, ret)//打開文件監(jiān)控
(3)ifpath == inspect_paththen
(4) inspectfds ∪= ret
(5) off[fd] = 0
(6)proceduresys_read_hook(fd, buf, count, ret)//讀取文件監(jiān)控
(7)iffd in inspectfdsthen
(8)fori = 1→ |ret|do
(9) shadow_mem[buf+i]=mktag(fd, off[fd]+i)
(10) off[fd] += ret
(11)proceduresys_lseek_hook(fd, offset, whence, ret)//文件句柄偏移的監(jiān)控
(12)iffd in inspectfdsthen
(13) off[fd] = ret
(14)proceduresys_close_hook(fd, ret)//關(guān)閉監(jiān)控
(15)iffd in inspectfdsthen
(16) inspectfds = fd
(17)apply_hooks(program)
程序讀取輸入文件前,需要調(diào)用sys_open系統(tǒng)調(diào)用進(jìn)行打開操作。因此,對sys_open進(jìn)行hook,然后通過pathname參數(shù)來判斷是否為程序讀入的輸入文件,如果是的話,則記錄返回句柄fd。
程序成功獲取句柄后,可以采用兩種方式將輸入文件中的數(shù)據(jù)載入內(nèi)存當(dāng)中。一種是通過sys_read調(diào)用直接讀取數(shù)據(jù),另一種則是通過sys_map調(diào)用創(chuàng)建一塊對文件映射的內(nèi)存。通過監(jiān)控這兩個(gè)系統(tǒng)調(diào)用,如果句柄參數(shù)是sys_open所返回的,則表明讀入的數(shù)據(jù)來源于輸入文件當(dāng)中,此時(shí)則創(chuàng)建的污點(diǎn)數(shù)據(jù),并且將其存放于對應(yīng)影子內(nèi)存當(dāng)中。
由于本文實(shí)現(xiàn)字節(jié)粒度的污點(diǎn)傳播,污點(diǎn)信息將會存儲污點(diǎn)在文件中的偏移,這就需要維護(hù)文件當(dāng)前指針,才能獲取調(diào)用sys_read時(shí)讀取的污點(diǎn)數(shù)據(jù)在輸入文件中的起始位置。而維護(hù)文件起始指針,則需要對sys_seek進(jìn)行監(jiān)控。當(dāng)讀入污點(diǎn)時(shí),會對每個(gè)污點(diǎn)數(shù)據(jù)創(chuàng)建一個(gè)數(shù)據(jù)結(jié)構(gòu),主要包括存儲污點(diǎn)的來源文件,污點(diǎn)在文件當(dāng)中的位置。然后將該污點(diǎn)結(jié)構(gòu)的指針存入讀入位置的影子內(nèi)存中。最后,當(dāng)調(diào)用sys_close時(shí),則需要刪除對應(yīng)的監(jiān)控句柄,避免引入錯(cuò)誤的污點(diǎn)數(shù)據(jù)。
2.1.2 基于數(shù)據(jù)比較的sink點(diǎn)定位
算法2:sink點(diǎn)操作的偽代碼
輸入:污點(diǎn)信息
輸出:影響程序執(zhí)行的關(guān)鍵污點(diǎn)輸入
(1)procedurecmp_hook(rip, dst_addr, src_addr, size)
(2)if!eflag_used_by_jmp(rip)then//判斷比較指令影響的eflags寄存器是否被條件跳轉(zhuǎn)指令使用
(3) return
(4)ifshadow_mem[dst_addr]then//判斷源操作數(shù)是否涉及污點(diǎn)
(5) fileoff = shadow_mem[dst_addr].off
(6) cmp_info ∪= {rip, fileoff, mem[src_addr], size}
(7)ifshadow_mem[src_addr]then//判斷目的操作數(shù)是否涉及污點(diǎn)
(8) fileoff = shadow_mem[src_addr].off
(9) cmp_info ∪= {rip, fileoff, mem[dst_addr], size}
本文實(shí)現(xiàn)工具的目的就是為了減少傳統(tǒng)模糊測試對初始數(shù)據(jù)集的依賴,利用程序執(zhí)行時(shí)的信息進(jìn)行指導(dǎo)如何對樣本進(jìn)行修改。傳統(tǒng)模糊測試專注于提高程序覆蓋率,因此sink點(diǎn)的位置是能夠影響程序執(zhí)行的指令,同時(shí)該指令也使用污點(diǎn)數(shù)據(jù)。在x86指令集中能夠影響程序執(zhí)行分支跳轉(zhuǎn)的為eflags寄存器,雖然能夠影響eflags寄存器的指令并不唯一,但是程序執(zhí)行過程中,多數(shù)是使用比較指令cmp來改變eflags寄存器的值?;谝陨纤悸?,本文通過監(jiān)控cmp指令,來識別sink點(diǎn)。如果此時(shí)cmp當(dāng)中的某一操作數(shù)包含污點(diǎn)數(shù)據(jù),則表明該位置為有效sink點(diǎn)。
當(dāng)執(zhí)行到sink點(diǎn)時(shí)會做進(jìn)一步檢驗(yàn),首先是對存儲污點(diǎn)中字節(jié)序的判斷,部分格式采用大端進(jìn)行數(shù)據(jù)存儲,而x86處理器則是小端處理器,通過對參與比較的每個(gè)字節(jié)污點(diǎn)來源位置來判斷數(shù)據(jù)的大小端,如果順序反序,則識別其大小端轉(zhuǎn)序。其次,對污點(diǎn)實(shí)際存儲的數(shù)據(jù)和文件中來源數(shù)據(jù)進(jìn)行比較,確保污點(diǎn)數(shù)據(jù)在傳播過程中沒有經(jīng)過運(yùn)算使得與輸入值不符,同時(shí)避免污點(diǎn)傳播中過污染的問題。
當(dāng)順利通過校驗(yàn)后,將sink點(diǎn)相關(guān)信息進(jìn)行記錄,主要包括<指令地址,污點(diǎn)在文件中的偏移,與污點(diǎn)數(shù)據(jù)進(jìn)行比較的數(shù)值,操作數(shù)大小>。這些記錄的信息指導(dǎo)樣本生成,同時(shí)整個(gè)執(zhí)行流中記錄的這些信息則作為種子優(yōu)先級排序時(shí)的依據(jù)。具體將在2.3節(jié)中敘述。
算法3:污點(diǎn)傳播的偽代碼
輸入:二進(jìn)制程序,樣本文件
輸出:被污染的影子內(nèi)存及影子寄存器
(1)proceduremov_rr_hook(dst_reg, src_reg, size)//寄存器到寄存器類型的污點(diǎn)傳播
(2)fori = 1→ |size|do
(3) shadow_reg[dst_reg][i]=shadow_reg[src_reg][i]
(4)proceduremov_mr_hook(dst_addr, src_reg, size)//寄存器到內(nèi)存類型的污點(diǎn)傳播
(5)fori = 1→ |size|do
(6) shadow_mem[dst_addr+i]=shadow_reg[src_reg][i]
(7)proceduremov_rm_hook(dst_reg, src_addr, size)//內(nèi)存到寄存器類型的污點(diǎn)傳播
(8)fori = 1→ |size|do
(9) shadow_ reg[dst_reg][i]=shaow_mem[src_addr+i]
(10)proceduremov_ri_hook(dst_reg, size)//立即數(shù)到寄存器數(shù)據(jù)轉(zhuǎn)移,清除污點(diǎn)
(11)fori = 1→ |size|do
(12) shadow_reg[dst_reg][i]=NULL
(13)proceduremov_mi_hook(dst_addr, size)//立即數(shù)到內(nèi)存數(shù)據(jù)轉(zhuǎn)移,清除污點(diǎn)
(14)fori = 1→ |size|do
(15) shadow_mem[dst_addr+i]=NULL
本文主要監(jiān)控對象為直接使用的污點(diǎn)數(shù)據(jù),并不關(guān)心執(zhí)行過程中參與運(yùn)算的那部分污點(diǎn)。因此,污點(diǎn)傳播的分析主要對涉及內(nèi)存存取的相關(guān)指令進(jìn)行監(jiān)控,在x86的體系架構(gòu)中,主要是mov類型指令,以及push、pop類型指令。mov指令主要是參與一般意義上的內(nèi)存和寄存器以及寄存器和寄存器間的數(shù)據(jù)傳輸,而push、pop則是棧變量和寄存器間的傳輸。通過判斷原操作數(shù)中是否存在污點(diǎn),如果存在的話,則將污點(diǎn)指針考入到目的操作數(shù)對應(yīng)的影子內(nèi)存中,否則清除污點(diǎn)。這樣的設(shè)計(jì)能夠有效減少污點(diǎn)傳播的復(fù)雜度,提高單次污點(diǎn)分析的執(zhí)行效率。還有余下的一小部分在寄存器內(nèi)變換字節(jié)位置的指令(例如bswap、shl、ror等)也需要模擬,以保證污點(diǎn)來源位置的準(zhǔn)確性。
由于模糊測試盡可能要求程序單次執(zhí)行速度,因此輸入樣本大小較小,最終污點(diǎn)傳播的范圍也有限。本文采用哈希數(shù)組的方式實(shí)現(xiàn)影子內(nèi)存而非libdft中類似頁目錄頁表的實(shí)現(xiàn)方式以減少實(shí)現(xiàn)復(fù)雜度。同時(shí),為每個(gè)寄存器分配寄存器長度大小的指針數(shù)組作為影子寄存器。目前的實(shí)現(xiàn)并不支持多線程程序,需要支持的話則需要為每個(gè)線程分配一套影子寄存器。
算法4:污點(diǎn)傳播的偽代碼
輸入:sink點(diǎn)輸出信息
輸出:新生成樣本,排序完成待測試樣本隊(duì)列
(1)proceduregen_new_input(input, cmp_info)
(2) fd = fopen(input, ‘rb’)
(3) inbuf = fd.read()
(4) fd.close()
(5)for{rip, fileoff, value, size} in cmp_infodo//遍歷記錄的污點(diǎn)信息
(6)ifvalue in values[rip]then//做過同樣內(nèi)容的修改,不生成新樣本
(7) continue
(8) values[rip] ∪= value
(9) outbuf = inbuf
(10) outbuf[fileoff] = value
(11) is_new_addr = 0
(12)ifnot{rip, value} in changesthen//判斷是否全新的校驗(yàn)信息
(13) changes ∪= {rip, value}
(14) is_new_addr = 1
(15) output = ‘output’ + str(rip) + str(value)
(16) fd = fopen(output, ‘wb’)
(17) fd.write(outbuf)
(18) fd.close()
(19) priority=is_new_addr*(2**10)+len(cmp_info)//計(jì)算優(yōu)先級
(20) queued_input ∪= {output, priority}//加入新樣本隊(duì)列
樣本生成則依據(jù)sink點(diǎn)時(shí)獲取的<指令地址,污點(diǎn)在文件中的偏移,與污點(diǎn)數(shù)據(jù)進(jìn)行比較的數(shù)值,操作數(shù)大小>四元組信息,來對污點(diǎn)分析的輸入樣本進(jìn)行修改。首先<指令地址,與污點(diǎn)數(shù)據(jù)進(jìn)行比較的數(shù)值>作為唯一性標(biāo)簽,即如果此前修改過相同的二元組,則表示已經(jīng)使用過該種類型的修改,放棄此次修改。采取這種策略的原因是,在程序相同位置做了相同的檢驗(yàn)操作,再次做相同的修改很大程度上也會得到相同的執(zhí)行路徑。
如果不存在,則根據(jù)<污點(diǎn)在文件中的偏移,與污點(diǎn)數(shù)據(jù)進(jìn)行比較的數(shù)值,操作數(shù)大小>三元組信息對文件進(jìn)行修改。具體實(shí)現(xiàn)就是將輸入文件中相應(yīng)的偏移修改為與污點(diǎn)數(shù)據(jù)進(jìn)行比較的數(shù)值。生成新的樣本文件。同時(shí)將<指令地址,與污點(diǎn)數(shù)據(jù)進(jìn)行比較的數(shù)值>記錄下來,避免生成重復(fù)文件。
生成的文件將按照優(yōu)先級排序加入下一輪污點(diǎn)分析的待測樣本中。傳統(tǒng)的模糊測試工具僅僅考慮是否產(chǎn)生新的覆蓋率來作為樣本隊(duì)列的參考,該方案僅僅考慮了程序控制流信息對樣本進(jìn)行篩選,忽略了數(shù)據(jù)流相關(guān)信息,也無法對樣本的優(yōu)先級進(jìn)行排序。本文依靠污點(diǎn)分析獲取的信息來對樣本優(yōu)先級進(jìn)行考量。首先,觀察指導(dǎo)生成樣本修改的指令地址是否首次出現(xiàn),如果是則說明新生成的樣本很有可能觸碰到新的路徑,將其初始優(yōu)先級設(shè)置為1024,否則初始為0。其次統(tǒng)計(jì)樣本參與的不同位置比較的總次數(shù),使用輸入中元數(shù)據(jù)次數(shù)越多,說明對輸入處理的越深入,由于無法預(yù)先知道生成后樣本的這部分信息,因此使用其父樣本中污點(diǎn)分析的信息來進(jìn)行代替。最終優(yōu)先級公式為priority=is_new_addr<<10+compare_counts,按照優(yōu)先級大小插入待測試隊(duì)列。這樣在有新路徑可能性的情況下就會優(yōu)先探索新路徑,同時(shí)考慮到經(jīng)過比較次數(shù)較多的樣本,說明其數(shù)據(jù)經(jīng)過了更多的校驗(yàn),其觸及程序深層次處理邏輯的概率越大,優(yōu)先對這部分樣本進(jìn)行下一輪測試。
基于上述研究,本文設(shè)計(jì)并實(shí)現(xiàn)了taint_fuzz混合模糊測試工具,其框架如圖2所示。
圖2 混合模糊測試工具taint_fuzz框架
污點(diǎn)分析模塊基于intel pin開發(fā),樣本生成和調(diào)度模塊通過編寫python腳本完成,混合執(zhí)行中可選的基于路徑反饋的模糊測試工具采用AFL。系統(tǒng)運(yùn)行過程中,taint_fuzz通過污點(diǎn)分析取得的信息進(jìn)行樣本生成,AFL則根據(jù)路徑反饋進(jìn)行樣本生成,因此需要編譯生成兩個(gè)可執(zhí)行被測程序。一個(gè)是原始程序,被taint_fuzz使用進(jìn)行污點(diǎn)分析,另外一個(gè)是基于源碼插樁以獲取路徑覆蓋的程序。通過共享taint_fuzz和AFL生成的樣本以達(dá)到混合執(zhí)行的目的,taint_fuzz通過污點(diǎn)分析生成符合元數(shù)據(jù)校驗(yàn)的樣本,AFL則通過比特翻轉(zhuǎn)等手段探測分支處理路徑、觸發(fā)漏洞。樣本調(diào)度模塊僅服務(wù)于taint_fuzz,這是考慮到基于覆蓋率反饋的模糊測試工具有其自身的樣本挑選規(guī)則。最后需要說明的是,本文實(shí)現(xiàn)的混合執(zhí)行框架無需程序源代碼也能進(jìn)行測試,taint_fuzz通過pin進(jìn)行插樁,直接作用于二進(jìn)制程序;而AFL也有基于qemu以及intel PT技術(shù)的擴(kuò)展,可以實(shí)現(xiàn)無需源碼的二進(jìn)制插樁。
如圖3所示,taint_fuzz通過python編寫的腳本taint-fuzz.py進(jìn)行統(tǒng)一調(diào)度,首先,調(diào)用pin以及相應(yīng)編寫的taint-fuzz.so動態(tài)插樁庫,進(jìn)行污點(diǎn)分析,再根據(jù)污點(diǎn)分析獲得的結(jié)果來進(jìn)行樣本生成,生成文件名中id代表樣本編號,origid代表父樣本id,off代表修改的位置,str則代表修改的值,該值是字符串化的十六進(jìn)制數(shù)。然后從樣本池中再挑選樣本進(jìn)行下一輪污點(diǎn)分析。
圖3 taint_fuzz運(yùn)行展示
實(shí)驗(yàn)環(huán)境采用處理器為i7-8550U,運(yùn)行內(nèi)存為8 GB,系統(tǒng)為ubuntu18 64位的虛擬機(jī)。為了驗(yàn)證本方法的有效性,通過與VUzzer64,AFL進(jìn)行比較,對audiofile、exiv2、libav這3個(gè)現(xiàn)實(shí)世界中的應(yīng)用程序以及應(yīng)用庫進(jìn)行模糊測試,用來對比生成樣本的代碼覆蓋率,以及挖掘到的有效漏洞數(shù)。其中VUzzer64是VUzzer的升級版本,主要提供64位程序的支持。實(shí)驗(yàn)中的代碼通過gcc 7.4.0編譯,代碼覆蓋率獲取通過編譯時(shí)添加-fprofile-arcs-ftest-co-verage參數(shù),使用gcov,lcov工具獲取。
圖4~圖6是taint_fuzz,VUzzer64,AFL分別對audiofile、exiv2、libav經(jīng)過10小時(shí)的模糊測試所得到的覆蓋率結(jié)果,每30分獲取一次覆蓋率信息。需要說明的是taint_fuzz的初始樣本為1000字節(jié)的a字符串,僅用來使被測程序能夠正常執(zhí)行,以獲得污點(diǎn)分析結(jié)果,即taint_fuzz的初始樣本完全沒有任何樣本格式預(yù)置信息。而為了避免AFL在魔數(shù)檢測部分空轉(zhuǎn),測試中另外為AFL和VUzzer64提供了一個(gè)符合程序解析要求的初始樣本。因此可以看到,在測試的初始的階段,即AFL和VUzzer64的初始樣本池中的樣本代碼覆蓋率要優(yōu)于taint_fuzz,這體現(xiàn)在了覆蓋行數(shù)的起始點(diǎn)。而且VUzzer64在執(zhí)行之前還需要通過其編寫的ida腳本對被測程序進(jìn)行一定的靜態(tài)分析。但是經(jīng)過一段時(shí)間過后,AFL難以嘗試出其它魔數(shù)以及元數(shù)據(jù)的值;VUzzer64的污點(diǎn)分析結(jié)果沒有taint_fuzz的充分,也無法有效利用所獲得的污點(diǎn)數(shù)據(jù),最終代碼覆蓋率被taint_fuzz超越。雖然VUzzer64同時(shí)通過靜態(tài)和動態(tài)手段獲取了更多關(guān)于程序信息,但在初始樣本比較簡單的情況下,其樣本生成效果甚至不如AFL。
圖4 audiofile測試的覆蓋率
圖5 exiv2測試的覆蓋率
圖6 libav測試的覆蓋率
在libav的測試中,VUzzer64主程序沒有獲得污點(diǎn)分析信息,認(rèn)為執(zhí)行出現(xiàn)錯(cuò)誤,拒絕繼續(xù)執(zhí)行。因此測試信息不包含VUzzer64。經(jīng)過人工分析發(fā)現(xiàn),由于VUzzer64的污點(diǎn)分析模塊沒有支持浮點(diǎn)指令集,而libav中自身實(shí)現(xiàn)了一套文件緩存機(jī)制,其通過事先讀取完整輸入樣本,再次讀取時(shí)無需系統(tǒng)調(diào)用,直接調(diào)用memcpy讀取緩存中內(nèi)容即可,造成了VUzzer64無法獲取到污點(diǎn)信息。這也直接印證了前文所述,在memcpy、memmove等調(diào)用頻繁的程序中,缺少對浮點(diǎn)寄存器的支持會對污點(diǎn)分析結(jié)果造成巨大影響。
在表1中列舉了挖掘到的漏洞以及漏洞的類型。其中heap overflow說明堆訪問越界、null reference說明引用了空指針、infinite loop說明程序執(zhí)行了死循環(huán)??梢钥吹?,即使沒有初始樣本,taint_fuzz也能夠生成有效樣本觸發(fā)漏洞,其挖掘到的漏洞數(shù)明顯超過了AFL和VUzzer64。taint_fuzz專注污點(diǎn)分析構(gòu)造有效樣本,然后利用混合模糊測試框架中的傳統(tǒng)模糊測試工具觸發(fā)漏洞;而VUzzer64僅關(guān)注樣本路徑的挖掘,卻沒有采取其它手段進(jìn)行漏洞的觸發(fā),最終也沒有觸發(fā)出任何漏洞。
同時(shí)在本次測試中,挖掘到了多個(gè)首次發(fā)現(xiàn)的漏洞,對此申請了CVE編號,獲得了CVE-2019-13147、CVE-2019-14368、CVE-2019-14369、CVE-2019-14371、CVE-2019-14372。
本文提出了一種自適應(yīng)程序的模糊測試樣本生成方法。與傳統(tǒng)模糊測試相比,taint_fuzz能夠針對性的對樣本進(jìn)行修改生成而非各種嘗試修改。與同樣采用字節(jié)粒度污點(diǎn)分析手段的工具相比,其污點(diǎn)分析結(jié)果更為精確,能夠識別大小端轉(zhuǎn)換,污點(diǎn)信息的利用效果也更好。同時(shí)本文提出了一種基于污點(diǎn)信息使用頻度的樣本優(yōu)先級排序方法,挑選出更有可能觸發(fā)新路徑以及輸入樣本數(shù)據(jù)使用次數(shù)多的樣本,同時(shí)兼顧了控制流和數(shù)據(jù)流信息。目前本方法只能針對沒有經(jīng)過數(shù)值運(yùn)算的數(shù)據(jù)直接修改,后面可以考慮對挖掘到的這些關(guān)鍵字段位置進(jìn)行嘗試性修改。同時(shí)通過污點(diǎn)分析挖掘觸發(fā)漏洞可能性大的字段,例如關(guān)鍵調(diào)用參數(shù),針對這些字段進(jìn)行導(dǎo)向性漏洞挖掘。