找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4960

积分

0

好友

677

主题
发表于 昨天 20:30 | 查看: 5| 回复: 0

这份代码实现虽然年代较早,但其所阐述的原理依然经典。本文引用自 qtfreet00/llvm-obfuscator 中关于控制流平坦化的部分。

核心思想

平坦化(Flattening)的核心目标是将函数原本的、具有清晰层次结构的控制流图(CFG),转换成一个难以直接阅读的“扁平”结构。其主要手段是引入一个中心调度器(Dispatcher),所有原始基本块的执行都由这个调度器通过一个 switch 语句来分发。

原始:                          平坦化后:

  Entry                          Entry
    |                              |
    ▼                              ▼
  Block A                    ┌─ Dispatcher (switch) ◄──┐
    |                        │    │    │    │          │
    ▼                        ▼    ▼    ▼    ▼          │
  Block B ──► Block C       A    B    C    D          │
    |                        │    │    │    │          │
    ▼                        └────┴────┴────┘          │
  Block D                      更新 switchVar ──────────┘

代码解析

实现流程通常包含以下步骤,这些步骤是许多编译时混淆技术的基础:

  • 原函数中已有的 switch 会干扰后续操作,先转成 if-else
  • 收集所有基本块,入口块单独处理
  • 将入口块拆成2部分
  • 创建 Switch 变量和 Dispatcher
  • 将每个原始块挂到 switch 的 case 上
  • 修改每个块的跳转,变成“更新 switchVar + 跳回 dispatcher”

为什么要拆分 Entry Block

入口块(Entry Block)扮演着特殊角色:它需要转变为 “调度器的入口”(负责分配 switch 变量并跳转到 dispatcher)。但如果原始入口块中已经包含了业务逻辑和条件跳转,就无法直接将其用作这个简单的入口。

假设 EntryBB 原本的结构如下:

┌─────────────────────────────┐
│         EntryBB             │
│                             │
│  %a = add %x, 1            │
│  %b = mul %a, 2            │
│  %cmp = icmp sgt %b, 10    │
│  br i1 %cmp, BB1, BB2      │  ← 条件跳转(2个后继)
└─────────┬──────────┬────────┘
          │          │
          ▼          ▼
     ┌────────┐ ┌────────┐
     │  BB1   │ │  BB2   │
     │ doA()  │ │ doB()  │
     └───┬────┘ └───┬────┘
         │          │
         ▼          ▼
       ┌──────────────┐
       │     BB3      │
       │    doC()     │
       │    ret       │
       └──────────────┘

平坦化期望 EntryBB 变成这样:

EntryBB:
    switchVar = 初始值
    br label %dispatcher

但现在的 EntryBB 里既有业务代码%a = add, %b = mul),又有条件跳转br i1 %cmp),直接覆盖就会丢失这些逻辑。因此,必须进行拆分。

解决方案:在指定的指令处将一个基本块切割成两半。切割点及其之后的指令被移动到新创建的基本块中,而原始块会自动添加一条无条件跳转指令跳转到这个新块。

splitBasicBlock(terminator)
                       切这里
                         ↓
┌─────────────────────────────────────┐
│              EntryBB                │
│                                     │
│  %a = add %x, 1                    │
│  %b = mul %a, 2                    │   ← 这些留在 EntryBB
│  %cmp = icmp sgt %b, 10            │
│ ─ ─ ─ ─ ─ ─ ─ ✂️ 切这里 ─ ─ ─ ─ ─ │
│  br i1 %cmp, BB1, BB2              │   ← 这条移到新块
└─────────────────────────────────────┘

拆分后得到两个块:

┌─────────────────────────────┐
│         EntryBB             │
│                             │
│  %a = add %x, 1            │
│  %b = mul %a, 2            │
│  %cmp = icmp sgt %b, 10    │
│  br label %first            │  ← 自动生成的无条件跳转
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│     “first”(新块)          │
│                             │
│  br i1 %cmp, BB1, BB2      │  ← 原来的 terminator 搬到这里
└─────────┬──────────┬────────┘
          │          │
          ▼          ▼
        BB1         BB2

拆分完成后,就可以对 EntryBB 进行平坦化改造了:

把 EntryBB 的 “br label %first” 删掉,替换成:

┌─────────────────────────────┐
│         EntryBB             │
│                             │
│  %a = add %x, 1            │   ← 原有业务代码保留
│  %b = mul %a, 2            │
│  %cmp = icmp sgt %b, 10    │
│                             │
│  %switchVar = alloca i32    │   ← 新增:分配 switch 变量
│  store FIRST_CASE_ID,       │   ← 新增:初始值 = “first” 块的 case 编号
│        %switchVar           │
│  br label %dispatcher       │   ← 新增:跳到调度器
└─────────────┬───────────────┘
              │
              ▼
          dispatcher

其他所有基本块(包括新生成的 first 块)的跳转逻辑也需要被修改,变为“更新 switch 变量值 + 跳回 dispatcher”的模式:

┌──────────────────┐    ┌──────────────────┐
│   “first”        │    │      BB1         │
│  (case 111)      │    │   (case 222)     │
│                  │    │                  │
│  原: br i1 %cmp  │    │   call doA()     │
│      BB1, BB2    │    │                  │
│                  │    │   store 444,      │
│  改: %next =     │    │         %switchVar│  ← 下一步去 BB3
│    select %cmp,  │    │   br dispatcher  │
│    222, 333      │    └────────┬─────────┘
│  store %next,    │             │
│        %switchVar│             │
│  br dispatcher   │    ┌──────────────────┐
└────────┬─────────┘    │      BB2         │
         │              │   (case 333)     │
         │              │                  │
         │              │   call doB()     │
         │              │                  │
         │              │   store 444,      │
         │              │         %switchVar│  ← 下一步也去 BB3
         │              │   br dispatcher  │
         │              └────────┬─────────┘
         │                       │
         ▼                       ▼
      dispatcher ◄───────────────┘

