肖順陶,周安民,劉 亮,賈 鵬,劉露平
(四川大學電子信息學院,成都610065)
(*通信作者電子郵箱906120662@qq.com)
底層虛擬機混淆器(Obfuscator Low Level Virtual Machine,OLLVM)[1]是瑞士西部應用科技大學信息安全小組于2010年6月發起的一個項目,該項目的目的是提供一個基于LLVM編譯套件的開源工具,通過代碼混淆和防篡改來提高軟件安全性。憑借其出色的混淆效果、自定義的混淆方案、不依賴編程語言和平臺架構等特性,OLLVM在軟件安全領域得到了廣泛運用。惡意軟件開發者也逐步在自己的軟件中使用該混淆技術,以增加安全人員的分析難度,防止自己的惡意軟件被破解。由于目前關于OLLVM的資料比較少,加上反混淆受OLLVM保護的程序難度較大,極少數大型殺毒公司雖有相應的OLLVM反混淆方案,但出于商業目的,并未開源自己的反混淆方案,因此實現OLLVM反混淆對于保護軟件用戶的合法權益和維護健康的軟件安全生態環境都具有重要的現實意義,也是一個亟需解決的問題。
在OLLVM反混淆方面,目前多采用基于符號執行的方法來消除控制流平展化[2]。該方面研究比較出色的是國外的Quarkslab團隊,該團隊提出了一種基于Python的逆向工程框架 Miasm[3],其支持可執行的可移植(Portable Executable,PE)文件、可執行與可鏈接格式 (Executable and Linkable Format,ELF)等多種文件格式解析,并且支持x86、高級精簡指令系統處理器(Advanced RISC Machines,ARM)、無互鎖管道微處理器(Microprocessor without Interlocked Piped Stages,MIPS)等多種架構平臺,可以通過中間表示(Intermediate Representation,IR)[4-5]表征匯編指令語義,并使用 JIT(Just In Time)[4]技術進行代碼的模擬執行。
Miasm雖然為目前最出色的針對OLLVM的反混淆工具,但其依然存在許多問題,如反混淆后的圖形顏色粗陋、基本塊中的IR中間表示晦澀難懂、反混淆后的圖形無法反編譯恢復程序源碼等。針對這些缺點,本文在研究符號執行工具angr[6]和二進制分析逆向工程框架(Binary Analysis and Reverse engineering Framework,BARF)[7]逆向框架的基礎上,在x86架構的Linux平臺下,提出了一種基于符號執行的OLLVM自動化反混淆框架。該框架以C/C++文件經OLLVM混淆后得到的ELF文件作為輸入,利用BARF框架進行反匯編和基本塊的分析,并結合自定義的基本塊識別算法確定序言、主分發器、相關塊、預分發器、返回塊等基本塊;接著,使用符號執行工具angr從各基本塊起始地址開始符號執行,確定真實的可達路徑以及各基本塊之間的前后關系;最后,修復二進制程序,包括使用無操作(No OPeration,NOP)指令填充無用基本塊、有用塊(包含序言、相關塊、返回塊)指令替換、有用塊跳轉偏移量修正這三個方面。由于所有的修改都是直接針對混淆后程序的匯編代碼進行的,因此經過反混淆后,最終得到一個可執行文件,并能實現反編譯恢復出未混淆程序的原始代碼。
控制流平展化(Control Flow Flattening)[8]思想最先由美國維吉尼亞大學的Wang等[9]提出,早在2008年便有學者將控制流平展化技術運用于C/C++代碼的保護[2]。控制流平展化是在不改變源程序功能的前提下,將C/C++代碼中的if/while/for/do等控制語句轉換為等價的switch分支結構,以消除case代碼塊之間原有的邏輯調用關系,使得所有代碼塊看起來都為平行關系,從而達到對程序控制流混淆的目的。
控制流平展化的思想是將源程序函數分成一個入口塊和多個相鄰的基本塊(case代碼塊),每個基本塊都有相應編號,并且這些基本塊都有相同的前驅和后驅模塊。前驅模塊也叫主分發器,通過表征程序狀態的控制變量(switch變量)來完成基本塊的分發,這使得基本塊的分支目標不能輕易地通過靜態分析方式來確定,從而阻礙逆向分析者對程序的理解,增加逆向分析者分析時間,其模型如圖1所示。

