初识OLLVM
作者:互联网
编译器
一般编译器分为前端,中间优化和后端三部分。前端进行语法分析,中间进行优化后由后端编译成对应平台的代码(arm,x86)。现在主流的编译器有linux平台下的gcc 和 llvm-clang,以及windows平台下的msvc编译器。
LLVM
gcc编译器虽然强大但是有一个缺点就是因为其相当于一个完整的可执行文件,编译器的前端,中间优化和后端中间的耦合度比较高,所以要想增加一个前端用来支持一种新的语言,或者增加一个后端来生成一种新平台的机器码都需要做很大的改动很不方便。于是就有了LLVM的出现,LLVM是一个编译器框架,和传统的编译器一样也分为前端,中间优化和后端三部分。但是其利用与平台无关的中间语言IR来实现前端,中间优化和后端的分离,这样如果编译器需要新增加一种语言只需要增加一个前端语法解析器即可。例如Clang就是基于LLVM架构的能够支持C/C++/Objective-C语言的编译器前端。
LLVM编译过程
llvm编译过程大致分为 预处理-->语法分析-->生成IR代码-->IR代码优化-->生成汇编代码(x86, arm)-->Link-->生成目标文件,之后就是链接器链接各个目标文件并生成可执行文件的过程。(所以汇编的过程实际包含在编译过程中)
LLVM IR代码文件的格式为bc/llc,bc是一种bitcode的格式,而llc是一种人类可以阅读的格式。还有一种格式就是内存中的格式,LLVM在内存中会将IR分为Module,function,basicblock,Instruction四种表达形式。
- Module就是一个c/cpp文件
- function就是一个函数
- basicblock就是控制流的一个基本块
- Instruction就是一条具体的指令
LLVM中间优化过程
llvm通过将前端代码(c/cpp等)生成与平台无关的llvm IR中间代码,然后通过多个pass(就是一个一个的类,每一个类对应一种功能)来对IR代码进行优化。
OLLVM
OLLVM是一种通过利用LLVM回生产IR中间代码并通过pass优化代码的特点,通过增加自己的pass来对代码进行优化,但是这种优化不是为了让代码更简洁相反是让代码更复杂,达到混淆的目的。一般支持三种主要的混淆手段:控制流平坦化,虚假执行流和替换指令。
OLLVM之控制流平坦化
ollvm三种混淆手段中控制流平坦化混淆效果最佳。其原理是将函数分为若干个控制流基本块,这些基本块是以跳转指令(不包含调用指令)结尾的,然后利用switch结构通过判断状态变量的值来执行相应的控制流基本块,因为一个基本块可能会跳转到一个或者两个基本块中,所以还需要通过新增加一些块或代码来控制修改状态变量的值从而跳转到不同的基本块中。
- 序言:通常是一个函数开头的部分
- 主分发器/子分发器:通过状态变量寻找对应的真实快
- 预处理器:可有可无,会统一跳转回主分发器中(有很多直接从真实块中跳转回主分发器中)
- 真实块:包含函数实际的代码基本块
- return块:函数结束代码块
去除ollvm控制流平坦化
根据ollvm控制流平坦化混淆的原理可得接混淆的步骤如下。
- 先找到函数中所有的控制流代码块
- 在所有的控制流代码块中找到所有的真实块
- 确定各个真实块的执行路径,即一个执行块能跳转到哪几个块(1/2个块)
- 对于只会跳转到一个块的真实块直接在其代码块末尾patch,跳转到对应的真实块
- 对于会跳转到两个块的真实块,平坦化是通过在此真实块的末尾根据跳转条件更改状态变量的值来实现跳转到不同的真实块中,我们需要将其更改状态变量的跳转到不同真实块的方式更改为通过跳转指令的方式实现(bge,ble...)。我们需要根据实际的修改状态变量的指令来使用对应的跳转指令例如其通过
ITT GT
我们就可以修改为BGE
. - 将控制流代码块中不是真实块的块直接nop(虚假块)
当然在实际分析中情况很复杂,可能有很多特殊的情况需要特殊处理,大致流程一样。
利用unicron模拟执行去除控制流平坦化
测试程序是一个arm-v7a(thumb)的程序
混淆前的代码流程图
混淆后的代码流程图
参考看雪无名侠大佬的思路
- 寻找所有的控制流代码块
我们观察到控制流代码块都需要以跳转指令(不包括blxx等函数调用指令)结尾。记录下所有基本快后继块的引用计数。
- 寻找真实块
如果一个基本控制流代码块的后继块的引用数大于1,并且不等于2则其就是一个真实块。因为引用数大于1的基本块是预处理器或者主分发器(引用数为2),而指向预处理器的是真实块,而预处理器指向的主分发器的引用数也大于1(引用数为2,一个是序言,一个是预处理器),而预处理器不是真实块所以需要将预处理器过滤。
因为在过滤预处理的时候也把序言过滤了,序言其实也属于真实块,所以需要加上。
观察到return块是一个以bne开头的代码块,也属于真实块需要加上。(所以在实际寻找真实块的过程中有很多需要特殊处理的过程,具体情况具体分析)
-
寻找各个真实块跳转的目标真实块
从函数序言的起始地址开始模拟执行,执行的过程中只关心此真实块能够执行到的目标真实块,其他内存相关操作都不需要关心。 -
先判断真实块中是否含有跳转指令,有跳转指令说明此真实块能够执行到两个目标真实块(两条路径),需要寻找两次路径
-
如果没有跳转指令则只有一条路径需要寻找
-
寻找到真实块加入到队列中,待此真实块的路径寻找完后从队列中去除新的真实块并以此块的地址作为起始地址继续模拟执行寻找它的路径。直到所有的真实块的路径都找完为止。
寻找一个真实块路径的思路是:如果模拟执行到一个地址是某个真实块的起始地址说明找到了目标真实块。
对于包含条件判断的真实块,也就是有两条路径的真实块需要人为的控制指令指令流程寻找两条路径对应的真实块。为了更好的在后续patch,我们每次都先寻找满足较小条件会执行的路径。
- 最后就是patch程序,根据不同的条件判断指令进行修改对应的跳转指令
还原后的程序流程图
参考链接:https://bbs.pediy.com/thread-252321.htm
标签:真实,代码,控制流,编译器,初识,指令,跳转,OLLVM 来源: https://www.cnblogs.com/revercc/p/16336307.html