LLVM學習總結與OLLVM項目分析

學習了一段時間的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為例):


OLLVM項目結構

讓我們只關注如下的文件夾,其它的暫且不管:
include文件夾

include文件夾

其實從文件夾名稱就能判斷include文件夾是頭文件所在的地方,include文件夾之下包含兩個文件夾:llvm和llvm-c。
llvm文件夾下有如下目錄:llvm\Transforms\Obfuscation,可以看到此文件夾下有一些頭文件:

Obfuscation頭文件

此處是存放OLLVM項目中自己寫的pass的頭文件的地方,由此可知,如果我們需要些自己的pass的話,那么對應的pass類的頭文件也需要在include\llvm\Transforms新建一個文件夾專門用來存放頭文件。頭文件的具體內容暫且不管,接下來再去看看實現文件在哪里。

打開與include文件夾平行的lib文件夾并進入lib\Transforms\Obfuscation目錄:

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。


IR代碼結構

以基本塊的分割為例

對于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 annotationsflag。關于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還有控制流平坦化,虛假控制流、指令替換、字符串加密等功能,對于這些內容還需要進一步的研究。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 近來,ollvm在國內移動安全,尤其是安全加固上的使用越來越廣泛,ollvm的混淆和反混淆也被視為比較高等的知識之...
    that_is_this閱讀 3,030評論 4 0
  • 前言 2000年,伊利諾伊大學厄巴納-香檳分校(University of Illinois at Urbana-...
    星光社的戴銘閱讀 15,974評論 8 180
  • LLVM架構介紹 本文主要介紹了LLVM的架構設計。LLVM命名源自于底層虛擬機(Low Level Virtua...
    愛笑的云里看夢閱讀 6,508評論 2 11
  • 三人行————眾 兩人行————從 一人行————one,靜靜的,一個人呆著沒有什么不好的,至少你可以沉思,至少你...
    嵐山閱讀 205評論 0 0
  • 姓名: 芳紫 公司: 新時代健康產業集團 組別: 365期 利他一組(組長) 【5月10日精進打卡第39天】 【知...
    芳紫芳香紫氣東來閱讀 182評論 0 4