本文來自網易雲社區。

越來越多的so文件採用了llvm進行加固,逆向的小夥伴表示不能愉快的玩耍了。本文對Obfuscator-llvm實現混淆的方式進行講解,希望能幫助到大家。

1. O-llvm介紹O-llvm是基於llvm進行編寫的一個開源項目(github.com/obfuscator-l),它的作用是對前端語言生成的中間代碼進行混淆,目前在市場上,一些加固廠商(比如360加固寶、梆梆加固)會使用改進的O-llvm對它們so文件中的一些關鍵函數採用O-llvm混淆,增加逆向的難度。因此,掌握O-llvm的實現過程,是很有必要的。O-llvm總體構架和llvm是一致的,如圖1所示。

圖1 LLVM總體架構

其中IR(intermediate representation)是前端語言生成的中間代碼表示,也是Pass操作的對象,它主要包含四個部分:(1)Module:比如一個.c或者.cpp文件。(2)Function:代表文件中的一個函數。(3)BasicBlock:每個函數會被劃分為一些block,它的劃分標準是:一個block只有一個入口和一個出口。(4)Instruction:具體的指令。他們之間的關係可用圖2表示。
圖2 IR中各部分的關係

本次源碼分析的版本為Obfuscator-llvm-3.6.1,目前O-llvm包含有三個pass,分別是BogusControlFlow、Flattening 和 Instruction Substitution。它們是O-llvm實現混淆功能的核心,具體實現位於llvm-3.6.1/lib/Transforms/Obfuscation/目錄下。下面就對這三個pass進行詳細的分析。2. Pass1:BogusControlFlowBogusControlFlow的功能是為函數增加新的虛假控制流和添加垃圾指令。2.1 入口函數runOnFunctionBogusControlFlow繼承了FunctionPass,因此它的入口函數即為runOnFunction。在runOnFunction函數的具體實現中,首先判斷了兩個參數的值:ObfTimes和ObfProbRate,分別代表bcf(BogusControlFlow)循環運行的次數和每個basic block被混淆的幾率,它們的默認值分別為1和30%。可通過設置參數boguscf-loop、 boguscf-prob修改它們的默認值。檢查完參數的正確性之後,代碼接著判斷是否包含了啟動bcf的命令。在編譯程序代碼時,若要啟動bcf模塊,需要帶上參數「-mllvm -bcf」。參數檢查完畢之後,首先調用bogus函數。bogus函數首先將本function的所有basicblock存放到一個list容器中,然後使用一個while循環調用addBogusFlow函數對選中的basicblock進行增加虛假控制流。

2.2 addBogusFlow函數

為了更方便的分析源碼,本文用一個簡單的例子來查看編譯時每一步關鍵的代碼執行之後IR圖的變化,測試代碼如下所示:

int func1(int a,int b){ return a+b;}該測試代碼的func1函數的IR圖如圖3所示。
圖3 func1函數的IR圖

addBogusFlow函數首先調用getFirstNonPHIOrDbgOrLifetime函數獲取本basicblock中第一個不是Phi、Dbg、Lifetime的指令的地址(在本例中,即為%a.addr = alloca i32, aling 4的地址),然後調用splitBasicBlock函數。splitBasicBlock函數可根據上述指令的地址將一個basicblock一分為二(可稱為first basicblock 和original basicblock)。此時的IR圖如圖4所示。
圖4 分割後的IR圖

接著調用createAlteredBasicBlock函數對original basicblock進行拷貝生成一個名為「altered basicblock」的basicblock,並對該basicblock加入一些垃圾指令。加入垃圾指令的方法是遍歷該basicblock中的所有OpCode,若包含有Add、Sub、UDiv、SDiv、URem、SRem、Shl、LShr、AShr、And、Or、Xor以及FAdd、FSub、FMul、FDiv、FRem指令,則用隨機生成一些指令來進行替換。由於該block在程序運行時並不會執行,因此無需擔心插入的指令對原始程序運行的結果產生影響。拷貝original basicblock後,IR圖如圖5所示。
圖5 拷貝後的IR圖

這時,所有的basicblock已經準備完畢,一共存在有3個basicblock,需要調整他們之間的關係。首先清除first basicblock和altered basicblock跟父節點的關係,代碼為:basicBlock->getTerminator()->eraseFromParent();alteredBB->getTerminator()->eraseFromParent();清除完畢後的IR圖如圖6所示。
圖6 清除父節點後的IR圖

