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还有控制流平坦化,虚假控制流、指令替换、字符串加密等功能,对于这些内容还需要进一步的研究。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容

  • 近来,ollvm在国内移动安全,尤其是安全加固上的使用越来越广泛,ollvm的混淆和反混淆也被视为比较高等的知识之...
    that_is_this阅读 2,897评论 4 0
  • 前言 2000年,伊利诺伊大学厄巴纳-香槟分校(University of Illinois at Urbana-...
    星光社的戴铭阅读 15,846评论 8 180
  • LLVM架构介绍 本文主要介绍了LLVM的架构设计。LLVM命名源自于底层虚拟机(Low Level Virtua...
    爱笑的云里看梦阅读 6,257评论 2 10
  • 三人行————众 两人行————从 一人行————one,静静的,一个人呆着没有什么不好的,至少你可以沉思,至少你...
    岚山阅读 141评论 0 0
  • 姓名: 芳紫 公司: 新时代健康产业集团 组别: 365期 利他一组(组长) 【5月10日精进打卡第39天】 【知...
    芳紫芳香紫气东来阅读 166评论 0 4