孫書彤 王祥武 蔡立志
1(中國銀聯(lián)股份有限公司 上海 201201) 2(中國海洋石油集團有限公司 北京 100010) 3(上海計算機軟件技術開發(fā)中心上海市計算機軟件評測重點實驗室 上海 201112)
近年來,隨著軟件的規(guī)模和復雜度的日益增大,軟件漏洞挖掘技術正逐漸向自動化和智能化演變。傳統(tǒng)漏洞挖掘技術包括模型檢測、二進制比對、模糊測試、符號執(zhí)行以及漏洞可利用性分析等。機器學習和深度學習技術在漏洞挖掘領域也有應用,包括二進制函數(shù)識別、函數(shù)相似性檢測、測試輸入生成、路徑約束求解等,但存在機器學習算法不夠健壯、樣本數(shù)據(jù)不足、算法和特征選擇過度依賴經(jīng)驗和專家知識等問題。
此外,也有利用如Digtool之類的工具進行系統(tǒng)層面漏洞挖掘的方法。Digtool運行后自動記錄內存訪問等行為,進行路徑探測,并盡量引導執(zhí)行更多的路徑;Digtool 會進一步分析,一旦發(fā)現(xiàn)符合主要的一些漏洞行為特征規(guī)則,就意味著找到一個漏洞。
這些方法都是針對某種局部特征或局部環(huán)境進行分析比對的。更重要的是,這些方法都是運行在一種模擬環(huán)境中,而非實際運行環(huán)境[1]。
本文提出一種新的研究思路和實現(xiàn)方案,專門針對操作系統(tǒng)內核中的漏洞進行識別、挖掘和自動封堵,在已知源碼的前提下,能夠自動識別和自動封堵操作系統(tǒng)內核中的幾乎所有控制轉移型漏洞[2-3]。
從運行軌跡角度上看,操作系統(tǒng)內核級漏洞可以分為控制轉移型漏洞和非控制轉移型漏洞兩種。前者是指能夠導致系統(tǒng)運行控制發(fā)生轉移,從而完全脫離原始設計的運行軌跡,去執(zhí)行入侵的代碼或其他超越權限的代碼的漏洞;后者是指不會導致系統(tǒng)的運行軌跡發(fā)生轉移,僅僅能欺騙系統(tǒng),使之放行的漏洞,如:驗證口令、加解密等函數(shù)或流程方面的漏洞[4]。
非控制轉移型漏洞范圍集中,代碼量小,易于研究和消滅。現(xiàn)實中這類漏洞也較少發(fā)生,故本文中不再考慮這類漏洞。本文中研究的漏洞是指控制轉移型漏洞。這些漏洞范圍廣泛,能夠導致系統(tǒng)運行失去控制,去執(zhí)行入侵的代碼或其他超越權限的代碼,是目前漏洞威脅的主體。
本文提出的方案是:通過改造源代碼,將操作系統(tǒng)代碼中的函數(shù)進行函數(shù)名標簽化,對每個函數(shù)增加記錄該函數(shù)運行時的調用序列和上下文關系的一段極小通用代碼,監(jiān)測任何控制轉移型漏洞的發(fā)生,進一步追蹤、定位、分析漏洞,也可以阻塞漏洞代碼的運行[5]。
在已知源代碼的前提下,以函數(shù)為基本單位看待操作系統(tǒng),整個系統(tǒng)的運行過程可以看作一個由這些函數(shù)互相調用構成的運行序列。這個運行序列是完全由源代碼決定的,在運行過程中,除非發(fā)生漏洞入侵或bug,否則不可能出現(xiàn)函數(shù)脫離既定序列運行的情況。反之,通過漏洞入侵本系統(tǒng)的代碼,不可能不改變原來的運行序列,也不可能不調用操作系統(tǒng)的固有函數(shù)。據(jù)此,通過將代碼中的所有函數(shù)進行改造,增加調用者信息和調用序列檢查功能,可以實時發(fā)現(xiàn)函數(shù)脫離原來既定序列運行的情況。這種情況一旦發(fā)生,即可自動阻塞執(zhí)行,并報告發(fā)生脫序運行的“拐點”函數(shù)名稱,所在源代碼的文件名、行號等精確信息,為操作系統(tǒng)的運行安全增加了一種可靠的保護機制[6-10]。
對代碼進行詞法分析,包括隱函數(shù)和虛函數(shù)的提取、識別和變造。
操作系統(tǒng)源代碼均為C語言實現(xiàn)的。根據(jù)C語言的規(guī)范,所有代碼都是由函數(shù)構成的,每個函數(shù)都具有統(tǒng)一的語法特征和詞法特征。因此通過對源代碼的語法分析,可以提取所有函數(shù),并自動改造每個函數(shù),插入函數(shù)級監(jiān)控和分析代碼。
從源代碼中,可以確定函數(shù)之間的調用關系和調用上下文。雖然一個函數(shù)可以被多個函數(shù)調用,一個函數(shù)也可以調用多個函數(shù),包括遞歸調用,但是每個調用必然是在源代碼中明確定義的。從代碼中可以很容易地確定任何函數(shù)的調用者列表和被調用者列表,以及調用序列表。這樣可以建立一個以函數(shù)名為索引的調用關系表。
一個操作系統(tǒng)中所有函數(shù)個數(shù)大致在十萬數(shù)量級以上,每個函數(shù)的平均調用者個數(shù)和被調用者個數(shù)在100以內,所以整個調用關系庫的大小應該在4 MB左右,而且恒定不變,完全可以永久性地放在內存中供函數(shù)調用時實時比對和驗證分析,不會影響系統(tǒng)內存占用。
通過改造每個函數(shù),在函數(shù)頭部插入一小段代碼,比對該函數(shù)的調用者和調用上下文與調用關系庫中是否一致,可以判斷出對該函數(shù)的調用是否合法,進而確定是否有漏洞的存在。再通過全息日志,分析漏洞成因,同時可以阻塞函數(shù)的運行,確保它們不造成危害。
由于函數(shù)名已被標簽化為整數(shù),可以直接作為查找索引去查詢調用關系庫,因此是一個直接定位、無須加鎖的全內存操作,相當于每個函數(shù)多運行幾十條指令而已,故對系統(tǒng)運行效率影響極小。
以源代碼文件為對象,從所有源代碼中識別和提取出所有函數(shù),包括隱函數(shù)、虛函數(shù),形成函數(shù)描述表,描述每個函數(shù)的詳細調用序列關系和調用上下文。
操作系統(tǒng)源代碼一般由C語言編寫,對源代碼文本進行語法分析和詞法分析,識別和提取其中所有的函數(shù),詳細步驟如下:
① 遍歷源代碼所有文件,包括頭文件和.c文件,去掉其中所有注釋信息,以便簡化后繼分析。注釋信息中通常包含文字或代碼等信息,容易帶來語法分析和詞法分析的困擾。
② 逐一遍歷所有文件,根據(jù)C語言函數(shù)的語法分析,標定所有函數(shù)的函數(shù)名、函數(shù)參數(shù)、函數(shù)起止位置。
③ 對所有標定的函數(shù)名統(tǒng)一進行標簽化,即對每個函數(shù)重新命名,命名為統(tǒng)一代號的標簽化函數(shù)名,這樣做一方面可以保證不會發(fā)生重名現(xiàn)象,比如函數(shù)名和變量名重名,另一方面是為后繼查詢提供了快速定位索引信息,流程如圖1所示。

