Back
Featured image of post Deobf ollvm with angr

Deobf ollvm with angr

看到一个用angr解ollvm控制流平坦化的脚本,就学习顺便记录一下

控制流平坦化

环境搭建

官方的仓库只支持到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语句)将这些真实块按照原本的顺序执行,但在静态分析工具中,真实块就会被平坦化到同一层中,增大了静态难度

经过混淆后的块可以大致划分为以下几类:

  • 序言块:函数开始的块为序言块(只有一个),用于初始化一些变量,或者是一些检查
  • 主分发器:紧跟在序言块后的块(只有一个),用于分发控制流
  • 子分发器:主分发器后的分发块,用于分发控制流
  • 真实块:子分发器后、预分发器前的块,用于执行真正的代码
  • 预分发器:真实块后、回到主分发器的块(只有一个)
  • 结束块:没有后继的块(只有一个),用于结束函数,通常位于子分发器的一个分支中

解混淆思路

整个解混淆过程可以大致分为三个步骤:

  1. 识别序言块、主分发器、子分发器、真实块、预分发器、结束块
  2. 通过符号执行,重建控制流
  3. 进行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)

重建控制流

真实块中也分为两种情况

  1. 后继块唯一
  2. 有两个后继块

第一种情况直接进行符号执行,由块A走到块B即可

第二种情况需要先令判断条件为True执行一次,随后再令判断条件为False执行一次

使用flow记录真实块的后继块地址

flow = defaultdict(list)

每次执行从一个真实块开始,当执行到另一个真实块时结

block_addrs = [node.addr for node in relevant_nodes] + [prologue_node.addr, return_node.addr]
Built with Hugo
Theme Stack designed by Jimmy