越来越多的so文件采用了llvm进行加固,逆向的小伙伴表示不能愉快的玩耍了。本文对Obfuscator-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操作的对象,它主要包含四个部分:
(1)Module:比如一个.c或者.cpp文件。
(2)Function:代表文件中的一个函数。
(3)BasicBlock:每个函数会被划分为一些block,它的划分标准是:一个block只有一个入口和一个出口。
(4)Instruction:具体的指令。
他们之间的关系可用图2表示。
图2 IR中各部分的关系
本次源码分析的版本为Obfuscator-llvm-3.6.1,目前O-llvm包含有三个pass,分别是BogusControlFlow、Flattening 和 Instruction Substitution。它们是O-llvm实现混淆功能的核心,具体实现位于llvm-3.6.1/lib/Transforms/Obfuscation/目录下。下面就对这三个pass进行详细的分析。
BogusControlFlow的功能是为函数增加新的虚假控制流和添加垃圾指令。
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进行增加虚假控制流。
为了更方便的分析源码,本文用一个简单的例子来查看编译时每一步关键的代码执行之后IR图的变化,测试代码如下所示:
int func1(int a,int b)
{
return a+b;
}
该测试代码的func1函数的IR图如图3所示。
图3 func1函数的IR图
addBogusFlow函数首先调用getFirstNonPHIOrDbgOrLifetime函数获取本basicblock中第一个不是Phi、Dbg、Lifetime的指令的地址(在本例中,即为%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,若包含有Add、Sub、UDiv、SDiv、URem、SRem、Shl、LShr、AShr、And、Or、Xor以及FAdd、FSub、FMul、FDiv、FRem指令,则用随机生成一些指令来进行替换。由于该block在程序运行时并不会执行,因此无需担心插入的指令对原始程序运行的结果产生影响。拷贝original basicblock后,IR图如图5所示。
图5 拷贝后的IR图
这时,所有的basicblock已经准备完毕,一共存在有3个basicblock,需要调整他们之间的关系。首先清除first basicblock和altered 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 basicblok和originalBBpart2),然后调用如下代码:
originalBB->getTerminator()->eraseFromParent();
消除original basicblok和originalBBpart2的关系后,再在original basicblock的末尾加入一个判断语句,为真时跳转到ret指令,为假则跳转到altered basicblock,伪代码如下所示:
if( 1.0 == 1. 0)
ret
else
altered basicblock
此时该func1函数的IR图如图8所示:
图8 执行完addBogusFlow函数后的IR图
该函数的功能是将Function中所有为真的判断语句进行替换,比如上一节中的“1.0 == 1.0 ”。它的思想是定义两个全局变量x、y并且初始化为0,然后遍历Module内的所有指令,并将所有的FCMP_TRUE分支指令替换为“y<10 || x*x(x-1)%2 ==0”。替换完毕后func1函数的IR流程图如图9所示:
图9 doF函数执行完毕后的IR图
至此,对func1函数的一次bcf混淆过程就完成了。从该分析也可以看出BogusControlFlow有很多可以改进的地方,这里就不再指出,有兴趣的读者可自行分析修改。
Flattening的主要功能是为函数增加switch-case语句,使得函数变得扁平化。下面就对它的实现源码进行分析。
Flattening继承了FunctionPass,因此它的入口函数即为runOnFunction。在runOnFunction函数的具体实现中,首先判断是否包含了启动fla的命令。在编译目标程序代码时,如要启动fla模块,需要带上参数“-mllvm -fla”。
参数检查完毕之后,调用flatten函数。flatten函数是该Pass的核心,下面对该函数进行分析。
为了更方便的分析源码,本文用一个简单的例子来查看编译时每一步关键的代码执行之后IR图的变化,测试代码如下所示:
int func1(int a,int b)
{
int result;
if(a>0){
result=a+b;
}
else{
result=a-b;
}
return result;
}
图10是func1的原始IR流程图。从该图可以看出,func1有4个basicblock。
图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才能执行下一个basicblock。fla和bcf的互相配合,能大大的提高对函数的混淆效果。
Substitution的主要功能是对程序的一些指令进行替换。
BogusControlFlow继承了FunctionPass,因此它的入口函数即为runOnFunction。在runOnFunction函数的具体实现中,首先判断是否包含了启动sub的命令。在编译程序代码时,如要启动sub模块,需要带上参数“-mllvm -sub”。sub模块还支持多次循环操作,可通过参数“-mllvm –sub-loop=xx”显式的设定循环次数,默认为1。
参数检查完毕之后,调用substitute函数。substitute函数的功能是遍历Function内的每一个指令,对符合要求的指令进行替换。
该函数的实现主要是依靠最外层的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指令的方式进行替换的。
由于O-llvm的开源性,大家如果要使用该产品的功能,可以在它的基础上做一些修改。
比如在BogusControlFlow中,对于跳转指令为真的分支,O-llvm采用如下指令进行替换“y<10 || x*(x-1)%2==0 ”,使用IDA打开混淆后的so文件可轻易的发现该特征。因此,我建议事先准备多条可以等价替换的指令,在遇到需要替换的地方时,随机的选取其中一条等价指令进行替换。对于basicblock块的划分,也可以采用其他规则来进行划分,大家可以脑洞大开,多尝试尝试。
在Substitution中,我们也可以采用其他的等价指令进行替换,这里也不再举例了。
前段时间,有人在看雪论坛发布了一篇名为《ollvm的混淆反混淆和定制修改》的文章(http://bbs.pediy.com/thread-217727.htm),大家也可以阅读下该文章,加深对O-llvm的了解。
同时,网易云安全(易盾)也提供Android 应用加固和iOS 应用加固服务,
本文来自网易实践者社区,经作者王泽华授权发布。