作者:劉瑞愷 @平安科技銀河實驗室
原文鏈接:基于Unicorn和LibFuzzer的模擬執行fuzzing
之前,銀河實驗室對基于unicorn的模擬執行fuzzing技術進行了研究。在上次研究的基礎上,我們進一步整合解決了部分問題,初步實現了基于Unicorn和LibFuzzer的模擬執行fuzzing工具:uniFuzzer。
關于這項研究的相關背景,可回顧實驗室之前的這篇文章基于 unicorn 的單個函數模擬執行和 fuzzer 實現,這里就不再綴述了。總體而言,我們想要實現的是:
- 在x86服務器上模擬運行MIPS/ARM架構的ELF(主要來自IoT設備)
- 可以對任意函數或者代碼片段進行fuzzing
- 高效的輸入變異
其中前2點,在之前的研究中已經確定用Unicorn解決;輸入的變異,我們調研后決定采用LibFuzzer,并利用其代碼覆蓋率反饋機制,提升fuzzing效率。
在這篇文章中,我們先簡要介紹下Unicorn和LibFuzzer,隨后對模擬執行fuzzing工具的原理進行詳細的分析,最后通過一個demo來介紹工具的大致使用方式。
1. 背景介紹
1.1 Unicorn
提到Unicorn,就不得不說起QEMU。QEMU是一款開源的虛擬機,可以模擬運行多種CPU架構的程序或系統。而Unicorn正是基于QEMU,它提取了QEMU中與CPU模擬相關的核心代碼,并在外層進行了包裝,提供了多種語言的API接口。
因此,Unicorn的優點很明顯。相比QEMU來說,用戶可以通過豐富的接口,靈活地調用CPU模擬功能,對任意代碼片段進行模擬執行。不過,我們在使用過程中,也發現Unicorn存在了一些不足,最主要的就是Unicorn其實還不是很穩定、完善,存在了大量的坑(可以看Github上的issue),而且似乎作者也沒有短期內要填完這些坑的打算。另一方面,由于還有較多的坑,導致Unicorn底層QEMU代碼的更新似乎也沒有納入計劃:Unicorn最新的release是2017年的1.0.1版本,這是基于QEMU 2的,然而今年QEMU已經發布到QEMU 4了。
不過,雖然存在著坑比較多、QEMU版本比較舊的問題,對我們的模擬執行fuzzing來說其實還好。前者可以在使用過程中用一些臨時方法先填上(后面會舉一個例子)。后者的影響主要是不支持一些新的架構和指令,這對于許多IoT設備來說問題并不大;而舊版本QEMU存在的安全漏洞,主要也是和驅動相關,而Unicorn并沒有包含QEMU的驅動,所以基本不受這些漏洞的影響。
1.2 QEMU
關于QEMU的CPU模擬原理,讀者可以在網上搜到一些專門的介紹,例如這篇。大致來說,QEMU是通過引入一層中間語言,TCG,來實現在主機上模擬執行不同架構的代碼。例如,如果在x86服務器上模擬MIPS的代碼,QEMU會先以基本塊(Basic Block)為單位,將MIPS指令經由TCG這一層翻譯成x86代碼,得到TB(Translation Block),最終在主機上執行。
而為了提高模擬運行的效率,QEMU還加入了TB緩存和鏈接機制。通過緩存翻譯完成的TB,減少了下次執行時的翻譯開銷,這即就是Unicorn所說的JIT。而TB鏈接機制,則是把原始代碼基本塊之間的跳轉關系,映射到TB之間,從而盡可能地減少了查找緩存的次數和相關的上下文切換。

