reverse with obfuscation

不知道从什么时候开始,突然开始深入研究reverse去了。。。这里记录一种reverse常见的问题以及相关框架的使用方法:

reverse with obfuscation

什么是 obfuscation

obfuscation,也就是混淆,其意义就在于将代码段的逻辑变得晦涩难懂,使人再静态甚至动态调试的时候无法了解程序本身的意图,从而增加逆向分析的难度。

ollvm

ollvm 是一个基于llvm的项目。llvm是一个非常有趣的编译器,人们可以通过研究其,对代码的编译本身以及其发展应用产生巨大的帮助。这个ollvm也是利用这个编译器产生的对代码混淆的项目。ollvm会在编译期间,将代码翻译成复杂(而不是想正常的编译器对其进行优化)的汇编,从而让程序流变得异常复杂,以至于不能分析。

混淆的方式

ollvm提供了多种混淆方式。这里我们介绍三种常见的混淆方法

替代(Instructions Substitution)

这种方式即使将我们普通运算中的运算替换成复杂的运算符。这种处理方法其实很容易被破解,但是由于有些运算会引入随机数,让取出这个混淆代码变得也不是那么容易。
比如说,a - b ==> r = rand (); a = b + r; a = a - c; a = a - r

使用方法:

  • -mllvm -sub: 使用替代的混淆方法
  • -mllvm -sub_loop=3: 反复使用替代的方法3次

程序流扁平化(Control Flow Flattening)

什么叫做程序流扁平化?就是说,一般的程序中会有很多的条件判断(if,for,while等)。这些逻辑的出现,让整个程序可读性上升了很多。而扁平化处理后的程序,将这些条件判断去掉,每一个分支条件块放入一个等价的块中,比如说C语言的switch,并且整个流程放入一整个循环中。此时就会变得难以查看这个程序的逻辑。这个将整个由上至下的程序放在同一个平面上的处理就叫做扁平化

如上就是一个经典的扁平化处理

使用方法:

  • -mllvm -fla: 开启程序流扁平化处理
  • -mllvm -split: 开启程序基本块切割. 如果在使用扁平化的同时使用这个声明,那么扁平化的效果就会更好
  • -mllvm -split_num=3: 这个会将程序基本块进行三次切割。

控制流伪造(Bogus Control Flow)

这个伪造控制流的方式,是通过在正确的程序块前加入一些虚假的程序块(opaque predicate),但是其并不影响程序整个的进程。就好像之前出题的时候使用过的花指令一样。虽然我们有jz和jnz两个跳转方式,但是实际上到达这个位置的时候,只会触发jz的内容,从而增加逆向的难度。

使用方法:

  • -mllvm -bcf: 使用控制流伪造
  • -mllvm -bcf_loop=3: 如果使用了控制流伪造的话,那么将这个功能使用三次
  • -mllvm -bcf_prob=40: 如果开启了控制流伪造的话,那么有40%的可能进行混淆

混淆方式总结

对于任意的一种混淆的方法,必定是有几个共同的内容

如果使用的是替代的方法,那么我们初始化的内容中初始化混淆过的内容在虚假的程序流程中往往是不参与关键数据的赋值,又或者是先+后-这样进行的处理。
而如果使用的是扁平化呢,则可能会出现一个循环里面包括着switch,然后利用一个数组对其进行程序块选择。

Miasm2 – 逆向框架

Miasm2是目前正在学习的一种逆向框架。这个框架能够将程序段以类似IDA的程序块的形式显示出来,并且在分析被混淆的代码段的时候,我们可以利用这个框架将混淆后的代码去除,从而理顺程序整体逻辑。

(目前正在瞎研究。。。所以只知道了部分功能)

Sandbox

Miasm2 支持使用一个沙盒环境进行程序模拟运行。支持pe和elf,以及arm,mips指令集下的文件执行
一个简单的模拟运行的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
from miasm2.analysis.sandbox import Sandbox_Linux_x86_64
# Parser arguments
parser = Sandbox_Linux_x86_64.parser(description="ELF sandboxer")
parser.add_argument("filename", help="ELF Filename")
options = parser.parse_args()
print "options is {}".format(options)
# Create sandbox
sb = Sandbox_Linux_x86_64(options.filename, options, globals())
sb.run()

上述为64bit linux环境下进行的环境模拟。使用对应环境下的parser对象处理当前的参数,然后在创建沙盒的时候,将当前的参数以及环境变量传入,从而创建一个完整的沙盒换进。

Disassemble with Graph

这个模块的功能为将【当前的程序进行静态反编译,然后以图画或者别的形式暂时出来以方便更好的分析】。这个功能的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Container is the wrapper for ELF, PE.....
from miasm2.analysis.binary import Container
# Machine is the wrapper for managing multiple architecture
from miasm2.analysis.machine import Machine
# read a binary file
cont = Container.from_stream(open("Reverse2"))
# bin stream is a view on the mapped binary
bin_stream = cont.bin_stream
# 'cont.arch' is "x86_64", extracted from the ELF header
machine = Machine(cont.arch)
# Disassembly engine associated with the current binary
main_dis = machine.dis_engine(bin_stream)
# Disassembly the main function
blocks = main_dis.dis_multiblock(0x4005B6)

Container:
用来存放可执行文件,里面能够将可执行文件转换成bin_stream等形式,并且存放了当前文件的种类

Machine:
用来模拟各类运行平台,利用machine可以指定我们的运行文件所处在的机器平台。当前的

method:
|— dis_engine(bin_stream): 将指定的文件流反编译成block(miasm里面的一种存放反编译文件的格式),得到的返回值是AsmCFG

AsmCFG:
存放了反编译的内容,以及程序流的整体关系。

method:
|— dis_multiblock(address):从address开始的反汇编内容所形成的blocks的状态(调用这个函数的时候才会进行反汇编)

Dissasemble only

由于加入blocks的处理有很多额外操作,这里有时会再处理的过程中使流程变慢,于是我直接去翻找了源代码,然后找到了这个:

1
2
3
from miasm2.arch.x86.arch import mn_x86
...
instr = mn_x86.dis(bin_stream, attrib, offset)

这个mn_x86就是相当于是一个loader和指令执行器,其中实现了x86的指令以及其解读方式。所以我们这里直接使用这个mn_x86进行内容的解读,dis函数的参数为:

  • bin_stream – 需要反汇编的内容
  • attrib – int整数,表示当前是16,32还是64bit的bin_stream
  • offset – 从当前偏移量开始进行反汇编

如果当前偏移量上没有可以进行计算的反汇编的话,那么此时会往之前寻找,直到找到合适的指令并且将其反汇编。

返回值instr为一个代表了当前指令的类,里面常用属性有;

  • l – 当前指令的长度
  • name – 当前operation操作的str(例如MOV,ADD)
  • args – tuple,内部为Expr*类型,表示当前的源操作数和目的操作数。其中的__str__方法均重载过,利用这点可以做出很多花操作。

直接使用mn_x86这个模拟机而不是封装后的Machine在处理ollvm中的多次替换处理程序有奇效。

参考内容:
Obfuscating C++ programs via control flow flattening