OLLVM控制流平坦化的改進

一、傳統平坦化
控制流平坦化這個混淆方式第一次出現于Ollvm項目中,作為其中的三種混淆方式之一fla。該混淆方式主要思想就是以基本塊為單位,通過一個主分發器來控制程序的執行流程。在這種結構下,原有的程序控制邏輯被打碎,通過其中的控制變量配合Switch結構來實現控制流的轉移。
OLLVM是一款是由瑞士西北科技大學開發的一套開源的針對LLVM的代碼混淆工具,旨在加強逆向的難度,整個項目包含數個包含獨立功能的LLVM Pass,每個Pass會對應實現一種特定的混淆方式,這些Pass將在后面進行細說,通過這些Pass可以改變源程序的CFG和源程序的結構。

具體的混淆流程如下:
1.收集原函數中所有的基本塊,并初始化隨機數種子。
2.對入口基本塊進行處理,切分基本塊保證入口基本塊只有一個后繼。
3.給每一個基本塊分配一個隨機數字,并新建一個變量var,在入口基本塊中賦值為入口基本塊后繼基本塊對應的數字。
4.構造出基本的switch結構和循環框架,使得switch鏈接所有原有基本塊。
5.修正每個原有基本塊的后繼,使其跳轉至switch結構,并在跳轉之前根據后繼和跳轉條件構造對var的賦值語句。
而在原版的框架中,該混淆的普適性并不好。在針對于存在異常處理的C++代碼中,是無法進行混淆的,同時由于該方案提出時間較早,早已有多種方法能夠很好的去除該混淆,因此在該篇文章中,對原有的混淆進行改進,使其能夠支持異常處理和防止被輕易去除。本文是基于控制流平坦化Pass的改進,主要提出的為改進原理,不會基于源碼一行行分析,最后會將源碼放出,可自行閱讀。
二、支持異常處理
在傳統的平坦化中,假定了每個函數的Terminator都是跳轉指令BranchInst,然而在實際處理過程中并不一定是BranchInst,還有可能是SwitchInst,由于平坦化只處理后繼小于等于2的情況,對于這種有多個后繼的Terminator是無法處理的。針對于這種情況比較好處理,直接在上述第5步不修正其Terminator,讓其保持原有的代碼,直接continue即可,這樣就可以保證其正常運行。

但是除此之外,有一個指令為InvokeInst指令,這個指令有兩個后繼,但是一個是正常的基本塊處,另一個是跳轉到unwind相關的邏輯中,這個unwind基本塊的開頭必須為LandingPadInst,且只能通過InvokeInst指令來到達。所以當LandingPadInst被插入到SwitchInst中去的時候,由于不是通過InvokeInst到達的,所以會報錯。


修正這種情況的思路也是非常的簡單,首先需要確定每個基本塊的Terminator是否是InvokeInst,是的話則從需要平坦化的基本塊中剔除掉InvokeInst對應的UnwindBasicBlock。同時在進行第五步時判斷當且僅當Terminator為BranchInst時才進行修正后繼,否則不做任何處理。最后處理后結果如下所示。

- 代碼修改如下(新增了判斷Terminator是否為BranchInst):

- 同時去除了LandingPad所在的基本塊。

三、增強反反混淆效果
在現存的對抗思路中,無外乎就是模擬執行和靜態分析兩種思路。模擬執行首先需要定位到混淆后的控制流圖中的真實塊,即原控制流圖中的基本塊,然后從每個基本塊開始模擬執行,給所有真實塊(原函數中的基本塊)下斷點,當從基本塊A出發,位于B中的斷點觸發,便能很容易的確定基本塊后繼(B是A的后繼),如果遇到存在兩個后繼的情況則將對應的條件判斷取反,從而使其獲得兩條路徑,這樣的思路往往采用angr或者unicorn實現,已經存在很多腳本了。而靜態分析往往更加的困難,需要定位到對應的控制變量,并根據程序邏輯,找出每個基本塊對應的值,并通過判斷計算出每個基本塊對應的后繼。
對于靜態分析,更加偏向于經驗化,通過少量的修改即可使得其適用性降低,因此靜態方法去除控制流平坦化的思路往往被使用的較少,且普適性較差。
而對于模擬執行,為了實現恢復的完整性,往往都是單獨的從一個基本塊出發,設下斷點,確定該基本塊的后繼是什么。(當然也存在動態trace整個程序的執行流程,只要確定真實塊即可簡單的trace恢復執行流程,但是往往程序邏輯的觸發條件是復雜的,有可能并不能覆蓋所有的分支,因此這里不做討論)

