OLLVM笔记_1—环境编译及BCF学习
工作原因,最近开始学习OLLVM,后边需要写一些pass给用来做混淆。因为主要是做混淆,所以OLLVM的整体代码并不在自己的学习范围内,主要是学习pass的原理及编写,后期也会根据学到的东西给OLLVM做对抗。目前计划学习的内容:
OLLVM一些源码上的学习及函数调用。
OLLVM的混淆原理。
pass原理及编写。
一、项目
目前学习有两个项目:
1、https://github.com/obfuscator-llvm/obfuscator
2、https://github.com/emc2314/YANSOllvm
第一个是官方项目,也就是OLLVM作者对LLVM衍生的新物,第二个是其他大佬自己魔改的Pass。选择这两个项目是因为学网上大多教程都是围绕官方的代码去做讲解,这样对于0基础来说学习成本能降低,其二就是可以通过其他魔改的项目进一步对pass编写的手法和思路进行学习。
二、编译
- Ubuntu 16.4
编译环境最好选择Linux,因为Window看了大多文章都是天坑巨多,我不想死在第一步(摊手)。
直接拉第一个项目后根据wiki进行编译。
1 | |
这里我输入的-DCMAKE_BUILD_TYPE为Debug,官方的wiki是Release。这里选择Debug是因为从开发角度&学习角度来说,Debug肯定是比较适合学习的。最后就是make,我这里因为给虚拟机的配置很高,所以无所吊谓。
等了N年,编译完了。(这里建议大家还是给虚拟机大一点内存,不然会出现内存爆炸导致编译失败,我这次编译的时候给了8g还是炸了,改到16g才跑通)
三、OLLVM介绍
O-llvm是基于llvm进行编写的一个开源项目(https://github.com/obfuscator-llvm/obfuscator),它的作用是对前端语言生成的中间代码进行混淆(反优化),目前在市场上,一些加固厂商(比如360加固宝、梆梆加固)会使用改进的O-llvm对它们so文件中的一些关键函数采用O-llvm混淆,增加逆向的难度。因此,掌握O-llvm的实现过程,是很有必要的。O-llvm总体构架和llvm是一致的,如下图所示。
LLVM总体架构
其中IR(intermediate representation)是前端语言生成的中间代码表示,也是Pass操作的对象,它主要包含四个部分:
Module:比如一个.c或者.cpp文件。
Function:代表文件中的一个函数。
BasicBlock:每个函数会被划分为一些block,它的划分标准是:一个block只有一个入口和一个出口。
Instruction:具体的指令。
他们之间的关系可用下图表示。
IR中各部分的关系
本次源码分析的版本为Obfuscator-llvm-4.0(官方维护到的最后一个版本),目前O-llvm包含有三个pass,分别是BogusControlFlow、Flattening 和InstructionSubstitution。它们是O-llvm实现混淆功能的核心,具体实现位于src/lib/Transforms/Obfuscation/目录下。
四、BCF(BogusControlFlow)虚假控制流
srv/lib/Transforms/Obfuscation/BogusControlFlow.cpp
打开文件后,头部的注释的描述可以看看,这是官方对BCF的一个原理描述,这里直接贴出(不翻译了,练一下英语)。
1 | |
字面意思,在原控制流上造出几个假的流程,在真实执行的时候不会执行这些分支,纯属充当搅屎棍。打开文件后可以看到BogusControlFlow继承于FunctionPass,表明这是一个函数的pass,并且重写了runOnFunction函数。该函数为入口函数,传入的对象实际上就是被结构化的函数实体,所有的处理都从这里开始。
函数首先是判断了传参ObfTimes和ObfProbRate是否合法(这两的含义看代码开头的注释),接着就是调用一系列函数进行混淆。
4.1 toObfscate
这个函数相对比较简单,主要逻辑如下:
如果函数为
预声明或者外部链接(extern)的则直接返回false。局部混淆,判断函数是否添加了配置
int func() __attribute((__annotate__(("参数三"))));,如果存在则混淆。(若从命令参数指定-boguscf则为全局混淆)
4.2 bogus
这里就是开始处理函数了。起手就是一个大循环do{...}while,循环的次数为输入的ObfTimes。这里只分析单次循环,剩下的循环其实也就是重复混淆(相当于混淆嵌套)。
首先遍历函数的所有存放到vector容器中。
接着while循环遍历弹出vector容器的BB,然后调用addBogusFlow(basicBlock, F);开始混淆。
4.3 addBogusFlow
真正实现混淆的地方。首先调用getFirstNonPHIOrDbgOrLifetime函数获取一个非PHI Node。
这里简单说一下PHI是啥,首先LLVM的IR是以SSA(Static Single Assignment,静态一次性赋值)形式存在,即每个变量只能被赋值一次。但在有分支和循环的程序中,同一个变量可能在不同路径上被赋予不同的值,所以PHI就是为了解决这种问题,引用Q神的解释就是类似三元运算符。
来自Q神的图片
如果PHI Node(check_for_condition)的前驱基本块是entry,则将current_i赋值为2,如果是for_body,则赋值为%i_plus_one。(从Q神的文章里还是没太理解,因为从后续解释没看出和这个有啥关系,反正知道他切割这玩意就行了=。=)
接着调用函数splitBasicBlock以il为界限切割,这样就得到了两个BB,其中Twine可以看做字符串,即新产生的BasicBlock的名称。
splitBasicBlock:从原基本块保留从开始到分割点之前的所有指令。新基本块包含从分割点到原基本块末尾的所有指令。
执行这个步骤后,代码块变成下列图示:
开始进行核心逻辑。
以originalBB为模板调用createAlteredBasicBlock创建了一个alteredBB,函数原理这里我不做解释,具体可以看Q神文章,这里先看他是怎么处理这个新BB。现在得到的代码块如下:
接着调用eraseFromParent进行断链,得到如下代码块:
创建了一条if(1.0f == 1.0f)的语句,将其连接到originalBB和alteredBB上,并设立条件成立的时候执行originalBB。
代码块此时变成如下:
最后进行收尾工作。
获取
originalBB的终端(这个终端不是那个控制台,而且尾部的意思)进行切割。断链。
永真表达式连接。
得到下图代码块:
BCF的实现原理通过这个函数基本就理清了,就是对每个真实块进行copy后使用永真表达式对真实块连接,拷贝的块充当搅屎棍,里边添加一些恶心指令就行,但是还有一个问题,那就是if表达式太简单了,扔进IDA F5就能直接优化,所以还需要一些操作。
4.4 doF
这个函数就是用来给永真表达式复杂化。函数逻辑就是使用不透明谓词和(y < 10 || x * (x + 1) % 2 == 0)进行表达式替换。
不透明谓词:人一眼就知道这个条件是true还是false。比如一个全局变量X,在编写代码时只定义但不进行二次赋值,那么在程序的整个运行期间都是0。如果在程序中的if表达式中引用了这个全局变量X
if(X),我们都知道他这个if不会成立,但是机器不行,程序没运行时X的值不确定,导致无法进行判断条件结果。因此IDA加载的时候就无法对if进行优化,只能老老老实实显示分支。
创建两个全局变量X,Y。
遍历所有永真表达式,由于doF传入的参数是Module,因此这里会收集到所有永真表达式,其中cond容器是原始表达式,tbb容器可以理解为指令前缀(cmp)。
进行替换。
简单解释一下代码。
- 557~579:创建
(y < 10 || x * (x + 1) % 2 == 0)表达式,然后与i的前继块链接。
- 581:断链 i 与
前继块的链接。
- 587:删除旧表达式。
图画出来实际上还是有点意思上的偏差,但是流程大致是这样的,理解一下就行。
五、总结
BCF相对来说还是比较简单的,但是设计的思路确实很巧妙,通过克隆原BB+永真表达式就可以实现一些静态分析干扰,自己通过倒立自摸的方式也是想出一些简单的魔改思路:
在替换表达式的时候,可以根据某种规律进行控制条件,比如当枚举下标为偶数时,
BranchInst::Create的逻辑创建可以改为con==false是跳转到originalBB。不透明谓词可以做一些校验,比如必须去执行一些
alteredBB,去初始化不透明谓词,然后带入下次的表达式计算,但让这个思路可能会遇到两个坑:createAlteredBasicBlock不保证克隆出来的BB中的所有指令正确,这个需要去验证测试。当前执行的
alteredBB与下一个表达式计算的关联性,否则会导致下一次的计算有误。
对抗上的话,感觉unicorn就能跑通,刚好手上也写了模拟器,只需要给表达式打点即可,但是实战>意淫。我认为一个正儿八经加了OLLVM的东西不可能只存在bcf,肯定是bcf和fla,因此目前没啥思路,纯扣666+卖惨嫖脚本(XD。