接著下一步的操作是增加basicblock之間的條件跳轉指令。對於first basicblock(即為圖中的entry),bcf源碼的做法是先增加一條比較語句 1.0 = = 1.0 ,然後為真時跳轉到original basicblock,為假則跳轉到altered basicblock。可用偽代碼如下表示:

if( 1.0 == 1. 0) original basicblockelse altered basicblock對於altered basicblock模塊,在它的尾部增加一條跳轉指令,使得當它執行完畢之後(實際上它並不會執行),跳轉到original basicblock模塊。此時的IR圖如圖7所示。
圖7 增加跳轉指令後的IR圖

最後,獲取original basicblock中最後一條指令的地址(在該例子中即ret指令的地址),調用splitBasicblock函數將original basicblock一分為二(original basicblok和originalBBpart2),然後調用如下代碼:originalBB->getTerminator()->eraseFromParent();消除original basicblok和originalBBpart2的關係後,再在original basicblock的末尾加入一個判斷語句,為真時跳轉到ret指令,為假則跳轉到altered basicblock,偽代碼如下所示:if( 1.0 == 1. 0)

retelse altered basicblock

此時該func1函數的IR圖如圖8所示:
圖8 執行完addBogusFlow函數後的IR圖

2.3 doF函數該函數的功能是將Function中所有為真的判斷語句進行替換,比如上一節中的「1.0 == 1.0 」。它的思想是定義兩個全局變數x、y並且初始化為0,然後遍歷Module內的所有指令,並將所有的FCMP_TRUE分支指令替換為「y<10 || x*x(x-1)%2 ==0」。替換完畢後func1函數的IR流程圖如圖9所示:
圖9 doF函數執行完畢後的IR圖

至此,對func1函數的一次bcf混淆過程就完成了。從該分析也可以看出BogusControlFlow有很多可以改進的地方,這裡就不再指出,有興趣的讀者可自行分析修改。

3. Pass2:Flattening

Flattening的主要功能是為函數增加switch-case語句,使得函數變得扁平化。下面就對它的實現源碼進行分析。3.1 入口函數runOnFunctionFlattening繼承了FunctionPass,因此它的入口函數即為runOnFunction。在runOnFunction函數的具體實現中,首先判斷是否包含了啟動fla的命令。在編譯目標程序代碼時,如要啟動fla模塊,需要帶上參數「-mllvm -fla」。參數檢查完畢之後,調用flatten函數。flatten函數是該Pass的核心,下面對該函數進行分析。

3.2 flatten函數為了更方便的分析源碼,本文用一個簡單的例子來查看編譯時每一步關鍵的代碼執行之後IR圖的變化,測試代碼如下所示:int func1(int a,int b){

int result;

if(a>0){ result=a+b; } else{ result=a-b; } return result;}圖10是func1的原始IR流程圖。從該圖可以看出,func1有4個basicblock。

圖10 func1原始IR圖

flatten函數首先將本Function中除了第一個basicblock外的所有basicblock保存到一個vector容器中。接著對basicblock的數目進行了判斷,當basicblock的數目小於等於1時,flatten函數會直接退出並返回false。接著通過F->begin獲取本Function的第一個basicblock,並判斷該basicblock是否包含有跳轉指令;如果有,再進一步判斷該指令是否為條件跳轉,若是的話則獲取該條件跳轉指令的地址,並調用splitBasicblock函數通過該地址將第一個basicblock一分為二。在本例子中,對func1函數調用splitBasicblock函數之後,此時的IR圖如圖11 所示。
圖11 分割後的IR圖

如果不是條件跳轉指令(比如for循環),則將跳轉指令的目標basicblock存儲起來,後面會將該basicblock添加到switch-case中。接著,將第一個basicblock與下一個basicblock的跳轉關係刪除,代碼為:insert->getTerminator()->eraseFromParent();刪除後的IR圖如圖12所示。
圖12 刪除第一個basicblock的跳轉指令之後的IR圖

然後在第一個basicblock的末尾創建一個變數switchVar並賦予它一個隨機的值,接著創建三個新的basicblock塊,分別為「loopEntry」、「loopEnd」以及「swDefault」,並且設置好它們之間的跳轉關係,此時的IR圖如圖13所示。
圖13 設置好基本跳轉關係後的IR圖

這時,基本的switch-case已經有了,下一步操作是將保存在vector中的每一個basicblock都添加到switch-case語句中,每一個basicblock對應一個case,並且每個case的值都是一個隨機值。此時的IR圖如圖14所示。
圖14 增加case後的IR圖