這里主要針對第二種思路進行對抗,即模擬執行。在模擬執行中,存在兩個關鍵點,一是真實塊的確定,二是后繼的確定。由于大部分思路都是從某個基本塊直接出發確定,這個方法能夠正確運行的原因,是因為在去除混淆時,我們并不關心原有代碼基本塊之間的運行關系,程序狀態并不會影響控制流的轉移。換句話說控制流平坦化中,由于完全依靠一個變量簡單確定后繼,且每個真實塊中該變量的賦值并沒有蘊含基本塊之間的關系,因此可以單獨的確定每個基本塊的后繼。
基于此,如果構造出一種能夠實現控制流的轉移需要基于現在已執行基本塊的信息的方式,即可很好的防護這種逐個擊破的攻擊方式。
對于一個函數的控制流圖,如果需要運行到節點(基本塊)B,存在一些必定經過的節點,而這些節點即必經節點。
定義如果從起點E出發,要到達節點B必須經過節點A,則可稱A支配節點B。
通過支配節點的定義,不難構造出一個方案來防御逐個擊破的手段:
每個節點都存在一個標志位,用于標記每個節點(基本塊)是否被訪問。
如果運行到某個節點B,支配B的節點的標志位必定被設置為1(被訪問)。
每個節點存在一個key變量,只有通過這個正確的key才能計算出正確的后繼。
一個節點B被第一次執行時會更新其他節點的key,這些其他節點被節點B支配。
例如下圖,每個節點都有一個變量來標記是否被經過,都有數組v存儲,v[A]=1代表A已經被訪問,且初始化結果都為0,因為剛開始時所有節點都沒有被訪問。
當一個節點A執行時,會賦值v[A]=1,如果發現它被賦值前為0的話說明為第一次經過,那么就根據自身分配到的隨機值修改其他節點的key值,這些其他節點都是被A支配的節點。