圖1 控制流平展化模型Fig.1 Control flow flattening model
目前OLLVM提供了三種混淆編譯選項,本文主要使用控制流平展化選項,其可以使用-mllvm、-perFLA兩個參數來自定義控制流平展化程度,如設置參數-perFLA的值為100對源程序所有函數開啟控制流平展化保護。
符號執行(symbolic execution)[10-11]就是把程序輸入和程序變量都視為符號,并在符號執行時,更新每個變量的符號表達式。本文提出的反混淆方案使用angr符號執行框架,angr是一款基于python的二進制文件分析框架,兼具靜態分析和動態符號執行功能,其由 angr、simuvex、claripy、cle、pyvex、archinfo[6]六大模塊組成,本文主要使用前五個模塊。
angr模塊是angr框架的分析和協調中心模塊,用于完成二進制文件的分析和路徑探索,本文提出的反混淆框架使用了Project/Factory/Path這3個容器。其中:Project容器用于給待分析的二進制文件創建一個angr的Project對象;Factory容器有多種類型,如:factory.block、factory.blank_state 等,本文則使用相應的factory類型來獲取程序執行過程中生成的SimState對象。SimState對象跟蹤并記錄著程序動態執行過程中的符號信息、符號對應的寄存器信息和符號對應的內存信息等;Path容器用于存放程序動態執行過程中的path對象,本文使用factory.path(state)來獲取以指定程序狀態state為起點的path對象,該path對象代表從state狀態起點到程序執行終點的完整路徑信息。
simuvex模塊是angr框架的VEX IR模擬執行引擎,負責記錄程序運行狀態并進行代碼模擬執行。在給定VEX IRSB(IR基本塊)和內存、寄存器的初始狀態后,它可以進行靜態分析和動態符號執行。
claripy模塊是angr框架的求解引擎,它將程序中的一些狀態用符號表示,并將程序的每一個路徑都翻譯為一個邏輯表達式,形成表達式樹(expression tree)[12],最終得到一個非常大的路徑公式,在完成公式求解后,就能得到覆蓋所有路徑的輸入變量。本文提出的反混淆框架主要使用了claripy的位向量值(Bit Vector Value,BVV)構造函數claripy.BVV(),用于指定表達式中臨時變量的值,從而使程序選擇不同的分支進行符號執行。
cle模塊是angr框架的文件解析加載器,angr使用一個精簡的加載器“CLE Loads Everything”[6],該加載器并非完全精確,但是能加載ELF/ARM等可執行文件。
pyvex模塊則為angr框架提供了將二進制代碼轉換為VEX IR中間表示的接口。
BARF是一個基于Python的,支持多平臺和二進制指令轉換為中間表示的逆向工具,其主要由 Core、Arch、Analysis[7]三個模塊組成。本文主要使用BARF對混淆后的二進制文件進行反匯編、生成包含逆向工程中間語言(Reverse Engineering Intermediate Language,REIL)[5]表示的控制流圖(Control Flow Graph,CFG)[13],并提供相應的 IR 基本塊分析功能。
Core模塊是BARF框架的核心模塊,它定義了REIL中間表示、可滿足性理論(Satisfiability Modulo Theories,SMT)求解器和二進制接口(Binary Interface,BI)。
Arch模塊指出了該框架所支持的平臺架構,目前支持x86和arm兩種平臺。
Analysis模塊是本文提出的反混淆框架的核心功能模塊,主要由 Basic Block和 Code Analyzer兩部分組成,Basic Block主要用于對生成的CFG進行分析,如獲取基本塊地址等,Code Analyzer主要用于獲取相應的 SMT[14-15]表達式。
要實現OLLVM的反混淆,主要面臨以下三個問題:
1)識別出有用塊和無用塊。經OLLVM控制流平展化混淆的程序中會增加很多無用的基本塊以混淆程序邏輯,這就需要設計有效的基本塊識別算法,找出有用的基本塊和無用的基本塊。
2)確定有用塊之間的前后關系,得到真實有效的程序執行路徑,因為混淆程序中的很多基本塊跳轉邏輯并不是程序的實際執行流程。
3)修復二進制程序。在使用NOP指令填充無用基本塊后,為使程序正常運行,我們需要對跳轉指令的跳轉偏移量進行修正;同時,還需要將cmov條件傳送指令改寫成相應的條件跳轉指令,并在其后添加一條jmp指令,使其跳向另一分支。
圍繞以上三個問題,本文提出了一種基于符號執行的OLLVM自動化反混淆框架,其架構如圖2所示。

