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为例):


LLVM学习总结与OLLVM项目分析_第1张图片
OLLVM项目结构

让我们只关注如下的文件夹,其它的暂且不管:
include文件夹

LLVM学习总结与OLLVM项目分析_第2张图片
include文件夹

其实从文件夹名称就能判断include文件夹是头文件所在的地方,include文件夹之下包含两个文件夹:llvm和llvm-c。
llvm文件夹下有如下目录:llvm\Transforms\Obfuscation,可以看到此文件夹下有一些头文件:

LLVM学习总结与OLLVM项目分析_第3张图片
Obfuscation头文件

此处是存放OLLVM项目中自己写的pass的头文件的地方,由此可知,如果我们需要些自己的pass的话,那么对应的pass类的头文件也需要在include\llvm\Transforms新建一个文件夹专门用来存放头文件。头文件的具体内容暂且不管,接下来再去看看实现文件在哪里。

打开与include文件夹平行的lib文件夹并进入lib\Transforms\Obfuscation目录:

LLVM学习总结与OLLVM项目分析_第4张图片
Obfuscation所在目录

打开 Obfuscation目录,可以看到与之前的头文件一一对应的实现文件:
LLVM学习总结与OLLVM项目分析_第5张图片
实现文件

至此,与我们编写自己的pass一样,在 include\llvm\Transforms\Obfuscation定义头文件,在 lib\Transforms\Obfuscation写实现文件。这样,我们就明白了该如何开始写自己的项目。不过要注意的是,不管是LLVM还是OLLVM,它们都是通过编写makefile来实现项目的运行的,所以我们得熟练掌握makefile的编写与依赖,才能玩转自己的项目。

OLLVM简单源码分析

在分析源码之前,首先介绍一下IR的基本结构:
IR代码是由一个个Module组成的,每个Module之间互相联系,而Module又是由一个个Function组成,Function又是由一个个BasicBlock组成,在BasicBlock中又包含了一条条Instruction。


LLVM学习总结与OLLVM项目分析_第6张图片
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 SplitNum("split_num", cl::init(2),
                             cl::desc("Split  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 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::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 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还有控制流平坦化,虚假控制流、指令替换、字符串加密等功能,对于这些内容还需要进一步的研究。

你可能感兴趣的:(LLVM学习总结与OLLVM项目分析)