所以在這種情況下,在程序具體執行中的每個節點的key是一個特定的值(因為一個節點有固定多個必經節點,當節點被執行時,其必經節點也必然被執行,且保證了每個節點只會更新一次其他節點的key值),如果將節點單獨抽取出來執行的話,沒有必經節點更新他的key值,則必然與具體執行中的key不一樣。
由于該特性,計算出每個節點的特定的值,并對原有的控制流平坦化中的控制變量switchVar進行異或運算,只有key是正確的才能保證switchVar的值是正確的,從而跳轉到正確的后繼節點。而如果單純的抽出某個節點開始執行的話,由于沒有執行節點的必經節點,導致其key值不正確,因此switchVar也是錯誤的,從而無法獲得正確的后繼,從而保護代碼防止被去混淆。
具體的修改代碼如下:
插入一個函數,通過給定的隨機值更新其他節點的key值。
Function *buildUpdateKeyFunc(Module *m){ std::vector<Type*> params; params.push_back(Type::getInt8Ty(m->getContext())); params.push_back(Type::getInt32Ty(m->getContext())); params.push_back(Type::getInt32Ty(m->getContext())->getPointerTo()); params.push_back(Type::getInt32Ty(m->getContext())->getPointerTo()); params.push_back(Type::getInt32Ty(m->getContext())); FunctionType *funcType=FunctionType::get(Type::getVoidTy(m->getContext()),params,false); Function *func=Function::Create(funcType,GlobalValue::PrivateLinkage,Twine("ollvm"),m); BasicBlock *entry=BasicBlock::Create(m->getContext(),"entry",func); BasicBlock *cond=BasicBlock::Create(m->getContext(),"cond",func); BasicBlock *update=BasicBlock::Create(m->getContext(),"update",func); BasicBlock *end=BasicBlock::Create(m->getContext(),"end",func); Function::arg_iterator iter=func->arg_begin(); Value *flag=iter; Value *len=++iter; Value *posArray=++iter; Value *keyArray=++iter; Value *num=++iter; IRBuilder<> irb(entry); Value *i=irb.CreateAlloca(irb.getInt32Ty()); irb.CreateStore(irb.getInt32(0),i); irb.CreateCondBr(irb.CreateICmpEQ(flag,irb.getInt8(0)),cond,end); irb.SetInsertPoint(cond); irb.CreateCondBr(irb.CreateICmpSLT(irb.CreateLoad(i),len),update,end); irb.SetInsertPoint(update); Value *pos=irb.CreateLoad(irb.CreateGEP(posArray,irb.CreateLoad(i))); Value *key=irb.CreateGEP(keyArray,pos); irb.CreateStore(irb.CreateXor(irb.CreateLoad(key),num),key); irb.CreateStore(irb.CreateAdd(irb.CreateLoad(i),irb.getInt32(1)),i); irb.CreateBr(cond); irb.SetInsertPoint(end); irb.CreateRetVoid(); return func;}
插入標記數組和key數組,計算出每個基本塊的key值數組(key_map),為每個基本塊分配一個隨機值用于更新其他節點的key,并在基本塊后添加更新標記數組和key數組的代碼。
IRBuilder<> irb(&*oldEntry->getFirstInsertionPt()); // generate context info key for each blockValue *visitedArray=irb.CreateAlloca(irb.getInt8Ty(),irb.getInt32(origBB.size()));Value *keyArray=irb.CreateAlloca(irb.getInt32Ty(),irb.getInt32(origBB.size()));irb.CreateMemSet(visitedArray,irb.getInt8(0),origBB.size(),(MaybeAlign)0);irb.CreateMemSet(keyArray,irb.getInt8(0),origBB.size()*4,(MaybeAlign)0);int idx=0;std::vector<unsigned int> key_list;DominatorTree tree(*f);std::map<BasicBlock*,unsigned int> key_map;std::map<BasicBlock*,unsigned int> index_map;for(std::vector<BasicBlock *>::iterator b=origBB.begin();b!=origBB.end();b++){ BasicBlock *block=*b; unsigned int num=getUniqueNumber(&key_list); key_list.push_back(num); key_map[block]=0;}for(std::vector<BasicBlock *>::iterator b=origBB.begin();b!=origBB.end();b++,idx++){ BasicBlock *block=*b; std::vector<Constant*> doms; int i=0; for(std::vector<BasicBlock *>::iterator bb=origBB.begin();bb!=origBB.end();bb++,i++) { BasicBlock *block0=*bb; if(block0!=block && tree.dominates(block,block0)) { doms.push_back(irb.getInt32(i)); key_map[block0]^=key_list[idx]; } } irb.SetInsertPoint(block->getTerminator()); Value *ptr=irb.CreateGEP(irb.getInt8Ty(),visitedArray,irb.getInt32(idx)); Value *visited=irb.CreateLoad(ptr); if(doms.size()!=0) { ArrayType *arrayType=ArrayType::get(irb.getInt32Ty(),doms.size()); Constant *doms_array=ConstantArray::get(arrayType,ArrayRef<Constant*>(doms)); GlobalVariable *dom_variable=new GlobalVariable(*(f->getParent()),arrayType,false,GlobalValue::LinkageTypes::PrivateLinkage,doms_array,"doms"); irb.CreateCall(FunctionCallee(updateFunc),{visited,irb.getInt32(doms.size()),irb.CreateGEP(dom_variable,{irb.getInt32(0),irb.getInt32(0)}),keyArray,irb.getInt32(key_list[idx])}); } irb.CreateStore(irb.getInt8(1),ptr); index_map[block]=idx;}
在基本塊結尾是switchVar異或該基本塊對應的key。
irb.SetInsertPoint(block);irb.CreateStore(irb.CreateXor(irb.CreateLoad(irb.CreateGEP(keyArray,irb.getInt32(index_map[succ]))),ConstantInt::get(sw->getCondition()->getType(),fixNum)),switchVar);BranchInst::Create(loopEnd,block);
最終結果如圖所示,每個基本塊后加入了一串新的代碼。

同時采用基于angr的deflat腳本無法正確的去除混淆了。

四 結語
最后修改后的控制流平坦化Pass被整合至了Pluto-Obfuscator,這個是正在維護的OLLVM項目,同時其中會不定期更新一些新的Pass。
https://github.com/bluesadi/Pluto-Obfuscator
該改進后的控制流平坦化混淆的代碼可在這個網址(https://github.com/bluesadi/Pluto-Obfuscator/blob/main/llvm/lib/Transforms/Obfuscation/FlatteningEnhanced.cpp)見到。