圖2 OLLVM自動化反混淆框架架構圖Fig.2 Architecture diagram of OLLVM automatic deobfuscation framework
為了得到有效的反混淆結果,需要找到混淆程序中的有用塊和無用塊,這里的有用塊是指對恢復原始控制流圖有用的基本塊,包含序言、相關塊、返回塊,而無用塊是指OLLVM混淆后添加的無用基本塊,詳細定義如下:
1)序言塊:函數的起始塊;
2)主分發器:序言塊的直接后繼塊;
3)預分發器:后繼為主分發器的塊(非序言塊);
4)相關塊:后繼為預分發器的塊;
5)無后繼的塊為返回塊;
6)剩下的為無用塊。
其次,BARF框架Analysis模塊中的Basic Block子模塊提供了CFG控制流圖恢復功能,由該模塊即可獲得基本塊的首地址、基本塊指令、基本塊分支等信息,因此可結合BARF框架的基本塊分析功能設計相應的基本塊識別算法。在對OLLVM控制流平展化混淆策略進行深入研究后,根據上述基本塊定義,設計了如下的基本塊識別算法:算法中 cfg為BARF框架解析輸入文件后得到的程序控制流圖;blocks代表程序控制流圖中的所有基本代碼塊;predispatcher_Retn(cfg)用來獲取返回塊、預分發器的首地址;relevant_NOP_Blks(cfg)用來獲取相關塊、無用塊的首地址,算法具體描述如下。
輸入 OLLVM混淆后的ELF文件;
輸出 各基本塊首地址。
Begin
序言←sys.argv[2] /* 序言塊首地址*/
主分發器←序言直接后繼
function predispatcher_Retn(cfg)
for each block∈blocks do
if block沒有分支then
返回塊←block
elif block直接后繼為主分發器then
預分發器←block
end if
end for
return預分發器,返回塊
end function
function relevant_NOP_Blks(cfg)
相關塊←[]
無用塊←[]
for each block∈blocks do
if block直接后繼為預分發器and
block指令條數>2 then
添加block到相關塊
elif block非序言非返回塊then
添加block到無用塊
end if
end for
return相關塊,無用塊
end function
End
相比Miasm,本文提出的基本塊識別算法,采用了更嚴格的相關塊定義(指令數大于2條的后繼為預分發器的塊才是相關塊,指令數小于等于2條的后繼為預分發器的塊只是完成到預分發器的跳轉,對恢復程序控制流圖沒有意義,Miasm在后續處理中也刪除了這些無用的相關塊),因而能在保證基本塊完備性的前提下,得到真實有用的相關塊。
由于在運行框架之初就會給定混淆文件特定函數入口地址(即序言塊首地址),而且經OLLVM混淆后,序言塊只有1個后繼塊,因此通過BARF框架獲得的序言塊直接后繼塊地址、以及沒有后繼分支的基本塊地址(分別對應主分發器首地址、返回塊首地址)都是正確的;依此類推,后續得到的基本塊地址也都是正確的,這就保證了所得基本塊首地址的正確性。
綜上,經過上述算法的處理,即可得到完備的、正確的有用塊和無用塊的首地址列表,這為下一步的符號執行做好了準備。
OLLVM將源程序的控制流打亂了,因此需要找到真實的控制流,這里采用符號執行的方式進行路徑探索,確定各個有用塊之間的前后順序以及分支關系。
雖然Miasm和angr兩種符號執行引擎都能得到混淆程序的控制流字典變量{父節點:(子節點集合)},但Miasm所獲得的控制流字典變量中,當父節點有2個子節點(即有分支)時,并不能看出子節點是滿足條件的分支,還是不滿足條件的分支,必須結合其符號執行的約束條件才能判斷;而本文基于angr的符號執行模塊,在符號執行過程中,當遇到有分支的相關塊時,首先將存在分支的指令保留下來,然后利用angr的求解引擎Claripy設置兩個1 b的位向量值〈BV1 1〉、〈BV1 0〉,分別進行符號執行,從而得到父節點的滿足條件的左子節點和不滿足條件的右子節點,即意義明確的控制流字典變量{父節點:(子節點集合)}。由于基本塊識別模塊已經得到完備的正確的有用塊地址列表,因而能保證符號執行后得到正確的有用塊之間的拓撲關系,詳細實現過程如下:
首先,利用符號執行工具angr,從基本塊識別模塊得到的有用塊地址列表中的每一個地址(除去返回塊首地址,因為返回塊沒有后續分支)處開始符號執行,一旦遇到cmov類型的條件傳送指令,就將該基本塊指令保存下來,并設置基本塊分支標志位has_branches為True,并且通過angr的求解引擎Claripy設置兩個1 b的位向量值〈BV1 1〉、〈BV1 0〉,分別代表條件滿足和條件不滿足時的約束條件,用于在符號執行過程中改變臨時變量的值,從而強行使程序走右側條件滿足時的分支或者走左側條件不滿足時的分支,以達到完整路徑遍歷的目的;接著,在符號執行函數 symbexec()中,調用BP_inspect()函數監控angr IR基本塊中是否出現ITE(If-Then-Else)[16]條件表達式,一旦出現ITE條件表達式便在此處設置斷點,并依次將ITE條件表達式中臨時變量的值修改為〈BV1 1〉、〈BV1 0〉,繼續符號執行,分別得到左右兩側不同分支對應的基本塊首地址。當依次完成有用塊地址列表中地址的遍歷后,就能得到有用塊之間的前后順序及其分支關系了。
另外,如果遇到call指令,則將該call指令地址保存下來作為hook函數的hook地址,并使用hook的方式直接返回,而不去執行call指令調用的子程序,因為我們更關注的是該基本塊的整體輪廓。經過上述步驟后,即可得到有用塊之間正確的拓撲關系,但無用的基本塊仍然存在,為了還原程序真實的控制流圖,還需使用NOP指令填充無用的基本塊,并完成有用塊的指令修復,這就是指令修復模塊的主要工作。
混淆后的程序從一個有用塊跳轉至下一個有用塊時,它們之間隔著許多無用的基本塊,因此在使用NOP指令填充無用塊后,還需修正跳轉指令的跳轉偏移量,使其能正常跳轉到下一個有用塊,所以指令修復模塊主要完成無用塊指令填充、有用塊指令修復兩方面的工作。
在指令修復模塊中創新性地使用指令修復技術,通過對無用塊進行NOP指令填充;對有用塊(含直接跳轉塊(只有一個子節點)、有分支塊(有兩個子節點))進行指令替換、指令填充、偏移修正,最終得到一個可執行的反混淆文件,從而克服了Miasm由于反混淆后的結果是一種圖片、無法進行反編譯等缺點。由于符號執行模塊已經得到了有用塊之間正確的拓撲關系并且保存下了有分支的指令,因此可依據符號執行結果進行指令修復工作,并能確保指令替換、偏移修正工作的有效性,從而保證反混淆結果能正常運行,并完成對程序語義的完整還原,得到正確的反混淆結果。
為此,本文提出的反混淆框架編寫了2個具有通用性的指令修復函數以自動完成指令修復工作。nop_padding()用于向指定地址填充0x90;jmp_padding()用于填充新的跳轉偏移量。對于無用塊,只需調用nop_padding()函數使用NOP指令填充該基本塊;對于有用塊則須進行直接跳轉塊指令修復、有分支塊指令修復。
2.3.1 直接跳轉塊
對于直接跳轉塊,只需將該基本塊的最后一條指令改寫為jmp指令,并完成跳轉偏移量的修正。具體如下:首先,向該基本塊最后一條指令首地址填充jmp的opcode值;接著調用nop_padding()函數向其后4個地址填充0x90;然后按照下一跳有用塊首地址,計算出修正的跳轉偏移量,并調jmp_padding()函數填充新的跳轉偏移量即可。
2.3.2 有分支塊
對于含有分支的基本塊,須將cmov指令改寫成相應的條件跳轉指令跳至符合條件的分支(如:cmovx指令可改寫成jx指令),并在其后添加一條jmp指令。jx指令的下一跳是滿足X條件的右分支基本塊,jmp指令的下一跳是不滿足X條件的左分支基本塊,然后分別根據兩個分支基本塊首地址進行跳轉偏移量修正即可,分支基本塊的首地址可從符號執行模塊中獲取。
測試環境為裝有最新版LLVM-4.0混淆框架、本文提出的OLLVM反混淆框架、Miasm反混淆框架的 Ubuntu16.04 amd64系統計算機,詳細配置為:CPU型號Intel Core i7-4790@3.60 GHz,內存 12 GB。
經OLLVM混淆后的程序(為ELF文件)含有非常多的分支,在對其進行反混淆,特別是符號執行時勢必會造成時間開銷,因此本節就從反混淆用時指標上對本文提出的反混淆框架性能進行測試,并與Miasm反混淆框架性能進行對比。
測試方法一 將從 SPECint-2000[17](https://www.spec.org/cpu2000/CINT2000/)下載的200個C/C++程序使用相同的OLLVM混淆策略進行混淆后,便得到了200個ELF格式的樣本程序。首先使用本文提出的反混淆框架對每個測試樣本進行反混淆,為減小誤差,取20次反混淆時間的平均值作為該樣本最終的反混淆用時,直至完成所有樣本的測試;然后,重啟電腦,改用Miasm反混淆框架,仍采用上述的測試方案,完成所有樣本的反混淆用時測試。為方便展示,此處從200個樣本程序中選取10個具有較大范圍覆蓋率并具有代表性的樣本程序進行結果展示,實驗結果如圖3所示。

圖3 兩種框架反混淆用時對比Fig.3 Time consumption comparison of deobfuscation for two frameworks
表1詳細列出了測試時使用的10個樣本程序大小,及各程序所包含的分支循環數。由圖3可知,ELF文件大小為2.12 MB時,兩個框架的反混淆用時增幅均較為明顯,對比表1發現,該ELF文件對應的分支循環個數為2427個,遠遠多于大小為1.08 MB和3.75 MB的ELF文件的分支循環個數。綜合各實驗數據可知,反混淆用時和ELF文件大小無關,而與分支循環個數呈正相關,分支循環個數越多,符號執行時耗費的時間也越多。

表1 OLLVM混淆后的ELF文件Tab.1 OLLVM obfuscated ELF files
其次,本實驗中Miasm框架反混淆用時為7 s~21 min不等,而本文提出的反混淆框架為5 s~16 min不等,在混淆程序分支個數不多的情況下,二者反混淆用時較為接近,但隨著分支循環個數的不斷增加,本文提出的反混淆框架反混淆用時優勢越發明顯。
綜上所述,本文提出的反混淆框架相比Miasm框架具有更優秀的反混淆用時表現。
反混淆中最關心的是反混淆結果的正確性,因此為驗證本文提出的反混淆框架的正確性,將從反混淆結果程序語義正確性(以代碼相似性來衡量)、反混淆結果運行正確性兩個方面進行測試。
由于Miasm框架反混淆后得到的只是保留程序語義的Miasm IR Graph,無法比較代碼相似性,因此只對本文提出的反混淆框架做該項測試。
測試方法二 使用GNU編譯套件(GNU Compiler Collection,GCC)依次將3.1節中的200個C/C++文件編譯為可執行文件,并使用二進制文件對比工具 BinDiff[18-19](http://www.zynamics.com/bindiff.html)逐個比較反混淆后程序與未混淆源程序的代碼相似性。為進一步凸顯本文提出的反混淆框架性能以及方便展示,這里僅展示3.1節中分支最多的10個程序的代碼相似性對比結果,如圖4所示。

圖4 代碼相似性Fig.4 Code similarity
從圖4數據可知,10個測試樣本的平均代碼相似度為96.7%,表明反混淆后的程序與未混淆源程序具有極高的代碼相似度。
由于反混淆后的程序均為可執行文件,因此可以通過對比未混淆程序和反混淆后程序的運行結果,來判斷反混淆結果的正確性。
測試方法三 分別運行3.2節中的200個未混淆源程序及與之對應的反混淆后的程序,對比二者的運行結果一致度。為方便展示,此處僅展示分支循環個數最多的10個程序的未混淆程序與反混淆后程序運行對比結果,如表2所示。
由表2可知,未混淆源程序與反混淆后程序的執行結果完全一致,這說明反混淆結果是正確的,加之反混淆后的程序與未混淆源程序具有極高的代碼相似度(見圖4),因此可以利用IDA等工具對反混淆后的程序進行反編譯以恢復源程序代碼。
本節將從多文件格式支持、多平臺架構支持、反混淆效果三方面對Miasm框架和本文提出的反混淆框架進行對比,詳細的對比結果如表3所示。
由表3可知,本文提出的OLLVM反混淆框架的反混淆效果明顯優于Miasm反混淆框架,特別是在反混淆結果的后續處理上,Miasm反混淆后得到的只是保留程序語義的Miasm IR Graph,不僅圖形粗陋,而且圖形中使用晦澀難懂的Miasm IR語言;更重要的是,其反混淆結果無法反編譯以恢復未混淆程序源代碼,而本文提出的通用型自動化反混淆框架直接針對OLLVM混淆后的程序的匯編代碼進行操作,在保證程序語義完整性前提下,最終得到一個結果正確的可執行文件,因而可通過使用IDA進行加載,獲得顏色分明、層次清晰的程序控制流圖,并且控制流圖中的代碼塊采用易懂的匯編語言;另外,還能通過IDA對反混淆后的程序進行反編譯以恢復源程序的C/C++代碼。這些都說明了本文提出的反混淆框架相比Miasm框架具有更出色的反混淆性能。

表2 未混淆程序與反混淆后程序運行結果一致度Tab.2 Consistency of non-obfuscated and deobfuscated program in operation result

表3 不同框架反混淆性能對比Tab.3 Deobfuscation performance comparison of different frameworks
OLLVM混淆技術的流行為惡意軟件的肆虐提供了土壤,針對這一問題以及Miasm框架在OLLVM反混淆方面的缺陷,本文提出了一種基于符號執行的OLLVM通用型自動化反混淆框架,該框架結合符號執行、指令修復技術很好地克服了Miasm框架的缺點,并能恢復出未混淆程序源代碼,實驗結果表明該框架相比Miasm框架具有更優秀的反混淆性能。
目前本文提出的反混淆框架只能很好地實現x86架構下C/C++文件的OLLVM反混淆,而Android SO文件因采用arm指令,其在基本塊識別上與x86指令具有不同的規則,但二者在反混淆的思路、核心技術上具有共通性,下一步將以此為基礎研究Android SO文件的OLLVM反混淆。