添加了全部basicblock塊之後,需要修改每個basicblock塊之間的跳轉關係,使得每個basicblock塊執行完畢之後,會重新設置switchVar的值,從而回到switch的判斷語句時,能夠順利的跳轉到下一個case,直到程序執行完畢。此時的IR圖如圖15所示。
圖15 修改各case之間的關係後的IR圖

從圖11和圖15的差別可以看出,執行Flattening後,函數的多了一些basicblock塊,而且函數的核心實現部分均位於同一層,每次執行完一個basicblock塊後均要返回loopEntry才能執行下一個basicblock。fla和bcf的互相配合,能大大的提高對函數的混淆效果。4. Pass3:SubstitutionSubstitution的主要功能是對程序的一些指令進行替換。4.1 入口函數runOnFunctionBogusControlFlow繼承了FunctionPass,因此它的入口函數即為runOnFunction。在runOnFunction函數的具體實現中,首先判斷是否包含了啟動sub的命令。在編譯程序代碼時,如要啟動sub模塊,需要帶上參數「-mllvm -sub」。sub模塊還支持多次循環操作,可通過參數「-mllvm –sub-loop=xx」顯式的設定循環次數,默認為1。參數檢查完畢之後,調用substitute函數。substitute函數的功能是遍歷Function內的每一個指令,對符合要求的指令進行替換。4.2 Substitution函數該函數的實現主要是依靠最外層的do-while循環和兩個for循環。do-while循環主要是根據設定的sub循環次數運行兩個for循環。外層for循環是遍曆本Function中的每一個basicblock,裡層for循環是遍歷basicblock中的每一個指令,接著採用一個switch-case語句來對不同的指令進行不同的操作。目前,sub支持五種指令的替換,分別是「Add」、「Sub」、「And」、「Or」以及「Xor」指令。「Add」指令支持4種替換方法,分別是a = b - (-c)、a = -(-b + (-c))、r = rand (); a = b + r; a = a + c; a = a – r 、r = rand (); a = b - r; a = a + b; a = a + r 。「Sub」指令支持3種替換方法,分別是a = b + (-c)、r = rand (); a = b + r; a = a - c; a = a – r 、r = rand (); a = b - r; a = a - c; a = a + r 。「And」 指令支持2種替換方法,分別是a = b & c => a = (b^~c)& b 、a = a & b <=> !(!a | !b) & (r | !r) 。「Or」 指令支持2種替換方法,分別是a = b | c => a = (b & c) | (b ^ c) 、a | b => [(!a & r) | (a & !r) ^ (!b & r) |(b & !r) ] | [!(!a | !b) & (r |!r)] 。「Xor」 指令支持2種替換方法,分別是a = a ^ b => a = (!a & b) | (a & !b) 、a = a ^ b <=> (a ^ r) ^ (b ^ r) <=> (!a & r | a & !r) ^ (!b & r | b & !r) 。在substitute函數的switch-case中,程序會隨機的調用這些替換方法,部分代碼如圖16所示。
圖16 替換指令的代碼

例如,Add指令中,funcAdd是個函數數組,裡面存儲了NUMBER_ADD_SUBST個替換add指令的函數,get_range是個獲取隨機數的函數,通過這種方法,可使替換的add具有一定的隨機性。對於其他的指令,也是採用類似add指令的方式進行替換的。5. 改進建議由於O-llvm的開源性,大家如果要使用該產品的功能,可以在它的基礎上做一些修改。比如在BogusControlFlow中,對於跳轉指令為真的分支,O-llvm採用如下指令進行替換「y<10 || x*(x-1)%2==0 」,使用IDA打開混淆後的so文件可輕易的發現該特徵。因此,我建議事先準備多條可以等價替換的指令,在遇到需要替換的地方時,隨機的選取其中一條等價指令進行替換。對於basicblock塊的劃分,也可以採用其他規則來進行劃分,大家可以腦洞大開,多嘗試嘗試。在Substitution中,我們也可以採用其他的等價指令進行替換,這裡也不再舉例了。6. 最後 前段時間,有人在看雪論壇發布了一篇名為《ollvm的混淆反混淆和定製修改》的文章(bbs.pediy.com/thread-21),大家也可以閱讀下該文章,加深對O-llvm的了解。網易雲安全(易盾)提供Android 應用加固iOS 應用加固服務,點擊鏈接可免費試用。

本文來自網易雲社區,經作者王澤華授權發布。

原文: Obfuscator-llvm源碼分析

更多網易研發、產品、運營經驗分享請訪問網易雲社區。


推薦閱讀:
相关文章