欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Obfuscator-llvm源码分析

程序员文章站 2024-03-22 08:15:16
...

本文来自 网易云社区 

越来越多的so文件采用了llvm进行加固,逆向的小伙伴表示不能愉快的玩耍了。本文对Obfuscator-llvm实现混淆的方式进行讲解,希望能帮助到大家。

1. O-llvm介绍

O-llvm是基于llvm进行编写的一个开源项目(https://github.com/obfuscator-llvm/obfuscator),它的作用是对前端语言生成的中间代码进行混淆,目前在市场上,一些加固厂商(比如360加固宝、梆梆加固)会使用改进的O-llvm对它们so文件中的一些关键函数采用O-llvm混淆,增加逆向的难度。因此,掌握O-llvm的实现过程,是很有必要的。O-llvm总体构架和llvm是一致的,如图1所示。

   Obfuscator-llvm源码分析                            

图1 LLVM总体架构

其中IR(intermediate representation)是前端语言生成的中间代码表示,也是Pass操作的对象,它主要包含四个部分:

(1)Module:比如一个.c或者.cpp文件。

(2)Function:代表文件中的一个函数。

(3)BasicBlock:每个函数会被划分为一些block,它的划分标准是:一个block只有一个入口和一个出口。

(4)Instruction:具体的指令。

他们之间的关系可用图2表示。

Obfuscator-llvm源码分析

图2 IR中各部分的关系

本次源码分析的版本为Obfuscator-llvm-3.6.1,目前O-llvm包含有三个pass,分别是BogusControlFlow、Flattening 和 Instruction Substitution。它们是O-llvm实现混淆功能的核心,具体实现位于llvm-3.6.1/lib/Transforms/Obfuscation/目录下。下面就对这三个pass进行详细的分析。

2. Pass1:BogusControlFlow

BogusControlFlow的功能是为函数增加新的虚假控制流和添加垃圾指令。

2.1 入口函数runOnFunction

BogusControlFlow继承了FunctionPass,因此它的入口函数即为runOnFunction。在runOnFunction函数的具体实现中,首先判断了两个参数的值:ObfTimes和ObfProbRate,分别代表bcf(BogusControlFlow)循环运行的次数和每个basic block被混淆的几率,它们的默认值分别为1和30%。可通过设置参数boguscf-loop、 boguscf-prob修改它们的默认值。

检查完参数的正确性之后,代码接着判断是否包含了启动bcf的命令。在编译程序代码时,若要启动bcf模块,需要带上参数“-mllvm -bcf”。

参数检查完毕之后,首先调用bogus函数。bogus函数首先将本function的所有basicblock存放到一个list容器中,然后使用一个while循环调用addBogusFlow函数对选中的basicblock进行增加虚假控制流。

2.2 addBogusFlow函数

为了更方便的分析源码,本文用一个简单的例子来查看编译时每一步关键的代码执行之后IR图的变化,测试代码如下所示:

  1. int func1(int a,int b)
  2. {
  3. return a+b;
  4. }

该测试代码的func1函数的IR图如图3所示。

Obfuscator-llvm源码分析

图3 func1函数的IR图 

addBogusFlow函数首先调用getFirstNonPHIOrDbgOrLifetime函数获取本basicblock中第一个不是Phi、Dbg、Lifetime的指令的地址(在本例中,即为%a.addr = alloca i32, aling 4的地址),然后调用splitBasicBlock函数。splitBasicBlock函数可根据上述指令的地址将一个basicblock一分为二(可称为first basicblock 和original basicblock)。此时的IR图如图4所示。

Obfuscator-llvm源码分析

图4 分割后的IR图

接着调用createAlteredBasicBlock函数对original basicblock进行拷贝生成一个名为“altered basicblock”的basicblock,并对该basicblock加入一些垃圾指令。加入垃圾指令的方法是遍历该basicblock中的所有OpCode,若包含有Add、Sub、UDiv、SDiv、URem、SRem、Shl、LShr、AShr、And、Or、Xor以及FAdd、FSub、FMul、FDiv、FRem指令,则用随机生成一些指令来进行替换。由于该block在程序运行时并不会执行,因此无需担心插入的指令对原始程序运行的结果产生影响。拷贝original basicblock后,IR图如图5所示。

Obfuscator-llvm源码分析

图5 拷贝后的IR图

这时,所有的basicblock已经准备完毕,一共存在有3个basicblock,需要调整他们之间的关系。首先清除first basicblock和altered basicblock跟父节点的关系,代码为:

  1. basicBlock->getTerminator()->eraseFromParent();
  2. alteredBB->getTerminator()->eraseFromParent();

清除完毕后的IR图如图6所示。

Obfuscator-llvm源码分析

图6 清除父节点后的IR图

接着下一步的操作是增加basicblock之间的条件跳转指令。对于first basicblock(即为图中的entry),bcf源码的做法是先增加一条比较语句 1.0 = = 1.0 ,然后为真时跳转到original basicblock,为假则跳转到altered basicblock。可用伪代码如下表示:

  1. if( 1.0 == 1. 0)
  2. original basicblock
  3. else
  4. altered basicblock

对于altered basicblock模块,在它的尾部增加一条跳转指令,使得当它执行完毕之后(实际上它并不会执行),跳转到original basicblock模块。此时的IR图如图7所示。

Obfuscator-llvm源码分析

图7 增加跳转指令后的IR图

最后,获取original basicblock中最后一条指令的地址(在该例子中即ret指令的地址),调用splitBasicblock函数将original basicblock一分为二(original basicblok和originalBBpart2),然后调用如下代码:

originalBB->getTerminator()->eraseFromParent();

消除original basicblok和originalBBpart2的关系后,再在original basicblock的末尾加入一个判断语句,为真时跳转到ret指令,为假则跳转到altered basicblock,伪代码如下所示:

  1. if( 1.0 == 1. 0)
  2. ret
  3. else
  4. altered basicblock

此时该func1函数的IR图如图8所示:

                   Obfuscator-llvm源码分析

图8 执行完addBogusFlow函数后的IR图

2.3 doF函数

该函数的功能是将Function中所有为真的判断语句进行替换,比如上一节中的“1.0 == 1.0 ”。它的思想是定义两个全局变量x、y并且初始化为0,然后遍历Module内的所有指令,并将所有的FCMP_TRUE分支指令替换为“y<10 || x*x(x-1)%2 ==0”。替换完毕后func1函数的IR流程图如图9所示:

Obfuscator-llvm源码分析 

图9 doF函数执行完毕后的IR图

至此,对func1函数的一次bcf混淆过程就完成了。从该分析也可以看出BogusControlFlow有很多可以改进的地方,这里就不再指出,有兴趣的读者可自行分析修改。

3. Pass2:Flattening

Flattening的主要功能是为函数增加switch-case语句,使得函数变得扁平化。下面就对它的实现源码进行分析。

3.1 入口函数runOnFunction

Flattening继承了FunctionPass,因此它的入口函数即为runOnFunction。在runOnFunction函数的具体实现中,首先判断是否包含了启动fla的命令。在编译目标程序代码时,如要启动fla模块,需要带上参数“-mllvm -fla”。

参数检查完毕之后,调用flatten函数。flatten函数是该Pass的核心,下面对该函数进行分析。

3.2 flatten函数

为了更方便的分析源码,本文用一个简单的例子来查看编译时每一步关键的代码执行之后IR图的变化,测试代码如下所示:

  1. int func1(int a,int b)
  2. {
  3. int result;
  4. if(a>0){
  5. result=a+b;
  6. }
  7. else{
  8. result=a-b;
  9. }
  10. return result;
  11. }

图10是func1的原始IR流程图。从该图可以看出,func1有4个basicblock。

Obfuscator-llvm源码分析

图10 func1原始IR图

flatten函数首先将本Function中除了第一个basicblock外的所有basicblock保存到一个vector容器中。接着对basicblock的数目进行了判断,当basicblock的数目小于等于1时,flatten函数会直接退出并返回false。

接着通过F->begin获取本Function的第一个basicblock,并判断该basicblock是否包含有跳转指令;如果有,再进一步判断该指令是否为条件跳转,若是的话则获取该条件跳转指令的地址,并调用splitBasicblock函数通过该地址将第一个basicblock一分为二。

在本例子中,对func1函数调用splitBasicblock函数之后,此时的IR图如图11 所示。

Obfuscator-llvm源码分析

图11 分割后的IR图

如果不是条件跳转指令(比如for循环),则将跳转指令的目标basicblock存储起来,后面会将该basicblock添加到switch-case中。

接着,将第一个basicblock与下一个basicblock的跳转关系删除,代码为:insert->getTerminator()->eraseFromParent();删除后的IR图如图12所示。

Obfuscator-llvm源码分析

图12 删除第一个basicblock的跳转指令之后的IR图

然后在第一个basicblock的末尾创建一个变量switchVar并赋予它一个随机的值,接着创建三个新的basicblock块,分别为“loopEntry”、“loopEnd”以及“swDefault”,并且设置好它们之间的跳转关系,此时的IR图如图13所示。

Obfuscator-llvm源码分析

图13 设置好基本跳转关系后的IR图

这时,基本的switch-case已经有了,下一步操作是将保存在vector中的每一个basicblock都添加到switch-case语句中,每一个basicblock对应一个case,并且每个case的值都是一个随机值。此时的IR图如图14所示。

Obfuscator-llvm源码分析

图14 增加case后的IR图

添加了全部basicblock块之后,需要修改每个basicblock块之间的跳转关系,使得每个basicblock块执行完毕之后,会重新设置switchVar的值,从而回到switch的判断语句时,能够顺利的跳转到下一个case,直到程序执行完毕。此时的IR图如图15所示。

Obfuscator-llvm源码分析

图15 修改各case之间的关系后的IR图

从图11和图15的差别可以看出,执行Flattening后,函数的多了一些basicblock块,而且函数的核心实现部分均位于同一层,每次执行完一个basicblock块后均要返回loopEntry才能执行下一个basicblock。fla和bcf的互相配合,能大大的提高对函数的混淆效果。

4. Pass3:Substitution

Substitution的主要功能是对程序的一些指令进行替换。

4.1 入口函数runOnFunction

BogusControlFlow继承了FunctionPass,因此它的入口函数即为runOnFunction。在runOnFunction函数的具体实现中,首先判断是否包含了启动sub的命令。在编译程序代码时,如要启动sub模块,需要带上参数“-mllvm -sub”。sub模块还支持多次循环操作,可通过参数“-mllvm –sub-loop=xx”显式的设定循环次数,默认为1。

参数检查完毕之后,调用substitute函数。substitute函数的功能是遍历Function内的每一个指令,对符合要求的指令进行替换。

4.2 Substitution函数

该函数的实现主要是依靠最外层的do-while循环和两个for循环。do-while循环主要是根据设定的sub循环次数运行两个for循环。外层for循环是遍历本Function中的每一个basicblock,里层for循环是遍历basicblock中的每一个指令,接着采用一个switch-case语句来对不同的指令进行不同的操作。目前,sub支持五种指令的替换,分别是“Add”、“Sub”、“And”、“Or”以及“Xor”指令。

Add”指令支持4种替换方法,分别是a = b - (-c)、a = -(-b + (-c))、r = rand (); a = b + r; a = a + c; a = a – r 、r = rand (); a = b - r; a = a + b; a = a + r 。

“Sub”指令支持3种替换方法,分别是a = b + (-c)、r = rand (); a = b + r; a = a - c; a = a – r 、r = rand (); a = b - r; a = a - c; a = a + r 。

“And” 指令支持2种替换方法,分别是a = b & c => a = (b^~c)& b 、a = a & b <=> !(!a | !b) & (r | !r) 。

“Or” 指令支持2种替换方法,分别是a = b | c => a = (b & c) | (b ^ c) 、a | b => [(!a & r) | (a & !r) ^ (!b & r) |(b & !r) ] | [!(!a | !b) & (r |!r)] 。

“Xor” 指令支持2种替换方法,分别是a = a ^ b => a = (!a & b) | (a & !b) 、a = a ^ b <=> (a ^ r) ^ (b ^ r) <=> (!a & r | a & !r) ^ (!b & r | b & !r) 。

在substitute函数的switch-case中,程序会随机的调用这些替换方法,部分代码如图16所示。

Obfuscator-llvm源码分析

图16 替换指令的代码

例如,Add指令中,funcAdd是个函数数组,里面存储了NUMBER_ADD_SUBST个替换add指令的函数,get_range是个获取随机数的函数,通过这种方法,可使替换的add具有一定的随机性。对于其他的指令,也是采用类似add指令的方式进行替换的。

5. 改进建议

由于O-llvm的开源性,大家如果要使用该产品的功能,可以在它的基础上做一些修改。

比如在BogusControlFlow中,对于跳转指令为真的分支,O-llvm采用如下指令进行替换“y<10 || x*(x-1)%2==0 ”,使用IDA打开混淆后的so文件可轻易的发现该特征。因此,我建议事先准备多条可以等价替换的指令,在遇到需要替换的地方时,随机的选取其中一条等价指令进行替换。对于basicblock块的划分,也可以采用其他规则来进行划分,大家可以脑洞大开,多尝试尝试。

在Substitution中,我们也可以采用其他的等价指令进行替换,这里也不再举例了。

6. 最后   

前段时间,有人在看雪论坛发布了一篇名为《ollvm的混淆反混淆和定制修改》的文章(http://bbs.pediy.com/thread-217727.htm),大家也可以阅读下该文章,加深对O-llvm的了解。

同时,网易云安全(易盾)也提供Android应用加固iOS应用加固服务,

本文来自网易云社区,经作者王泽华授权发布。

更多网易研发、产品、运营经验分享请访问网易云社区。 

            </div>