ollvm分析 part0x01

ollvm分析 分割基本块以及指令替换

在之前打了一些基本功之后,现在可以进行分析ollvm的混淆实现了,首先,查看下整个混淆器用到的头文件

图片.png-34.9kB

本着从易到难的理念,我们首先看的是指令替换和分割基本块这两个类,但是在那之前,这几种混淆中都会用到util这个类里面的内容,所以我们的首要任务还是分析util这个头文件以及这个类
图片.png-47.5kB
主要声明了三个函数,fixStack,readAnnotate和toObfuscate这三个函数,其他导入头文件就不去深究了,现在去util.cpp 中看下这三个方法的实现,首先是fixStack,在之前我们已经学习过了phi结点的转换,大概意思就是llvm ir用alloca,load。Store指令去完成了这一系列操作,先贴下fixStack的代码
图片.png-99.6kB
看注释就可以知道,这个函数他删除了phi结点,并且把SSA寄存器的值放回了栈中,从头开始撸一遍代码吧,申请了两个vector去暂存phi结点和SSA寄存器的值,然后开始遍历全部函数的基本块(基本块是组成llvm ir的最基本单位),依次检查了该基本块是否为phi结点和内部是否包含使用alloca去申请栈空间的ir指令,如果有的话,就会把包含这些东西的基本块全部入栈,其实就是把SSA寄存器里的值全部放回栈中(从之前我们可以了解到,clang前端生成的llvm ir并不是SSA的,通过DemotePHIToStack和mem2Reg这两个pass将ir优化成了SSA的),所以说,这里更多的类似一种反优化,把之前SSA的代码变回了不是SSA的,方便进行混淆操作(上述是猜测),然后就是util的第二个函数readAnnotation了
图片.png-122.4kB
其实这个函数没必要去细分,本质上就是读取了之前对函数做的一些注释,并且返回注释,用来判断是否开启混淆的,然后看下最后一个函数,
图片.png-102.2kB
这个函数很短,没有几行,主要是给别的一些具体的混淆方法来使用的,比如传入了bcf参数,类似toObfuscate(flag,fun,xxx),然后就会判断当前函数是否已经开启了当前这种混淆方式,分析下实现可以知道,是读取了传入的函数的annotation,然后对比字符串实现的,如果已经开启了,则把flag置true,如果没有就置false。从函数注释可以体验下,比如toObfuscate(flag,fun1,“fla”),就会判断当前函数是否已经被控制流平坦化处理过。
下面就可以看下具体的混淆方法了,最简单的应该就是splitBasicBlock这个pass了,一言以概之,就是把一个正常的基本块分成了两个,可以称作first basicblock和original basicblock,说起来比较抽象,写个函数去试一下就好
图片.png-58.6kB
样例程序为这个,我们首先不分割基本块,查看下cfg是啥样的,首先通过clang去生成他的中间语言的字节码,
图片.png-23kB
然后我们就得到了llvm ir的字节码,也就是可以看到.bc文件,这里不得不提一下,llvm是一款具有just in time也就是jit的优秀编译器,它有自己的中间语言的解释器,也就是lli,通过lli xxx.bc可以直接执行目标程序,这是十分方便的,llvm的中间语言的文件后缀为ll,中间语言转化成可供lli执行的字节码的文件后缀为bc,bc和ll之间可以相互转换,好了话归正题,我们现在去看下吧!通过opt命令绘制出cfg(control flow graph程序控制流图)的点阵,然后去绘制下点阵就ok
图片.png-100.2kB
这样我们就可以得到了刚刚写的程序的控制流图了,去看下吧!
图片.png-39.8kB
可以看到是一整块的,然后让我们现在进行分割基本块之后再看下cfg图观察下有多大的改变。编译指令和之前十分相似,只是在后面加上了选用分割基本块的标志,-mllvm -split,其实还可以在split后面跟上分割基本块的数目,数目大小是0-10,先看下默认分割成一块的,
可以看到同样是add函数,之前的一大块add,已经被分割成了三块,分割其实也没什么特别的,单纯的跳转而已,但是这样子的话,如果对简单的单纯的跳转加以混淆的话,那么结果就不一样了,这也是为什么之后的虚假控制流中第一步要先分割基本块,在我看来,是为了分割出前缀,也就是谓词,然后对这个跳转改变成较为复杂的,也就是经常学术上所说的不透明谓词混淆方法,关于不透明谓词,我打算在学习到bcf混淆方式的具体研究,看完了效果,可以去细细分析下实现了,
首先看下splitBasicBlock的头文件
十分的短小,主要就是导入了llvm的一些头文件,并且定义了名为llvm的命名空间,下面再看下分割基本块的实现,
图片.png-40.4kB
首先可以看到,这个文件除了用到自己的头文件和util头文件之外还用到了CryptoUtil这个文件,去看了下,这个类主要提供了aes加密算法和sha签名方法供开发者使用,当然如果你自己需要使用可以加入一些别的。继续看源码
图片.png-42.9kB
可以看出来,这个pass的入口点是runOnFunction,然后它定义依次声明了runOnFunction,split,containsPHI以及shuffle这几个函数,依次观察
图片.png-11.4kB
首先splitNum是一个全局变量,它的数值等于你编译时输入的分割基本块的数目,判断是否属于1-10之后,把当前函数传入toObfuscate判断是否已经开启过分割基本块,如果没有,就继续进行split函数的处理,看下split函数的源代码
图片.png-74.8kB
首先申请了一个向量用来保存函数中的所有基本块,然后再把基本块分割成一条条指令进行操作,如果,当前的基本块只有两条instruction或者包含有phi结点,则不进行分割操作,然后判断下splitN和当前基本块的大小,如果要分割的数目比当前基本块包含的指令数目要多,则重新设置下splitNum为基本块的大小减一(其实也很好理解,每个基本块至少包含一条instruction啊),然后就开始生成分割基本块的结点,生成结点后使用shuffle打乱
也就是,分割的结点是随机的,确定好分割结点后,最后调用splitBasicBlock进行分割,这时候,我们去官方文档仔细看下这个函数
图片.png-65.2kB
可以看到有两个参数,第一个是指令I,第二个应该就是生成的基本块的名字,大致可以看出,是从随机生成的第n条指令开始分割基本块,因此,每一次分割基本块的内容都是随机的,可以看到,同一个add函数,我进行了两次分割的结果是不一样的(下图为第二次分析结果)
图片.png-35.2kB
关于分割基本块的分析就结束了,下面我们分析下最简单的混淆,指令替换,就是把程序中使用到的加减以及随机之类的指令,使用更复杂的指令替换,首先我们需要看下效果,代码还是add不变
图片.png-48.7kB
由于ollvm的指令替换的模式是随机的,这里我替换了两次以以展示出更好的结果
图片.png-17.3kB
可以看到,之前只是简单的加法,现在被替换成了加上一个大数并且减去一个大数。。。可以说,这种混淆方式如果不配合其他两种混淆方式一起使用的话安全性是十分差的,现在我们去分析一下它的头文件
图片.png-10.3kB
依旧十分短小,唯一的作用就是申请了llvm这个命名空间,现在去看下实现的c文件
图片.png-73.7kB
首先是导入了必要的头文件,以及申请一些全局变量,我们还可以得知,编译指令中使用sub_loop可以实现多次替换,并且替换次数等于你loop的次数,并且最终会复制给obfTimes,并且指令替换替换的指令有五种,add,sub,and,or和xor,然后我们直接去看它的runOnFunction函数,可以看到类似于之前的分割基本块,读取了annotation之后传入substitute函数进行替换,substitute函数主要是switch的形式,遍历每一个函数的每一条指令进行选择,读取指令的op,如果属于替换的五种之一,就传入对应的funcXXX函数进行替换,还有一个参数就是从llvm的加密工具类中生成的随机数(之前变换的加法中可以看到一个较大的随机数),由于指令替换比较单一,我们支取分析funcAdd函数作为代表即可
图片.png-67.4kB
下面我们看下funcAdd函数,其实funcAdd是一个函数数组
图片.png-2.6kB
代表4种不同的替换方法,具体的替换内容可以去ollvm的官方wiki中查看(https://github.com/obfuscator-llvm/obfuscator/wiki/Instructions-Substitution),这里我们详细分析下,addNeg顾名思义是取反后加,也就是a+b=a-(-b),看下实现
图片.png-46.1kB
传入的参数是含有add的指令,首先判断了opCode是否为add,如果是的,则使用creatNeg取反当前指令的操作数,并且creat出一条sub指令,operand就是之前创造出的操作数,最后使用replaceAllUsesWith函数替换之前的指令,我们再去分析下addDoubleNeg这个函数,顾名思义就是对两个操作数都取反,表达是的话a+b=-(-b-a)本质上没变但是比单独取反复杂度更高,可以看到比addNeg只是多了次取反操作
图片.png-40.3kB
第三个就是addRand了,也就是我之前测试时使用的混淆方式,就是加上一个rand随机数后再减去,
图片.png-96.8kB
校验运算符为加法后,首先定义一个即使整数co保存加密类中获得的随机数,然后用create函数生成一条加法指令,操作数为随机数,也就是a=a+rand,然后再加上之前的操作数,也就是a=a+rand+b,最后再减去之前那个随机数,也就达到了目标的效果。再看看最后一种替换方式addRand2,可以看到大致上与addRand相同,唯一的不同就是先减去了随机数rand然后加上了随机数r,至此,我们分隔基本块和指令替换的分析到此结束,后面会分析ollvm的两大特色,一个就是bcf(虚假指令流),另一个就是fla(控制流平坦化)。
图片.png-60.6kB