Obfuscator-llvm源码分析

越来越多的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所示。

                               

1 LLVM总体架构

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

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

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

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

4Instruction:具体的指令。

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

2 IR中各部分的关系

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

2. Pass1BogusControlFlow

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

2.1 入口函数runOnFunction

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

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

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

2.2 addBogusFlow函数

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

int func1(int a,int b)
{
       return a+b;
}

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

3 func1函数的IR

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

4 分割后的IR

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

5 拷贝后的IR

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

basicBlock->getTerminator()->eraseFromParent();
alteredBB->getTerminator()->eraseFromParent();

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

6 清除父节点后的IR

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

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

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

7 增加跳转指令后的IR

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

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

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

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

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

                  

8 执行完addBogusFlow函数后的IR

2.3 doF函数

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

 

9 doF函数执行完毕后的IR

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

3. Pass2Flattening

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

3.1 入口函数runOnFunction

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

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

3.2 flatten函数

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

int func1(int a,int b)
{
     int result;
     if(a>0){
            result=a+b;
     }
     else{
            result=a-b;
     }
     return result;
}

10func1的原始IR流程图。从该图可以看出,func14basicblock

10 func1原始IR

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

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

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

11 分割后的IR

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

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

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

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

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

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

14 增加case后的IR

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

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

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

4. Pass3Substitution

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所示。

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 应用加固服务,

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