控制流平坦化
环境搭建
官方的仓库只支持到llvm4,但有一些开源项目扩展到了一些比较新的版本
在Ubuntu2204上实测,这个仓库可以正常运行:https://github.com/buffcow/ollvm-project/tree/14.x
直接用Readme中提供的命令编译:
cmake -S llvm -B build -G Ninja -DLLVM_ENABLE_PROJECTS=clang -DCMAKE_BUILD_TYPE=Release -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_NEW_PASS_MANAGER=OFF
cmake --build build -j16
然后用build里面的clang对目标代码进行编译混淆
clang test.c -o test -mllvm -fla
混淆思路
控制流平坦化的思路是将原本的控制流图变成一个个基本块(真实块),然后使用分发器(类似大型Switch或者if else语句)将这些真实块按照原本的顺序执行,但在静态分析工具中,真实块就会被平坦化到同一层中,增大了静态难度
经过混淆后的块可以大致划分为以下几类:
- 序言块:函数开始的块为序言块(只有一个),用于初始化一些变量,或者是一些检查
- 主分发器:紧跟在序言块后的块(只有一个),用于分发控制流
- 子分发器:主分发器后的分发块,用于分发控制流
- 真实块:子分发器后、预分发器前的块,用于执行真正的代码
- 预分发器:真实块后、回到主分发器的块(只有一个)
- 结束块:没有后继的块(只有一个),用于结束函数,通常位于子分发器的一个分支中
解混淆思路
整个解混淆过程可以大致分为三个步骤:
- 识别序言块、主分发器、子分发器、真实块、预分发器、结束块
- 通过符号执行,重建控制流
- 进行Patch,将真实块的执行路径还原
解混淆的输入是程序和函数初始地址
识别混淆块
读取程序并获取函数的CFG图,这里需要使用to_supergraph
函数将CFG图转换为SuperGrap,更加接近IDA中的图
import angr
from angr-management.utils.graph import to_supergraph
proj = angr.Project("test", load_options={'auto_load_libs': False})
cfg = proj.analyses.CFGFast()
supergraph = to_supergraph(cfg.graph)
先通过入度和出度判断序言块和结束块
for node in supergraph.nodes():
if supergraph.in_degree(node) == 0:
prologue_node = node
if supergraph.out_degree(node) == 0:
return_node = node
随后通过序言块的后继找到主分发器
main_dispatcher_node = list(supergraph.successors(prologue_node))[0]
通过主分发器的前驱找到预分发器,前驱只有序言块和预分发器,所以可以排除序言块
for node in supergraph.predecessors(main_dispatcher_node):
if node.addr != prologue_node.addr:
pre_dispatcher_node = node
break
通过预分发器的前驱找到真实块
relevant_nodes = []
nop_nodes = []
for node in supergraph.predecessors(pre_dispatcher_node):
if node.size > 8:
relevant_nodes.append(node)
continue
其他块为子分发器(无用,后面可以直接nop掉),这一步可以和上一步压缩到一个循环中,根据块鱼预分发器是否存在边(supergraph.has_edge(node, pre_dispatcher_node)
)来判断是否为真实块
for node in supergraph.nodes():
if node not in (pre_dispatcher_node, prologue_node, main_dispatcher_node, return_node) and node not in relevant_nodes:
nop_nodes.append(node)
重建控制流
真实块中也分为两种情况
- 后继块唯一
- 有两个后继块
第一种情况直接进行符号执行,由块A走到块B即可
第二种情况需要先令判断条件为True执行一次,随后再令判断条件为False执行一次
使用flow记录真实块的后继块地址
flow = defaultdict(list)
每次执行从一个真实块开始,当执行到另一个真实块时结
block_addrs = [node.addr for node in relevant_nodes] + [prologue_node.addr, return_node.addr]