圖1 函數(shù)標簽化流程
④ 經(jīng)過上述三步處理后的源代碼文件,所有函數(shù)均已標簽化、清晰定位、語法分析和詞法分析階段已經(jīng)完成。接著,統(tǒng)計所有函數(shù)的調用關系,生成函數(shù)調用關系庫。具體方法是遍歷所有文件,根據(jù)每個標簽函數(shù)名查找調用者和被調用者,將得到的所有信息索引化處理后,保存入庫,流程如圖2所示。

圖2 函數(shù)調用關系入庫流程
將每個函數(shù)進行源碼變造,在保證不改變原始邏輯的前提下,增加三個參數(shù)和一條語句,用以描述函數(shù)運行時的調用上下文關系、調用序列關系驗證等。
(1) 函數(shù)增加調用參數(shù)。除了main函數(shù)和其他標準函數(shù),所有自行定義的函數(shù)均增加三個參數(shù),分別用來表示調用者、調用者的上級調用者、調用發(fā)生的代碼行。變造后的源碼所有文件都有唯一的編號,這樣,后繼任何函數(shù)在執(zhí)行中出現(xiàn)異常時,都可以輕松定位到源代碼及調用上下文,便于區(qū)分漏洞和異常。
(2) 函數(shù)增加驗證和日志語句。在代碼中每個自定義的函數(shù)頭部增加相同的處理語句,即把新增加的三個參數(shù)傳遞給公共的驗證函數(shù),驗證函數(shù)將根據(jù)這三個參數(shù)進行驗證,如發(fā)現(xiàn)脫序調用,即可阻止該函數(shù)的運行,并報告出錯的函數(shù)及調用上下文。據(jù)此可以追蹤出整個調用序列中的所有調用上下文,分析漏洞或異常的原因。
如圖3所示,一個已標簽化函數(shù)8344在被調用時傳遞三個參數(shù):調用者函數(shù)序號,本函數(shù)所在文件號,行號。在執(zhí)行時首先調用脫序檢查函數(shù)RuntimeCheck,并返回脫序檢查結果。

