學習了一段時間的LLVM后,難免需要對其做一個總結,同時準備下一階段的學習工作——基于LLVM自定制代碼混淆器。在此只記錄學習內容,不表達實現方式。
LLVM、clang、IR概述
對于LLVM,個人認為可以將它理解為是一個編譯器,或者是一個完整的編譯架構。它將源代碼(.c或者.cpp或者.m等文件代碼)生成與機器無關的中間代碼,稱之為IR。然后對產生的IR進行優化,生成對應的機器匯編語言。這和傳統編譯器前端,中間優化,后端的設計模式很相似。而不同之處在于,可以通過自定制前端或者后端來使之支持編譯你的語言,對應的就是將源碼轉為中間IR代碼,或者中間IR代碼轉為指定的機器代碼,即只需要實現指定的前端后者后端即可。這就是LLVM強大的可擴展性。
對于LLVM來說,其前端是clang,在編譯源碼文件的時候使用的編譯工具也是clang。而生成中間IR代碼后需要對IR代碼進行一些操作,例如添加一些代碼混淆功能。LLVM的做法是通過編寫Pass(其實就是對應的一個個類,每個類實現不同的功能)來實現混淆的功能。所以實現混淆,其實就是編寫功能性的Pass。怎樣編寫pass在之前的文章可以找到。
加固與保護
如果你是安卓開發從業者,那么我相信你應該聽說過VMP保護,VMP(虛擬軟件保護技術)的思路是自定義一套虛擬機指令和對應的解釋器,并將標準的指令轉換成自己的指令,然后由解釋器將自己的指令給對應的解釋器。由于安卓系統后端使用了LLVM,并且smali2c的技術已經漸漸成熟,所以OLLVM(一個開源的代碼混淆器)變成了一個可選項,但是對于加固來說,它的保護是基于代碼級別的,需要提供源碼或者編譯的中間代碼。
這當然不是企業能接受的事情,所以需要做二進制加固。但是二進制加固離不開反匯編解析引擎(capstone),它可以將指令抽出來,然后轉為自己的虛擬指令,例如將LLVM IR虛擬為自己的虛擬指令,但是這種方法難度較大。對于代碼混淆來說,只用對IR代碼進行處理就可以了。
如何開始
當然首先想到的是Google,但是Google出來的文章對于真正想做一個有意義的項目的人來說意義并不大。對于本人而言,目前學習了LLVM,了解了其架構與簡單的實現,下面要學習的自然是該如何仿照OLLVM或者是Hikari或者上交的Armariris(孤挺花)等一些開源項目來實現一些自己的混淆功能。感謝這些開源作者。
從熟悉項目開始
下載以上的項目中的一個,用CLion或者其他IDE打開項目查看項目結構(以OLLVM為例):
讓我們只關注如下的文件夾,其它的暫且不管:
include文件夾
其實從文件夾名稱就能判斷include文件夾是頭文件所在的地方,include文件夾之下包含兩個文件夾:llvm和llvm-c。
llvm文件夾下有如下目錄:llvm\Transforms\Obfuscation
,可以看到此文件夾下有一些頭文件:
此處是存放OLLVM項目中自己寫的pass的頭文件的地方,由此可知,如果我們需要些自己的pass的話,那么對應的pass類的頭文件也需要在include\llvm\Transforms
新建一個文件夾專門用來存放頭文件。頭文件的具體內容暫且不管,接下來再去看看實現文件在哪里。
打開與include
文件夾平行的lib
文件夾并進入lib\Transforms\Obfuscation
目錄:
打開
Obfuscation
目錄,可以看到與之前的頭文件一一對應的實現文件:至此,與我們編寫自己的pass一樣,在
include\llvm\Transforms\Obfuscation
定義頭文件,在lib\Transforms\Obfuscation
寫實現文件。這樣,我們就明白了該如何開始寫自己的項目。不過要注意的是,不管是LLVM還是OLLVM,它們都是通過編寫makefile來實現項目的運行的,所以我們得熟練掌握makefile的編寫與依賴,才能玩轉自己的項目。
OLLVM簡單源碼分析
在分析源碼之前,首先介紹一下IR的基本結構:
IR代碼是由一個個Module組成的,每個Module之間互相聯系,而Module又是由一個個Function組成,Function又是由一個個BasicBlock組成,在BasicBlock中又包含了一條條Instruction。
以基本塊的分割為例
對于OLLVM的每個pass,其主要的工作繼承對應的pass類,就是對相應的方法進行重寫,例如SplitBasicBlock的實現,它繼承自FunctionPass,并重寫了runOnFunction方法:
bool SplitBasicBlock::runOnFunction(Function &F) {
// Check if the number of applications is correct
if (!((SplitNum > 1) && (SplitNum <= 10))) {
errs()<<"Split application basic block percentage\
-split_num=x must be 1 < x <= 10";
return false;
}
Function *tmp = &F;
// Do we obfuscate
if (toObfuscate(flag, tmp, "split")) {
split(tmp);
++Split;
}
return false;
}
1、SplitBasicBlock 首先對SplitNum
進行判斷,SplitNum
定義如下:
static cl::opt<int> SplitNum("split_num", cl::init(2),
cl::desc("Split <split_num> time each BB"));
此處是對用clang編譯源文件的時候選用的參數split做的定義:
clang -mllvm -split test.c
clang -mllvm -split_num=3 test.c
第一條命令表示啟用對基本block的分割,使之扁平化。
第二條命令表示對基本block分割次數為3次(前提是必須已啟用split),默認是1次。
2、對于splitNum在1~10 之外的情況,提示分割次數錯誤,即分割次數必須在1~10次之內。
3、對于符合要求的splitNum,調用toObfuscate
函數進行處理,處理方式如下(該函數在Utils.h
文件中):
bool toObfuscate(bool flag, Function *f, std::string attribute) {
std::string attr = attribute;
std::string attrNo = "no" + attr;
// Check if declaration
if (f->isDeclaration()) {
return false;
}
// Check external linkage
if(f->hasAvailableExternallyLinkage() != 0) {
return false;
}
// We have to check the nofla flag first
// Because .find("fla") is true for a string like "fla" or
// "nofla"
if (readAnnotate(f).find(attrNo) != std::string::npos) {
return false;
}
// If fla annotations
if (readAnnotate(f).find(attr) != std::string::npos) {
return true;
}
// If fla flag is set
if (flag == true) {
/* Check if the number of applications is correct
if (!((Percentage > 0) && (Percentage <= 100))) {
LLVMContext &ctx = llvm::getGlobalContext();
ctx.emitError(Twine("Flattening application function\
percentage -perFLA=x must be 0 < x <= 100"));
}
// Check name
else if (func.size() != 0 && func.find(f->getName()) != std::string::npos) {
return true;
}
if ((((int)llvm::cryptoutils->get_range(100))) < Percentage) {
return true;
}
*/
return true;
}
return false;
}
可以看到該函數主要是各種檢查以及判斷是否啟用了split功能,判斷依據就是Functions annotations
和flag
。關于Functions annotations
的介紹請看這里。
接下來看分割處理的函數split
:
void SplitBasicBlock::split(Function *f) {
std::vector<BasicBlock *> origBB;
int splitN = SplitNum;
// Save all basic blocks
for (Function::iterator I = f->begin(), IE = f->end(); I != IE; ++I) {
origBB.push_back(&*I);
}
for (std::vector<BasicBlock *>::iterator I = origBB.begin(),
IE = origBB.end();
I != IE; ++I) {
BasicBlock *curr = *I;
// No need to split a 1 inst bb
// Or ones containing a PHI node
if (curr->size() < 2 || containsPHI(curr)) {
continue;
}
// Check splitN and current BB size
if ((size_t)splitN > curr->size()) {
splitN = curr->size() - 1;
}
// Generate splits point
std::vector<int> test;
for (unsigned i = 1; i < curr->size(); ++i) {
test.push_back(i);
}
// Shuffle
if (test.size() != 1) {
shuffle(test);
std::sort(test.begin(), test.begin() + splitN);
}
// Split
BasicBlock::iterator it = curr->begin();
BasicBlock *toSplit = curr;
int last = 0;
for (int i = 0; i < splitN; ++i) {
for (int j = 0; j < test[i] - last; ++j) {
++it;
}
last = test[i];
if(toSplit->size() < 2)
continue;
toSplit = toSplit->splitBasicBlock(it, toSplit->getName() + ".split");
}
++Split;
}
}
該函數首先定義了一個vector
數組origBB
用于保存所有的block塊,然后遍歷origBB
,對每一個blockcurr
,如果它的size
(即包含的指令數)只有1個或者包含PHI節點,則不分割該block。
對于待分割的block,首先生成分割點,用test
數組存放分割點,用shuffle
打亂指令的順序,使sort
函數排序前splitN個數能盡量隨機。
最后分割block是調用splitBasicBlock
函數分割基本塊。
以上就是對分割基本塊的一個簡單介紹。OLLVM還有控制流平坦化,虛假控制流、指令替換、字符串加密等功能,對于這些內容還需要進一步的研究。