最终,整个函数的结构将演变为一个中心化的调度循环:

┌────────────────────────────────┐
│           EntryBB              │
│                                │
│  %a = add %x, 1               │
│  %b = mul %a, 2               │
│  %cmp = icmp sgt %b, 10       │
│  %switchVar = alloca i32       │
│  store 111, %switchVar         │
│  br label %dispatcher          │
└───────────────┬────────────────┘
                │
                ▼
┌────────────────────────────────┐ ◄─────────────────────┐
│          dispatcher            │                       │
│                                │                       │
│  %val = load %switchVar        │                       │
│  switch %val:                  │                       │
│    case 111 → first            │                       │
│    case 222 → BB1              │                       │
│    case 333 → BB2              │                       │
│    case 444 → BB3              │                       │
└──┬──────┬──────┬──────┬────────┘                       │
   │      │      │      │                                │
   ▼      │      │      │                                │
┌──────┐  │      │      │                                │
│first │  │      │      │                                │
│      │  │      │      │                                │
│select│  │      │      │                                │
│222/  │  │      │      │                                │
│  333 │  │      │      │                                │
│store │──┼──────┼──────┼──►──────────────────────────────┤
└──────┘  │      │      │                                │
          ▼      │      │                                │
     ┌──────┐    │      │                                │
     │ BB1  │    │      │                                │
     │doA() │    │      │                                │
     │store │    │      │                                │
     │ 444  │────┼──────┼──►──────────────────────────────┤
     └──────┘    │      │                                │
                 ▼      │                                │
            ┌──────┐    │                                │
            │ BB2  │    │                                │
            │doB() │    │                                │
            │store │    │                                │
            │ 444  │────┼──►──────────────────────────────┤
            └──────┘    │                                │
                        ▼                                │
                   ┌──────┐                              │
                   │ BB3  │                              │
                   │doC() │                              │
                   │ ret  │   ← 函数出口,不回 dispatcher  │
                   └──────┘                              │

在真实实现中,各个基本块对应的 case 值通常是随机生成的。

还原思路

既然平坦化是通过追踪 switchVar 的值来决定执行流程,那么理论上,只要跟随这个变量的值走完全程,就能还原出原始的控制流。

对于简单的线性流程,还原非常直接:

Entry: switchVar = 0xAAAA
           │
           ▼
Dispatcher: switch(switchVar)
    case 0xAAAA → Block A → switchVar = 0xBBBB → goto Dispatcher
    case 0xBBBB → Block B → switchVar = 0xCCCC → goto Dispatcher
    case 0xCCCC → Block C → switchVar = 0xDDDD → goto Dispatcher
    case 0xDDDD → Block D → return

还原:A → B → C → D ✅

你从 Entry 块获得初始的 switchVar 值,在 dispatcher 的 switch 表中找到对应的第一个 case 块,执行完该块后,查看它将 switchVar 改成了什么值,再去查下一个 case……通过这种链式追踪就能还原出顺序。这种情况下,静态分析就能轻易解决,甚至写个简单脚本即可完成。

对于包含分支的情况,思路类似,只是稍微复杂,但仍可还原。
原始逻辑:

if (x > 0)
    doA();
else
    doB();
doC();

平坦化后可能的结构:

Block_if:
    if (x > 0)
        switchVar = 0x1111    // → doA
    else
        switchVar = 0x2222    // → doB
    br dispatcher

Block_doA:
    call doA()
    switchVar = 0x3333        // → doC
    br dispatcher

Block_doB:
    call doB()
    switchVar = 0x3333        // → doC
    br dispatcher

Block_doC:
    call doC()
    ret

追踪与还原过程

Block_if 有两条路径:
    ├─ x > 0  → switchVar = 0x1111 → Block_doA → switchVar = 0x3333 → Block_doC
    └─ x <= 0 → switchVar = 0x2222 → Block_doB → switchVar = 0x3333 → Block_doC

还原:
         Block_if
        /        \
    Block_doA   Block_doB
        \        /
        Block_doC

完美还原 ✅

因此,现有反混淆工具的核心思路高度一致,都是围绕着追踪 switchVar 的赋值链,进而重建原始控制流图(CFG)。

工具                         方法
─────────────────────────────────────────────────────
手动 IDA 脚本               静态追踪 switchVar 赋值
D-810 (IDA 插件)            模式匹配 + 符号执行
Miasm                       符号执行追踪 switchVar
Binary Ninja + 脚本         数据流分析追踪常量传播
Triton                      动态符号执行
LLVM opt Pass               在 IR 层做常量传播 + CFG 重建

它们的核心都是一样的:追踪 switchVar 的赋值链,重建 CFG。 当然,这只是最原始、基础的 OLLVM 平坦化,现代混淆技术在此基础上增加了更多对抗静态分析符号执行的手段。

若你对这类底层软件安全技术感兴趣,欢迎在云栈社区安全与逆向工程板块与其他开发者深入探讨。




上一篇:企业数字化指南:先做BI打好数据基础,还是直接上AI智能分析?
下一篇:Hermes 事件循环策略源码深度解读:主线程、工作线程与异步上下文下的并发编程实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-19 00:16 , Processed in 0.953248 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表