值得一提的是,Unicorn所提供的hook功能,就是在目標代碼翻譯成TCG時,插入相關的TCG指令,從而在最終翻譯得到的TB中,于指定位置處回調hook函數。而由于TCG指令和架構無關,因此添加的TCG指令可以直接適用于不同架構。
1.3 LibFuzzer
LibFuzzer應該許多人都不陌生,這是LLVM項目中內置的一款fuzzing工具,相比我們之前介紹過的AFL,LibFuzzer具有以下優點:
- 靈活:通過實現接口的方式使用,可以對任意函數進行fuzzing
- 高效:在同一進程中進行fuzzing,無需大量
fork()進程 - 便捷:提供了API接口,便于定制化和集成
而且,和AFL一樣,LibFuzzer也是基于代碼覆蓋率來引導變異輸入的,因此fuzzing的效率很高。不過,這兩者都需要通過編譯時插樁的方式,來實現代碼覆蓋率的跟蹤,所以必須要有目標的源代碼。接下來,在uniFuzzer的原理中,我們會介紹如何結合Unicorn和LibFuzzer的功能,對閉源程序進行代碼覆蓋率的跟蹤反饋。
2. uniFuzzer原理
uniFuzzer的整體工作流程大致如下:
- 目標加載:在Unicorn中加載目標ELF和依賴庫,并解析符號
- 設置hook:通過Unicorn的基本塊hook,反饋給LibFuzzer代碼覆蓋率
- 準備環境:設置棧、寄存器等信息
- fuzzing:將Unicorn的模擬執行作為目標函數,開始LibFuzzer的fuzzing
下面對各環節進行具體的介紹。
2.1 目標加載
遇到的許多IoT設備,運行的是32位MIPS/ARM架構的Linux,所以我們初步設定的目標就是這類架構上的ELF文件。
如實驗室之前對模擬執行研究的那篇文章中所講,我們需要做的就是解析ELF格式,并將LOAD段映射到Unicorn的內存中。而在隨后的研究中,我們發現目標代碼往往會調用其他依賴庫中的函數,最常見的就是libc中的各類C標準庫函數。通過Unicorn的hook機制,倒是可以將部分標準庫函數通過非模擬執行的方式運行。但是這種方式局限太大:假如調用的外部函數不是標準庫中的,那么重寫實現起來就會非常麻煩。所以,我們還是選擇將目標ELF的全部依賴庫也一并加載到Unicorn中,并且也通過模擬執行的方式,運行這些依賴庫中的代碼。
那么,以上所做的,其實也就是Linux中的動態鏈接器ld.so的工作。Unicorn本身并不包含這些功能,所以一種方式是由Unicorn去模擬執行合適的ld.so,另一種方式是實現相關的解析代碼,再調用Unicorn的接口完成映射。由于后一種更可控,所以我們選擇了這種方式。不過好在ld.so是開源的,我們只需要把相關的代碼修改適配一下即可。最終我們選擇了uClibc這個常用于嵌入式設備的輕量庫,將其ld.so的代碼進行了簡單的修改,集成到了uniFuzzer中。
由于我們集成的是ld.so的部分功能,導入函數的地址解析無法在運行時進行。因此,我們采取類似LD_BIND_NOW的方式,在目標ELF和依賴庫全部被加載到Unicorn之后,遍歷符號地址,并更新GOT表條目。這樣,在隨后的模擬執行時,就無需再進行導入函數的地址解析工作了。
集成ld.so還帶來了一個好處,就是可以利用LD_PRELOAD的機制,實現對庫函數的覆蓋,這有助于對fuzzing目標進行部分定制化的修改。
2.2 設置hook
接下來需要解決的一個重要問題,就是如何獲取模擬執行的代碼覆蓋率,并反饋給LibFuzzer。LibFuzzer和AFL都是在編譯目標源碼時,通過插樁實現代碼覆蓋的跟蹤。雖然LibFuzzer的具體插樁內容我們還沒有分析,但是之前對
而這個數組,記錄的就是每個跳轉,如 回到我們的fuzzing工具。如之前所說,LibFuzzer和AFL之所以需要目標的源碼,是為了在編譯時,在跳轉處插入相關的代碼,而跳轉正好對應的就是基本塊這一概念。恰巧,Unicorn提供的hook接口中,也包含了基本塊級別的hook,可以在每個基本塊被執行之前,回調我們設置的hook函數: 另一方面,通過搜索相關資料,我們發現在LibFuzzer中還神奇地提供了這樣一個機制, 可見,類似于AFL,通過一個記錄跳轉發生次數的數組,就可以作為代碼覆蓋率的信息。作為用戶,我們只需要按照格式,聲明這樣一個數組,并在每次跳轉時,更新相應下標處的內容,就可以輕松地將覆蓋率信息反饋給LibFuzzer了。 綜合以上信息,我們得出了下面的方案: 其中第2點,為基本塊(即分支)生成一個隨機數,AFL是在編譯插樁時就生成這樣的隨機數并硬編碼的。對于Unicorn來說,如果要實現這樣的效果,必須修改Unicorn的源碼,在基本塊翻譯時加入相應的TCG指令。但這樣做對Unicorn本身的改動比較大,所以最終我們還是選擇通過hook的方式,而盡量不去魔改Unicorn破壞通用性。具體地,我們是將基本塊的入口地址計算CRC16哈希,作為其對應的隨機數。 現在,目標已經加載到Unicorn中,代碼覆蓋率反饋也已經實現,接下來就只需要準備運行環境了。通過Unicorn的接口,我們可以映射出棧、堆、數據等不同的內存區域,并根據目標代碼的需求,設置好相應的寄存器值。 另外,如之前所說,我們移植的 準備工作到這里已經完成,接下來就可以fuzzing了。使用LibFuzzer,需要用戶實現 由于fuzzing時所模擬運行的目標代碼片段恒定不變,因此QEMU的JIT機制可以有效地提升運行效率。然而,起初我們測試時,卻發現并不是這樣:每一輪的模擬執行,都會重新翻譯一遍目標代碼。經過分析代碼,我們發現這是Unicorn的一個坑:為了解決基本塊中單步執行遇到的某個問題,Unicorn引入了一個臨時解決方案,即在模擬執行停止后,清空QEMU的TB緩存。因此,第二輪模擬執行時,即使是同一段代碼,由于緩存被清空,還是需要再重頭開始翻譯。為了恢復性能,我們需要再注釋掉這個臨時方案,重新編譯安裝Unicorn。 我們整理了上述研究結果,實現了一套概念驗證代碼:https://github.com/rk700/uniFuzzer ,其中包含了一個demo。下面我們就以這個demo為例,再次介紹整個fuzzing的運行流程。 demo-vuln.c是要進行fuzzing的目標,其中包含了名為 可以看到,輸入的內容未檢查長度,就直接 接下來,我們將這段代碼編譯成32位小端序的MIPS架構ELF。首先我們需要mipsel的交叉編譯工具,在Debian上可以安裝 將其編譯得到ELF文件demo-vuln。我們要fuzzing的目標,就是其中的 由于 我們需要在Unicorn中分配一片內存作為堆,然后每次 接下來,我們將包含上述preload函數的demo-libcpreload.c,也編譯成與demo-vuln同樣架構的ELF動態庫: 現在,目標ELF和preload庫都已經準備完成,接下來就需要編寫相關代碼,設置好模擬執行的環境。uniFuzzer提供了以下幾個回調接口: 用戶通過在目錄 最終,在代碼根目錄下運行 相關的參數是通過環境變量傳遞的。 下圖是一個fuzzing觸發的崩潰: 可以看到,uniFuzzer檢測到了堆溢出。觸發漏洞的,是長度68 bytes的字符串,當其被 下圖是另一個fuzzing觸發的崩潰: 這次的輸入只有1個字符, 通過結合Unicorn和LibFuzzer的功能,我們實現了對閉源代碼的fuzzing。上述開源的uniFuzzer代碼其實還屬于概念驗證階段,許多功能例如系統調用的支持、其他架構/二進制格式的支持等等,還需要后續進一步完善。也歡迎在這方面有研究的小伙伴多提建議和PR,進一步完善功能。
A->B,所發生的次數。AFL以此數組作為代碼覆蓋率的信息,進行處理,并指導后續的變異。
__libfuzzer_extra_counters:
uint8_t類型的數組2.3 準備環境
ld.so支持通過PRELOAD的方式,覆蓋掉要模擬執行的庫函數。比如說,目標代碼中調用的某些庫函數是不必要的,而且由于Unicorn不支持系統調用,所以像printf()這類IO輸出的庫函數,就可以通過PRELOAD的方式忽略掉,而不影響代碼的正常運行。當然,編譯的preload庫,需要確保其和目標ELF是同一架構、同一符號哈希方式,才能被正確地加載到Unicorn中。2.4 運行fuzzing
LLVMFuzzerTestOneInput(const uint8_t *data, size_t len)這個函數,在其中調用要fuzzing的函數,在這里即就是目標代碼的Unicorn模擬。根據LibFuzzer生成的輸入和其他環境信息,Unicorn開始模擬運行指定的代碼片段,并將代碼覆蓋率通過extra counters數組反饋給LibFuzzer,從而變異生成下一個輸入,再次開始下一輪模擬運行。3. 示例
vuln()的函數,存在棧溢出和堆溢出:
strcpy()到堆上;另外,輸入內容的第一個字節作為長度,memcpy()到棧上。gcc-mipsel-linux-gnu這個包。接下來運行mipsel-linux-gnu-gcc demo-vuln.c -Xlinker --hash-style=sysv -no-pie -o demo-vulnvuln()函數。demo-vuln提供了源代碼,所以我們看到在vuln()函數中,還調用了printf(), malloc(), strcpy(), memcpy(), free()這些標準庫函數。其中printf()如之前所說,可以通過PRELOAD的機制來忽略掉;strcpy()和memcpy(),可以繼續模擬執行mipsel架構的libc中的實現;比較復雜的是malloc()和free(),因為一般來說malloc()需要brk()的系統調用,而Unicorn還不支持系統調用。所以,我們也重新寫了一個非常簡單的堆分配器,并通過PRELOAD的方式替換掉標準庫中的實現:
malloc()調用,就直接從這片內存中切一塊出來。而為了檢測可能發生的堆溢出漏洞,我們參考棧溢出檢測的機制,在malloc()分配的內存末尾加上一個固定的canary,并在頭部寫入這塊內存的大小,以便后續檢查。free()也被簡化為空,因此不需要進行內存回收、合并等復雜操作。mipsel-linux-gnu-gcc -shared -fPIC -nostdlib -Xlinker --hash-style=sysv demo-libcpreload.c -o demo-libcpreload.so
void onLibLoad(const char *libName, void *baseAddr, void *ucBaseAddr): 在每個ELF被加載到Unicorn時回調int uniFuzzerInit(uc_engine *uc): 在目標被加載到Unicorn之后回調,可以在這里進行環境的初始化,例如設置堆、棧、寄存器int uniFuzzerBeforeExec(uc_engine *uc, const uint8_t *data, size_t len): 每輪fuzzing執行前回調int uniFuzzerAfterExec(uc_engine *uc): 每輪fuzzing執行完成后回調callback/中編寫.c代碼,實現上述回調函數,進行fuzzing。針對demo-vuln,我們也編寫了一個callback/demo-callback.c文件作為參考。make,即可編譯得到最終的fuzzing程序uf。運行以下命令,開始fuzzing:UF_TARGET=<path to demo-vuln> UF_PRELOAD=<path to demo-libcpreload.so> UF_LIBPATH=<lib path for MIPS> ./ufUF_TARGET是要fuzzing的目標ELF文件,UF_PRELOAD是要preload加載的自定義ELF動態庫,UF_LIBPATH是依賴庫的搜索路徑。在Debian上安裝libc6-mipsel-cross這個包,應該就會安裝所需的mipsel庫,此時依賴庫的搜索路徑就在/usr/mipsel-linux-gnu/lib/。
strcpy()到長度為60 bytes的堆時,canary的值被修改,最終被檢測發現。
\xef。其被作為memcpy()的參數,復制了超長的內容到128 bytes的棧變量上,從而修改了vuln()函數返回地址,觸發了內存訪問錯誤。4. 總結
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1002/
暫無評論