王亞昕,李孝慶 ,伍高飛,唐士建,朱亞杰,董 婷
(1.北京空間機電研究所,北京,100094;2.西安電子科技大學 網絡與信息安全學院,陜西 西安 710071)
C語言是一種面向過程的通用程序設計語言。編譯系統的交叉編譯能力使得C語言能夠適用于ARM、C51等多種嵌入式體系架構。由C語言編寫的嵌入式代碼廣泛應用于操作系統內核及驅動、應用程序及代碼庫、單片機軟件等,其使用場景包括移動終端、IoT設備[1]、工控設施、航空航天系統[2]等。這對其安全性和可靠性提出了很高的要求。
因此,針對嵌入式C代碼中的缺陷進行及時檢測和修復極為重要。C代碼中的釋放后重用 (Use-after-Free,UaF) 缺陷極為常見且危害嚴重。雖然不同嵌入式平臺使用的動態內存管理函數各不相同,但是UaF缺陷的成因是相同的,即C代碼在內存釋放之后未將內存指針清零,導致“野指針”留存,并在后續代碼執行中繼續被用來進行相應操作。Android手機所使用的嵌入式內核驅動[3]就曾因一個UaF缺陷導致了華為、三星等諸多廠商的智能手機面臨極為嚴重的安全風險,惡意應用可獲得手機的完全控制權。這一案例充分證明了嵌入式C代碼中UaF缺陷的危害性。
現有的嵌入式代碼缺陷檢測工作未能有效支持UaF缺陷,在一般計算機系統中較為成熟的自動化UaF檢測工具又不能支持復雜多樣的嵌入式平臺。因此,筆者設計了一套支持不同嵌入式平臺的靜態代碼分析工具,實現了對于C代碼UaF缺陷的自動化檢測。主要貢獻如下:
(1) 實現了基于內存指向的、支持數據結構操作的、上下文和路徑約束敏感的過程間數據流分析。
(2) 歸納了UaF缺陷代碼的數據操作和傳遞的特征,并基于數據流分析開展污點追蹤。
(3) 利用大量測試用例與大型嵌入式項目代碼開展驗證性實驗,論證了該工具在發現UaF缺陷的有效性、可靠性及準確性,證明了其在不同架構平臺、大規模代碼項目上的適用性。
C語言具有廣泛而復雜的應用場景。對C代碼UaF缺陷開展檢測有助于提高其在各種應用場景中的可靠性與安全性。現有的代碼缺陷檢測技術多針對操作系統或應用程序等單一場景開展研究,針對嵌入式C代碼UaF的檢測未能得到有效支持。
為了有效利用嵌入式設備的有限計算資源,通常情況下嵌入式系統對于代碼復雜度有較高限定,并會以此來評價軟件的代碼質量。評價方法所涵蓋的軟件度量單元包括控制流節點度量、扇入扇出度量、循環深度度量、圈復雜度等[4]。Mccabe公司的ASM8086,奧吉通公司的CRESTS/ATAT等工具能對此類問題開展基于代碼規范的自動化分析。
從代碼缺陷角度來看:在高安全性高穩定性要求的領域,對于嵌入式軟件的堆棧使用情況的安全測試必不可少,基于匯編代碼的堆棧溢出靜態測試方案可以實現對此類缺陷的自動化測試[5];具體到航天器軟件產品,其常見的代碼缺陷包括變量未初始化、數組越界、整型溢出、操作符優先級錯誤、循環變量錯誤等,使用代碼分析可實現相關缺陷檢測[6]。
常見的C語言UaF檢測工作主要針對計算機系統及其程序,分為動態執行和靜態分析兩種方法。
動態執行以Fuzz測試[7]為代表,利用惡意構造的測試用例觸發并捕獲目標代碼中的異常行為。主要難點在于如何捕獲UaF代碼異常。對于源代碼,可利用編譯框架提供的ASAN選項,進行異常檢測代碼的插裝[8-9];對于二進制代碼,通常需要仿真調試或者二進制指令插裝[10]的方式實現異常監控。動態執行還需解決生成測試用例以觸發更多代碼分支的問題[11-12]。
靜態分析不需運行被測代碼。對于源代碼,通常轉換為中間語言(Intermediate Representation,IR)開展分析,現有工作涵蓋了單線程應用程序[13]和多線程內核驅動[14]中UaF缺陷的檢測方法。針對二進制的靜態分析則需結合反匯編工具[15]。靜態分析的優勢在于理論上能覆蓋代碼所有執行路徑,漏報率低;缺點在于誤報率較高,其主要原因是無效的代碼執行路徑未被識別,通常會引入符號執行[16]工具以降低誤報。
綜合分析相關工作,現有的嵌入式代碼缺陷檢測方案并不能實現有效的UaF檢測;而針對一般計算機C程序的UaF檢測工作雖然已有較多,但其在復雜多樣的嵌入式平臺并不完全適用。因此,設計一種適配多類型嵌入式平臺的UaF檢測工具是十分必要的。
靜態代碼分析能更好適應于多種嵌入式平臺,而動態執行技術則受限于代碼執行和調試環境,適用場景有限。因此,選擇靜態代碼分析中的污點追蹤技術開展UaF自動化檢測。由于C語言是一種直接面向內存的編程語言,其污點分析工具在設計與實現過程中比其他語言更為復雜,須支持:① 內存指向分析,跟蹤內存指針在寄存器和內存之間的傳遞,記錄指針變量指向關系;② 數據結構內部變量追蹤,數據結構是C語言中極為重要和廣泛應用的數據格式;③ 跨函數的過程間追蹤,追蹤函數調用和返回過程的數據傳遞;④ 上下文敏感,以處置基于代碼上下文進行選擇性變量賦值的操作;⑤ 路徑約束敏感,記錄約束條件并求解,防止無效路徑。
基于上述分析,設計如圖 1所示的系統架構。整個分析過程如下:
(1) 將C源碼文件編譯為IR代碼。
(2) 開展直接函數調用圖分析。
(3) 開展跨函數控制流分析。
(4) 開展跨函數數據流分析,并特別針對函數指針、內存指針和路徑約束變量進行數據追蹤。
(5) 基于數據流追蹤,實現間接函數調用分析、指向分析和路徑約束分析。
(6) 基于UaF漏洞特征開展污點追蹤。
可以看出,全面的數據追蹤是進一步開展指向分析、污點追蹤和路徑約束分析的基礎。為了實現準確、全面的數據流分析,定義了如表1中所示的存儲單元和存儲元素,每種存儲單元可存儲任意一種存儲元素。