圖3 源代碼變造實例
(1) 函數(shù)級上下文日志。根據(jù)2.2節(jié)中描述的數(shù)據(jù)結構和算法,實現(xiàn)任意函數(shù)調用時,都會另外增加三個參數(shù),用來指明本次調用的調用函數(shù)、上級調用函數(shù)、調用代碼所在的代碼行。每個函數(shù)被調用時,同時知道它的調用函數(shù)以及調用函數(shù)的調用函數(shù),這就為函數(shù)調用序列的跟蹤和驗證提供了保證。
(2) 函數(shù)級調用存儲。如前所述,任何函數(shù)發(fā)生調用時,根據(jù)調用參數(shù)信息可以進行調用序列驗證和追蹤分析。所有函數(shù)的調用序列關系是由源代碼完全決定的。通過2.1節(jié)中描述的方法,得到如表1所示的函數(shù)調用序列驗證數(shù)據(jù)庫。

表1 函數(shù)調用關系數(shù)據(jù)庫
在所有函數(shù)的最前面插入驗證語句Runtime-Check(),在調用原函數(shù)邏輯功能前,都會調用一個公共的驗證函數(shù)。驗證函數(shù)在初始化時即裝于驗證數(shù)據(jù)庫。該數(shù)據(jù)庫的內容是描述代碼中定義的所有函數(shù)的調用序列關系,因此是靜態(tài)不變的。這就意味著可以公共訪問,無須加鎖。因此這個訪問驗證函數(shù)不會消耗太多的系統(tǒng)CPU,可以始終在線運行。
(3) 拐點函數(shù)的識別。當公共驗證函數(shù)檢測到任何函數(shù)脫序調用時,即可斷定這里發(fā)生了漏洞或BUG,這時可以通過追蹤調用序列,查找調用序列中發(fā)生拐點的函數(shù),以進行漏洞分析,同時也可以直接阻止該函數(shù)的運行,確保漏洞不能被利用。
如圖4所示,五列數(shù)據(jù)分別為函數(shù)號,調用者,所在文件號,行號,檢查結果。一個已標簽化函數(shù)8344返回了脫序檢查結果,即函數(shù)被調用執(zhí)行時,執(zhí)行軌跡沒有脫序。

圖4 內核檢查調用序列輸出實例
本方案改造的操作系統(tǒng)是Linux內核,數(shù)據(jù)庫不到5 MB,運行可以監(jiān)測到由外部輸入觸發(fā)的控制轉移型的漏洞[11]。
經(jīng)過本方案改造的操作系統(tǒng)內核,能夠在函數(shù)粒度上進行全息跟蹤和運行驗證。本文方案能夠自動發(fā)現(xiàn)和阻止利用本系統(tǒng)漏洞攻擊成功的惡意代碼,通過實現(xiàn)整個系統(tǒng)運行全過程的詳細日志,為各類系統(tǒng)運行的安全提供可驗證和追溯的軌跡數(shù)據(jù)。