表1 數據存儲單元與數據存儲元素
數據流分析是實現整個缺陷檢測的核心,以此為基礎可開展全面有效的污點追蹤技術,從而準確判定UaF缺陷是否存在。靜態代碼分析中常見的路徑爆炸、誤報率高等難點也得到了有效處置。

表2 針對特定LLVM IR語句進行數據流分析
數據流分析采用正向分析,沿著執行路徑分析每條語句是否會引起:① 存儲單元之間傳遞了存儲元素;② 新創建的存儲元素被存入了目的存儲單元;③ 存儲單元中的原有存儲元素遭到了覆蓋。表2展示了不同LLVM IR語句所導致的存儲元素從源存儲單元向目的存儲單元的數據傳遞關系。
結合UaF代碼行為特征的污點追蹤技術可實現有效的缺陷檢測。算法1展示了污點源的判定規則。當一條指令進行內存釋放,分析代碼將獲取內存指針對應的內存對象,并為其添加釋放標簽。算法2展示了污點陷入的判定規則。首先獲取語句使用的變量集合,逐一分析其是否為內存指針,并且指向已添加了釋放標簽的內存對象,如是則判斷是否為安全敏感的UaF操作。為了實現完整的污點追蹤:在數據結構內部變量的獲取時,需進行污點傳遞;在內存對象不被其他內存指針引用時,需進行污點消除。
算法1內存釋放的標簽添加過程。
輸入1 待分析函數調用指令callInst。
輸入2 代碼狀態記錄analysisState。
返回值:更新后的代碼狀態記錄analysisState。
① func =獲取callInst 被調函數
② if(func不是內存釋放函數)
③ 返回analysisState
④ freedOpe =獲取被釋放的目的內存寄存器
⑤ freedPointer =從analysisState 中查詢freedOpe 存儲的值
⑥ freedMemoryBlock=從analysisState 中查詢freedPointer 指針指向的內存塊
⑦ freeTag =創建記錄了釋放操作的內存塊標簽
⑧ 向analysisState 中添加記錄:freedMemoryBlock 被打上了freeTag 標簽
⑨ 返回analysisState
算法2釋放后重用導致污點陷入的判定。
輸入1 待分析指令inst。
輸入2 代碼狀態記錄analysisState。
返回值:更新后的代碼狀態記錄analysisState。
① allOpes =獲取inst 的所有操作數寄存器
② for(依次取出allOpes 里每一個的操作數寄存器)
③ ope=本次取出的操作數寄存器
④ value=從analysisState 中查詢ope 存儲的值
⑤ if(value不是一個內存指針
⑥ 進行下一輪循環
⑦ memoryBlock=從analysisState 中查詢value 代表的內存塊
⑧ hasFreeTag=從analysisState 中查詢memoryBlock 是否有內存釋放標簽
⑨ if (hasFreeTag == false)
⑩ 進行下一輪循環
在工具實現過程中面臨著靜態代碼分析工具普遍存在的一些難點。文中以降低漏報率、適度容忍誤報率為原則,對這些難點設計實現了解決方案。
(1) 函數調用圖不完善。直接函數調用分析無法涵蓋復雜代碼中基于函數指針進行間接調用的情況。文中設計了針對函數指針這一特殊的常量型存儲元素的追蹤方法,補全了函數調用圖中的間接函數調用路徑,從而提高了分析過程的準確性和全面性。
(2) 控制流路徑爆炸。路徑爆炸問題主要來源于循環語句、遞歸調用等。通過限制基礎代碼塊在當前函數分析過程中的分析次數、限定代碼執行路徑上每個函數的被執行次數的手段提高了測試的成功率,并進一步利用路徑約束求解降低進入無效代碼路徑的可能性,提高了測試準確性。
(3) 針對數組元素的數據流分析。如果在數組元素的數據訪問過程中索引值為符號值,則文中將嘗試統計目標數組中可訪問范圍內的所有元素,并在后續分析中對于每種取值情況開展數據流分析,從而覆蓋所有可能的取值情況。這樣可確保數據流分析過程的全面性,降低漏報率。
(4) 起始函數設計與測試流程調控。對于一些測試目標,需為其創建虛擬的測試起始函數,實現測試過程調控。例如Linux內核的seq_file文件操作接口,會利用代碼段 1的數據結構對響應函數接口進行設定。
代碼段 1 seq_file文件響應函數的結構定義如下:
① struct seq_operations {
② void * (*start) (struct seq_file *m,loff_t*pos);
③ void (*stop) (struct seq_file *m,void *v);
④ void * (*next) (struct seq_file *m,void *v,loff_t *pos);
⑤ int (*show) (struct seq_file *m,void *v);
⑥ };
⑦ struct seq_operationstest_op;
假設測試目標為名為test_op的該數據結構實例,則測試起始函數的設計如代碼段 2所示,從而模擬進行讀文件操作時的響應流程。此方案可解決在多次內核響應過程中的代碼狀態存留問題,提高準確率。
代碼段 2 針對seq_file的測試起始函數如下:
① void TEST(struct seq_file *m,void *v,loff_t*pos){
② for(i=0;i< 2;i++){
③ test_op.start(m,pos);
④ test_op.next(m,v,pos);
⑤ test_op.show(m,v);
⑥ test_op.stop(m,pos);
⑦ }}
UaF缺陷檢測工具實驗驗證分為兩個部分,第1部分通過自行編寫的和公開的測試用例集合(所用測試用例已開源:http://dwz.date/dn35),驗證該工具發現代碼安全問題的效果和準確性;第2部分則通過有真實漏洞編號的UaF案例,驗證該工具在大型項目上的應用效果。
4.1.1 自有測試用例設計與實驗
在UaF缺陷檢測工具工具的實現過程中,同步編寫了自有測試用例,涵蓋了數據流、調用圖、控制流等多個方面。具體測試內容包括:① 調用圖全面性測試,包括直接調用和間接調用;② 控制流分析,包括路徑分支、代碼循環;③ 數據流分析,包括面向局部變量、全局變量、數據結構、數組元素的數據追蹤。圖2左側展示的為測試用例代碼,右側為測試結果。測試結果以基礎代碼塊為單位,"L:XX"代表源代碼行數,虛線框表示所屬函數。其中代碼塊標注:橢圓,代表分析起點;點狀,代表發生內存申請;橫線,代表發生內存釋放;豎線,代碼發生內存重用。跳轉的標注:C(all) 代表函數調用;Y(es)和N(o)分別代表條件語句為是和否;括號中的編號則標注了代碼執行流程。該結果直接、清晰地呈現了UaF缺陷觸發時的代碼執行路徑。
④ void foo(int argc) {
⑤ char* buf=malloc(10);
⑥ if(buf == NULL)
⑦ return;
⑧ buf[0]=100;
⑨ free(buf);
⑩ if(buf != NULL)

(a)測試用例代碼
4.1.2 開源測試用例集實驗驗證
Juliet測試用例集是軟件保障參考數據庫中的一個公開測試樣本集 ,其中包含138個C代碼UaF缺陷樣本,每個文件代碼量為數百行。利用這些樣本開展了驗證性實驗。實驗結果如表3所示,證明了該工具能以較低的資源消耗完成準確、快速的UaF檢測。限于篇幅,不再對單個用例的測試結果展開分析。

表3 Juliet測試結果統計
選取在嵌入式操作系統領域和應用軟件領域有廣泛應用的Linux操作系統內核和OpenSSL安全通信程序進行驗證。實驗過程使用ThinkPad X1,處理器為英特爾I7-8 750H,設備擁有16 GB內存。
4.2.1 針對嵌入式操作系統漏洞的實驗驗證
Linux內核被廣泛應用于嵌入式系統,其代碼量超過27 000 000行。在4.7.1版本之前的disk_seqf_stop函數存在UaF漏洞[17]。該函數是/proc/diskstats文件的內核響應接口,在內存釋放后未對指針變量seqf->private進行清零,遺留了“野指針”,最終導致UaF觸發。測試過程參考4.3節編寫了針對性的測試起始代碼。
選擇Linux 4.7版本開展測試。實驗過程進行了38分11秒,完成了1 399 020條路徑組合的分析工作。在對無關函數調用進行了自動化“剪枝”后,得到了精簡版的測試結果,如圖3所示。結果顯示disk_seqf_stop函數中被釋放的內存在disk_seqf_next函數中發生了重用。
根據縱線方框的標注,定位disk_seqf_next異常代碼,如代碼段3。此段代碼在第844行進行函數調用,將seqf->private作為調用參數,這一指針正是被disk_seqf_stop釋放的內存。因此確認存在UaF缺陷。
代碼段 3 Linux內核disk_seqf_next實現代碼如下:
839 static void *disk_seqf_next(struct seq_file *seqf,
void *v,loff_t *pos)
840 {
841 struct device *dev;
842
843 (*pos)++;
844 dev=class_dev_iter_next(seqf->private);
…
此外,disk_seqf_start函數在第2次被調用的執行路徑(編號為17-18的有向線段)與第1次調用時是顯著不同的。結合代碼段4中該函數的源碼,可看出成功發現了一條可避免在第2次被調用時seqf->private野指針被覆蓋的執行路徑。這也驗證了此測試報告的準確性。
代碼段 4 disk_seqf_start實現代碼如下:
818 static void *disk_seqf_start(struct seq_file *seqf,…){
820 loff_t skip=*pos;
821 struct class_dev_iter *iter;
822 struct device *dev;
824 iter=kmalloc(sizeof(*iter),GFP_KERNEL);
825 if (!iter)
826 return ERR_PTR(-ENOMEM);
827
828 seqf->private=iter;
…

圖3 Linux內核UaF漏洞的測試結果
4.2.2 針對嵌入式應用軟件的實驗驗證
OpenSSL是一款Linux嵌入式系統上廣泛使用的軟件,代碼量約為450 000行。其1.1.0a版本中存在一個嚴重的UaF缺陷[18]。實驗選取了針對漏洞版本開展測試,選取了以服務程序的讀狀態機實現函數read_state_machine為測試起始點。測試過程持續7分51秒,完成286 567條代碼執行路徑分析。為了驗證結果準確性,設立了對比實驗,基于ASAN異常捕獲機制獲取缺陷動態觸發時的調用棧信息,如圖4所示。將其與圖5中的靜態分析結果比較,可發現兩者的結果相符合。

圖4 OpenSSL UaF漏洞動態觸發調用棧

圖5 OpenSSL軟件UaF漏洞測試結果
在UaF的自動化缺陷檢測領域,靜態分析和動態測試是特點鮮明的兩種方法。兩者并沒有優劣之分,只是因其特點的不同,有著各自的適用場景。表4中總結了在現有研究工作中具有代表性的檢測方法。

表4 現有UaF缺陷檢測方法對比
對比表4中各項工作可發現:動態測試環境更加適用于通用計算機代碼的缺陷檢測,該場景下的代碼執行環境和異常捕獲機制均較為完善,但對嵌入式代碼言并不適用;二進制靜態分析工具受限于其依賴的反匯編工具和符號執行工具的適用范圍,僅能支持部分嵌入式平臺上的缺陷檢測。理論上來講,源代碼分析工具最適用于嵌入式代碼UaF的檢測,但現有工作[13-14]不能實現文中全面的數據流分析,使得開展UaF代碼特征識別的過程中存在較高的誤報率和漏報率,檢測效果并不理想。
筆者提出了一種針對嵌入式C代碼的UaF缺陷檢測方法,并基于LLVM編譯框架編寫了自動化檢測工具,實現了針對操作系統、應用程序、單片機程序等多種嵌入式代碼的UaF缺陷檢測。工具具有全面、準確的數據流分析能力,能夠針對UaF缺陷代碼特征開展污點追蹤,從而實現自動化缺陷檢測和報告輸出。驗證實驗在測試樣本集、嵌入式操作系統和應用程序等多個目標上開展。實驗結果表明,文中方法能夠準確、高效地實現不同場景下UaF缺陷的自動化檢測,并且能適用于大規模嵌入式代碼項目。