ReDex:DEX 字节码的编译器级操控

从 IR 理解到实战反混淆与混淆 Pass 编写


第一部分:什么是 ReDex

1.1 定位

ReDex 是 FaceBook 开源的 Android DEX 字节码优化器。它不是传统意义上的”工具”,而是一个 DEX-to-DEX 编译器——读入 DEX,经过一系列变换 Pass,输出优化后的 DEX。

与 ProGuard/R8 的区别:ProGuard 工作在 Java 字节码层面(.class),ReDex 直接操作 Dalvik 字节码(.dex)。这意味着 ReDex 能做到 ProGuard 做不到的事——比如跨 DEX 优化、DEX 内部布局重排、以及本文要讲的 IR 级别的精确变换。

1.2 Pass Pipeline 架构

ReDex 的核心是 Pass Pipeline——一条由多个独立变换单元串联而成的流水线:

flowchart LR
    A["input.dex"] --> P1["Pass 1"]
    P1 --> P2["Pass 2"]
    P2 --> P3["..."]
    P3 --> PN["Pass N"]
    PN --> B["output.dex"]
    style A fill:#e8f5e9
    style B fill:#e3f2fd

每个 Pass 继承 Pass 基类,Pipeline 由 JSON 配置驱动:

1
2
3
4
5
6
7
8
9
10
11
class MyPass : public Pass {
public:
MyPass() : Pass("MyPass") {}
void run_pass(DexStoresVector& stores,
ConfigFiles& conf,
PassManager& mgr) override {
auto scope = build_class_scope(stores);
// 在这里操作所有类、方法、指令
}
};
static MyPass s_pass; // 文件末尾声明静态实例,自动注册
1
2
3
4
5
{
"redex": {
"passes": ["Pass1", "Pass2", "Pass3"]
}
}

PassManager 按顺序执行每个 Pass,Pass 之间共享同一份 IR 数据结构。一个 Pass 的输出就是下一个 Pass 的输入。

1.3 数据模型

ReDex 将整个 APK 的 DEX 文件加载为统一的内存数据结构:

graph TD
    A["DexStoresVector"] --> B["DexStore(DEX 文件)"]
    B --> C["DexClasses"]
    C --> D["DexClass"]
    D --> E["DexField[]"]
    D --> F["dmethods[]
static / private / 构造器"] D --> G["vmethods[]
虚方法"] F --> H["DexMethod"] G --> H H --> I["IRCode
方法体(IR 指令序列)"] style I fill:#fff3e0

build_class_scope(stores) 将所有 DexStore 展平为一个 Scope(即 vector<DexClass*>),这是大多数 Pass 的起点。

1.4 方法遍历

有了 Scope,就可以遍历所有方法的 IR。ReDex 提供了并行遍历工具:

1
2
3
4
walk::parallel::code(scope, [&](DexMethod* method, IRCode& code) {
// 每个线程处理不同的方法,互不干扰
// 对共享数据需要 atomic 或 mutex
});

walk::parallel::code 自动跳过没有代码体的抽象方法和 native 方法,多线程并行处理,充分利用多核。

到这里,我们知道了 ReDex 的整体架构:DEX 加载为内存数据结构 → Pass 遍历方法 → 操作 IRCode。下一步的问题是:IRCode 里面到底是什么?


第二部分:ReDex IR — 比 Dalvik 更好操作的中间表示

2.1 从 Dalvik 到 IR:为什么需要一层抽象

Pass 直接操作 Dalvik 字节码会遇到很多”编码层面”的麻烦:

  • 同一个语义有多种编码:const/4const/16const/high16const 都是加载整数常量
  • add-int/2addr v0, v1 隐含 v0 = v0 + v1,dest 和 src 耦合
  • invoke-virtual/range {v3..v7} 要求参数寄存器连续
  • 字符串加载 const-string v0, "hello" 把 dest 编码在指令内部

ReDex 在 Dalvik 之上建立了一层 IR,将这些全部规范化:

Dalvik ReDex IR 变化
const/4, const/16, const/high16, const 统一 OPCODE_CONST 消除编码差异
const-string v0, "hello" CONST_STRING "hello" + MOVE_RESULT_PSEUDO_OBJECT v0 dest 分离
add-int/2addr v0, v1 ADD_INT v0, v0, v1 dest/src 分离
invoke-virtual/range {v3..v7} INVOKE_VIRTUAL v3, v4, v5, v6, v7 无需连续

核心原则:一个语义,一个 opcode;dest 和 src 完全分离。Pass 编写者不需要关心底层编码细节。

那么,规范化之后的单条指令长什么样?

2.2 IRInstruction:一条指令的结构

每条 IR 指令由 IRInstruction 表示,核心字段:

1
2
3
4
5
6
7
8
9
10
11
IROpcode   m_opcode      操作码(OPCODE_CONST、OPCODE_INVOKE_STATIC 等)
reg_t m_dest 目标寄存器(若有)
vector m_srcs 源寄存器列表
union {
int64_t m_literal 整数立即数(CONST / CONST_WIDE)
DexString* m_string 字符串引用(CONST_STRING)
DexType* m_type 类型引用(NEW_INSTANCE / NEW_ARRAY)
DexField* m_field 字段引用(IGET / IPUT)
DexMethod* m_method 方法引用(INVOKE 系列)
DexOpcodeData* m_data 内联数据(FILL_ARRAY_DATA)
}

注意 union——同一时刻只有一个有效。OPCODE_CONSTm_literalOPCODE_CONST_STRINGm_stringOPCODE_INVOKE_STATICm_method。通过 opcode 判断该读哪个字段。

但有一类指令比较特殊——它们的 dest 不在自身上。

2.3 MOVE_RESULT_PSEUDO:may-throw 指令的 dest 分离

在 Dalvik 中,const-string v0, "hello" 把目标寄存器编码在指令内部。但 const-string 涉及堆操作,可能抛出 OutOfMemoryError——它是一条 may-throw 指令。

ReDex 的处理方式:所有 may-throw 且有 dest 的指令,dest 都拆到紧随其后的伪指令中

1
2
OPCODE_CONST_STRING                "hello"       ← 无 dest
IOPCODE_MOVE_RESULT_PSEUDO_OBJECT v0 ← dest 在这里

同理,NEW_ARRAY 也是 may-throw(可能 OOM):

1
2
OPCODE_NEW_ARRAY                   v0, [B        ← src=长度寄存器, type=[B
IOPCODE_MOVE_RESULT_PSEUDO_OBJECT v0 ← dest 在这里

这个模式在编写 Pass 时必须牢记:

  • 读取 CONST_STRING 的 dest → 看下一条 MOVE_RESULT_PSEUDO 的 dest
  • 插入 CONST_STRING → 必须紧跟一条 MOVE_RESULT_PSEUDO_OBJECT
  • 漏掉这条伪指令 → 寄存器未定义,运行时崩溃

现在我们知道了单条指令的结构。接下来的问题是:一个方法中的所有指令是如何组织的?

2.4 指令的两种组织形式:线性 IR 与 CFG

ReDex 的 IRCode 提供两种表示形式,可以随时互相转换:

flowchart LR
    A["线性 IR
(IRList 双向链表)"] -- "code.build_cfg()" --> B["CFG
(基本块 + 边)"] B -- "code.clear_cfg()" --> A style A fill:#e8f5e9 style B fill:#e3f2fd

线性模式:指令存储在双向链表 IRList 中,每个节点是 MethodItemEntry(MIE)。MIE 有多种类型:

MIE 类型 含义 示例
MFLOW_OPCODE IR 指令 CONST v0, 42
MFLOW_TARGET 跳转标签(跳转目标位置) :label_0
MFLOW_TRY try/catch 边界标记 try_start / try_end
MFLOW_CATCH catch 块入口标记 catch Ljava/lang/Exception;
MFLOW_POSITION 源码行号信息 line 42

CFG 模式:调用 code.build_cfg() 后,IRList 在跳转指令、跳转标签、try/catch 边界处被切割为基本块(cfg::Block),块之间通过边(cfg::Edge)连接。

用一个具体的 if-else 例子展示两种形式的对应关系:

1
2
3
// 源码
int x = (flag != 0) ? 42 : 99;
return x;

线性 IR(IRList 双向链表)——所有 MIE 节点按顺序排列:

1
2
3
4
5
6
7
8
9
10
MIE[MFLOW_OPCODE]   LOAD_PARAM        v1          // flag
MIE[MFLOW_OPCODE] IF_EQZ v1 // if (flag == 0) goto :else
MIE[MFLOW_TARGET] :label_0 ←────────────────────── IF_EQZ 的跳转目标
MIE[MFLOW_OPCODE] CONST v0, 42 // then 分支
MIE[MFLOW_OPCODE] GOTO // goto :end
MIE[MFLOW_TARGET] :label_1 ←────────────────────── GOTO 的跳转目标
MIE[MFLOW_TARGET] :label_0_target ──→ 这就是 :label_0 指向的位置
MIE[MFLOW_OPCODE] CONST v0, 99 // else 分支
MIE[MFLOW_TARGET] :label_1_target ──→ 这就是 :label_1 指向的位置
MIE[MFLOW_OPCODE] RETURN v0

MFLOW_TARGET 是”标签”——它本身不执行任何操作,只是标记一个位置,供跳转指令引用。MFLOW_OPCODE 才是真正的指令。

build_cfg()** 后的 CFG**——在跳转指令和跳转标签处切割,标签消失,变成边:

1
2
3
4
5
6
7
8
9
10
11
12
13
B0: preds=[] succs=[B1(goto), B2(branch)]
LOAD_PARAM v1
IF_EQZ v1 ← 切割点:跳转指令

B1: preds=[B0(goto)] succs=[B3(goto)]
CONST v0, 42 ← then 分支
(GOTO 指令被边吸收,不再显式存在)

B2: preds=[B0(branch)] succs=[B3(goto)]
CONST v0, 99 ← else 分支

B3: preds=[B1(goto), B2(goto)] succs=[]
RETURN v0 ← 汇合点

对应关系一目了然:

flowchart TD
    B0["B0
LOAD_PARAM v1
IF_EQZ v1"] -- "goto
(if-false)" --> B1["B1
CONST v0, 42"] B0 -- "branch
(if-true)" --> B2["B2
CONST v0, 99"] B1 -- "goto" --> B3["B3
RETURN v0"] B2 -- "goto" --> B3 style B0 fill:#e8f5e9 style B3 fill:#e3f2fd

转换的核心规则:

  • 线性 → CFGMFLOW_TARGET 标签变成边,GOTO 指令被吸收为 EDGE_GOTOIF_* 产生两条边(EDGE_GOTO = false 分支,EDGE_BRANCH = true 分支)
  • CFG → 线性:边变回 MFLOW_TARGET 标签和显式的 GOTO 指令,基本块按拓扑序拼接

两种模式的转换 API:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 线性 → CFG
code.build_cfg();
auto& cfg = code.cfg();
for (auto* block : cfg.blocks()) {
for (auto& mie : *block) {
if (mie.type != MFLOW_OPCODE) continue;
IRInstruction* insn = mie.insn;
// 操作指令...
}
}

// CFG → 线性
code.clear_cfg();

边类型决定了块之间的控制流关系:

EdgeType 含义
EDGE_GOTO 无条件跳转、if-false 分支
EDGE_BRANCH if-true 分支、switch case
EDGE_THROW 异常边,指向 catch 块
EDGE_GHOST 虚拟边,连接所有出口到统一 exit block

几乎所有分析和变换都在 CFG 模式下进行——基本块结构是数据流分析的基础。那么,如何在 CFG 模式下安全地修改指令?

2.5 CFGMutation:安全的批量修改

直接在遍历 CFG 时插入/删除指令会导致迭代器失效。ReDex 提供了 CFGMutation——先收集所有修改意图,最后一次性应用:

1
2
3
4
5
6
7
8
9
10
11
12
cfg::CFGMutation mutation(cfg);

for (auto it = cfg::InstructionIterable(cfg).begin();
it != cfg::InstructionIterable(cfg).end(); ++it) {
if (/* 匹配条件 */) {
mutation.insert_after(it, {new_insn1, new_insn2}); // 插入
mutation.replace(it, {replacement1, replacement2}); // 替换
mutation.remove(it); // 删除
}
}

mutation.flush(); // 一次性应用所有修改

replace() 有一个重要的隐含行为:当锚点指令具有关联的 move-result(如 INVOKE_STATIC 后面的 MOVE_RESULT_OBJECT),replace 会自动移除关联的 move-result。

2.6 关键 opcode 速查

Opcode 含义 示例
OPCODE_CONST 加载 32 位整数常量 CONST v0, 42
OPCODE_CONST_WIDE 加载 64 位整数常量 CONST_WIDE v0, 100L
OPCODE_CONST_STRING 加载字符串引用(may-throw) CONST_STRING "hello"
OPCODE_NEW_ARRAY 创建数组(may-throw) NEW_ARRAY v0, [B
OPCODE_FILL_ARRAY_DATA 批量填充数组 FILL_ARRAY_DATA v0, <payload>
OPCODE_APUT_BYTE 写入 byte 数组元素 APUT_BYTE v_val, v_arr, v_idx
OPCODE_INVOKE_STATIC 静态方法调用 INVOKE_STATIC v0, v1, Lfoo;.bar:()V
OPCODE_MOVE_RESULT_OBJECT 获取方法返回值 MOVE_RESULT_OBJECT v0
IOPCODE_MOVE_RESULT_PSEUDO_OBJECT 获取 may-throw 指令的 dest MOVE_RESULT_PSEUDO_OBJECT v0
OPCODE_XOR_INT_LIT 整数异或(立即数) XOR_INT_LIT v0, v0, 0x5A

到这里,我们掌握了 ReDex IR 的完整图景:指令结构(2.2)→ 特殊模式(2.3)→ 组织形式(2.4)→ 修改方式(2.5)。但仅仅能读写 IR 还不够——要做有意义的变换,需要”理解”IR 的语义。这就是分析基础设施的作用。


第三部分:分析基础设施 — 从”能读写”到”能理解”

第二部分让我们能读写 IR,但面对一条 INVOKE_STATIC v0, v1, ...,我们只知道”v0 和 v1 被传入了某个方法”,却不知道 v0 和 v1 的值是什么、从哪来。分析基础设施解决的就是这类问题。

ReDex 内置了三类核心分析能力,层层递进:

flowchart TD
    A["Reaching Definitions
方法内 · 前向数据流
回答:寄存器的值由谁定义?"] --> B["Def-Use Chain
方法内 · 双向查询
回答:定义被谁使用?使用来自谁?"] A --> C["Call Graph
全程序 · 调用关系图
回答:谁调用了这个方法?"] style A fill:#e8f5e9 style B fill:#e3f2fd style C fill:#fff3e0

3.1 不动点迭代:所有数据流分析的共同基础

所有方法内数据流分析都基于同一个框架(底层使用 Facebook 的 SPARTA 抽象解释库),遵循相同的三步模式:

1
2
3
4
5
6
7
8
// 1. 构造分析器(传入 CFG)
MyFixpointIterator fp(cfg);

// 2. 运行不动点迭代(传入初始状态)
fp.run(MyDomain::initial_value());

// 3. 查询结果
auto state = fp.get_entry_state_at(block); // 块入口状态

“不动点”的含义:分析器反复遍历 CFG 的所有块,在每个块的入口处合并来自所有前驱的状态,直到所有块的状态不再变化。这保证了结果考虑了所有可能的执行路径(包括循环)。

3.2 Reaching Definitions:追踪寄存器值的来源

核心问题:对于某条指令的某个源寄存器,它的值是由哪条指令产生的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "ReachingDefinitions.h"

// 构造并运行 Move-Aware 版本(穿透 move 指令)
reaching_defs::MoveAwareFixpointIterator fp(cfg);
fp.run(reaching_defs::Environment());

// 逐指令查询
for (auto* block : cfg.blocks()) {
auto env = fp.get_entry_state_at(block);
for (auto& mie : InstructionIterable(block)) {
auto* insn = mie.insn;
// env 此时是 insn 执行前的状态
auto defs = env.get(insn->src(0));
// defs 是一个集合:所有可能定义该寄存器的指令
for (IRInstruction* def : defs.elements()) {
// def 就是定义该寄存器的那条指令
}
fp.analyze_instruction(insn, &env); // 推进到下一条
}
}

两个关键概念:

Move-Aware:基础版中,MOVE v3, v5 会让 v3 的定义变成这条 move 指令本身。Move-Aware 版穿透 move 链,追踪到 v5 的原始定义。

1
2
3
CONST v5, 42
MOVE v3, v5
ADD_INT v0, v3, v1

Domain 语义env.get(reg) 返回的是一个集合。如果集合中有多个元素,说明该寄存器在不同的执行路径上有不同的定义(例如来自 if-else 的两个分支)。只有集合大小为 1 时,值才是静态确定的。

3.3 Call Graph:全程序调用关系

Reaching Definitions 工作在单个方法内部。但如果我们想知道”谁调用了某个方法”,就需要跨方法的全程序分析。

构建过程分两步:

flowchart LR
    A["Scope
(所有类)"] --> B["Method Override Graph
解析虚方法重写关系"] B --> C["Call Graph
建立调用者↔被调用者关系"] C --> D["cg.get_callers(target)
精确获取调用者集合"] style D fill:#e3f2fd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "MethodOverrideGraph.h"
#include "CallGraph.h"

// 步骤 1:构建方法重写图(解析虚方法多态)
auto override_graph = method_override_graph::build_graph(scope);

// 步骤 2:构建调用图
auto cg = call_graph::single_callee_graph(*override_graph, scope);

// 查询:谁调用了 target?
const auto& callers = cg.get_callers(target);
for (const DexMethod* caller : UnorderedIterable(callers)) {
// ...
}

为什么需要 Method Override Graph?Java 的虚方法调用(invoke-virtual)在编译期无法确定实际调用的是哪个实现。Override Graph 记录了所有类的方法重写关系,Call Graph 据此解析虚调用。

single_callee_graph 是保守的——只有当虚调用能唯一确定被调用者时才建立边。这保证了 get_callers() 返回的结果是精确的。

3.4 Def-Use Chain:定义与使用的双向查询

Reaching Definitions 回答”谁定义了这个寄存器”(use → def 方向)。Def-Use Chain 补充了反向查询:”这个定义被谁使用了”(def → use 方向)。

1
2
3
4
5
6
7
8
9
10
11
12
#include "LiveRange.h"

live_range::MoveAwareChains chains(cfg);
auto def_use = chains.get_def_use_chains();

// 查询:某条指令的结果被哪些指令使用了?
for (const auto& [def_insn, uses] : UnorderedIterable(def_use)) {
for (const auto& use : UnorderedIterable(uses)) {
// use.insn = 使用该值的指令
// use.src_index = 是第几个源操作数
}
}

Def-Use Chain 的典型用途:判断某个值是否”逃逸”——如果一个定义的所有 use 都是特定类型的指令,就可以对这组指令做整体优化或删除。具体应用将在第四部分的实战中展示。

至此,三类分析能力形成了完整的工具箱:Reaching Definitions 追踪值的来源,Call Graph 定位调用关系,Def-Use Chain 追踪值的去向。接下来,我们用这些工具解决一个真实问题。


第四部分:实战 — 用 ReDex 反混淆字符串加密

4.1 目标

某 APP 使用了字符串加密保护。所有敏感字符串在 DEX 中不以明文存在,而是通过一个统一的解密函数在运行时还原:

1
2
// 混淆后的代码(jadx 反编译)
String s = (String) a.a(16777217, 0, 0L, "350312", new byte[]{33,56,78,...});

方法签名:Lcom/bytedance/mobsec/matrix/a;.a:(IIJLjava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;

参数含义:

  • i = 16777217(0x1000001):功能标识,表示字符串解密
  • i2 = 0:子功能码
  • j = 0L:保留参数
  • key = "350312":6 字符 hex 密钥
  • obj = byte[]{...}:XOR 加密后的密文

解密算法是简单的 XOR:用 key 和固定种子派生 10 字节密钥,逐字节异或密文。

目标:编写一个 ReDex Pass,自动找到所有调用点,提取参数,解密字符串,并将解密结果直接写回 IR。

4.2 分析流水线

整个 Pass 的工作流程串联了第一到第三部分的所有知识:

为什么用 Call Graph 而非线性扫描?抖音有数十万个方法,但实际调用解密函数的只有 181 个。cg.get_callers(target) 直接返回精确的调用者集合,跳过所有无关方法。

流水线的前半段(步骤 A→E)用到了第一部分的 Pass 架构和第三部分的 Call Graph。接下来的步骤 F→G 是核心——如何从 IR 中提取参数的具体值。

4.3 参数提取:Def-Use Chain 的实际应用

目标方法有 5 个参数,invoke 指令的源寄存器布局:

1
2
3
4
INVOKE_STATIC v0, v1, v2, v4, v5, Lcom/bytedance/mobsec/matrix/a;.a:(...)
↑ ↑ ↑ ↑ ↑
src[0] [1] [2] [3] [4]
i i2 j key obj

对每个 caller 构建 Def-Use Chain,从 invoke 指令的每个源寄存器出发,反向查找定义指令(use → def),再从定义指令中提取常量值。

提取 int/string 常量很直接——通过 use-def 方向找到唯一的 CONSTCONST_STRING 定义:

1
2
3
4
// 构建 def-use 链
live_range::MoveAwareChains chains(cfg);
auto use_def = chains.get_use_def_chains(); // use → def 方向
auto def_use = chains.get_def_use_chains(); // def → use 方向

对于 int 参数,从 invoke 的 use 出发找 def:如果唯一定义是 OPCODE_CONST,直接取 get_literal()。对于 string 参数,如果唯一定义是 OPCODE_CONST_STRING,取 get_string()->str()

但第 5 个参数 obj(byte[] 密文)的提取要复杂得多。byte[] 在 IR 中不是一条指令,而是一组指令的协作:

提取思路——基于 Def-Use Chain,不需要遍历整个 CFG:

  1. 从 invoke 的 src[4] 通过 use→def 找到 NEW_ARRAY 定义指令
  2. NEW_ARRAY 通过 def→use 找到它的所有使用者
  3. 在使用者中筛选 FILL_ARRAY_DATAAPUT_BYTE,直接提取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. use→def:从 invoke 的 obj 参数找到 NEW_ARRAY
// (MoveAwareChains 自动穿透 MOVE 指令)
IRInstruction* new_arr_insn = /* use_def 查到的 NEW_ARRAY */;

// 2. def→use:从 NEW_ARRAY 出发,查找所有使用点
auto it = def_use.find(new_arr_insn);
const auto& uses = it->second;

// 3. 从 uses 中提取数组内容
for (const auto& use : uses) {
if (use.insn->opcode() == OPCODE_FILL_ARRAY_DATA
&& use.src_index == 0) {
// src[0] 是数组引用 → 直接提取 payload
*out = get_fill_array_data_payload<uint8_t>(
use.insn->get_data());
return true;
}
if (use.insn->opcode() == OPCODE_APUT_BYTE
&& use.src_index == 1) {
// src[1] 是数组引用 → 提取 index 和 value
// ...
}
}

与遍历整个 CFG 的方式相比,Def-Use Chain 的优势是精确定向——直接从 NEW_ARRAY 的定义跳到它的所有使用点,不需要扫描无关指令。而且 use.src_index 天然告诉我们”这个 use 是把数组作为第几个参数”,不需要额外的指针同一性检查。

参数提取完成后,5 个参数齐备,就可以执行解密了。下一步是把解密结果写回 IR。

4.4 IR 替换:从 invoke 到 CONST_STRING

替换前的 IR 模式:

1
2
3
4
5
6
7
8
9
10
11
CONST           v0, 16777217           // param i
CONST v1, 0 // param i2
CONST_WIDE v2, 0 // param j
CONST_STRING v4, "key" // param key
CONST v5, len // array size
NEW_ARRAY v5, [B // byte array
MOVE_RESULT_PSEUDO_OBJECT v5
FILL_ARRAY_DATA v5 // encrypted bytes
INVOKE_STATIC v0..v5, target // 解密调用
MOVE_RESULT_OBJECT v6 // 返回 Object
CHECK_CAST v6, String // 转为 String

替换后:

1
2
3
4
5
CONST           v0, 16777217           ← 死代码(后续清理)
... ← 死代码
CONST_STRING "decrypted_string" ← 新指令
MOVE_RESULT_PSEUDO_OBJECT v6 ← 新指令,复用原 dest
CHECK_CAST v6, String ← String→String,无害保留

核心思路:用 CONST_STRING + MOVE_RESULT_PSEUDO_OBJECT 替换 INVOKE_STATIC,上方的参数准备指令变为死代码,交给后续 DCE 清理。

使用第二部分介绍的 CFGMutation 实现替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cfg::CFGMutation mutation(cfg);
//遍历所有记录的位置
for (const auto& r : sites) {
if (!r.params_resolved || r.decrypted.empty()) continue;

auto invoke_it = cfg.find_insn(r.invoke_insn);
auto move_it = cfg.move_result_of(invoke_it);
//存在返回值不被使用的情况,影响反编译效果
if (move_it.is_end()) {
// 返回值被丢弃 → 解密函数无副作用,直接删除
mutation.remove(invoke_it);
} else {
// 返回值被使用 → 替换为 CONST_STRING
auto dest = move_it->insn->dest();
auto* str_insn = new IRInstruction(OPCODE_CONST_STRING);
str_insn->set_string(DexString::make_string(r.decrypted));
auto* move_pseudo =
new IRInstruction(IOPCODE_MOVE_RESULT_PSEUDO_OBJECT);
move_pseudo->set_dest(dest);
mutation.replace(invoke_it, {str_insn, move_pseudo});
}
}

mutation.flush(); // 一次性应用所有修改

两个要点:

  1. CONST_STRING 必须配对 MOVE_RESULT_PSEUDO_OBJECT。回顾 2.3 节——CONST_STRING 是 may-throw 指令,dest 在伪指令中。漏掉这条伪指令,寄存器未定义,运行时崩溃。
  2. mutation.replace()** 自动移除关联的 MOVE_RESULT**。当锚点指令有 has_move_result_any() 时(如 INVOKE_STATIC 后面的 MOVE_RESULT_OBJECT),replace 会自动跳过并移除它。所以我们只需 replace invoke,MOVE_RESULT_OBJECT 自动消失。

替换完成后,原来为 invoke 准备参数的指令全部变成了死代码。但它们不会自动消失——需要显式清理。

4.5 针对性死代码消除

替换后的死代码分两类:

类型 示例 特点
无副作用指令 CONSTCONST_WIDECONST_STRINGMOVE LocalDce 可自动清理
有副作用指令 NEW_ARRAYAPUT_BYTEFILL_ARRAY_DATA 堆写入,LocalDce 不处理

LocalDce(Local Dead Code Elimination)是 ReDex 内置的死代码消除服务。它能清理所有”结果未被使用且无副作用”的指令。但 APUT_BYTE 是堆写入——从类型系统看它有副作用,LocalDce 不敢删。

解决方案:两阶段清理。

Phase 1:死数组消除。这里用到了第三部分介绍的 Def-Use Chain——从 NEW_ARRAY 的定义出发,查找它的所有 use。如果所有 use 都是数组内部操作(APUT_BYTEFILL_ARRAY_DATAMOVE),说明数组没有逃逸到外部,整组指令可以安全移除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
live_range::MoveAwareChains chains(cfg);
auto def_use = chains.get_def_use_chains();

for (const auto& [def_insn, uses] : def_use) {
if (def_insn->opcode() != OPCODE_NEW_ARRAY) continue;

bool can_remove = true;
for (const auto& use : uses) {
if (!is_array_internal_use(use.insn->opcode())) {
can_remove = false; // 数组逃逸了
break;
}
}
if (can_remove) {
// 标记 NEW_ARRAY + 所有 aput/fill 为待删除
}
}

Phase 2:LocalDce 服务调用。ReDex 的 LocalDce 是独立服务类,可脱离 Pass 单独使用:

1
2
UnorderedSet<DexMethodRef*> pure_methods;
LocalDce(nullptr, pure_methods).dce(cfg, false);

两个阶段仅对修改过的 caller 运行,不触碰无关方法——这避免了全量 DCE 在某些复杂方法上触发的寄存器越界问题。

4.6 运行结果

下面的代码基本都是AI生成的,哎现在AI的编码能力真的非常强,而且对于逆向分析也有很大冲击

pass代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
/*
* CallGraphXrefPass — Call Graph + Reaching Definitions 交叉引用分析
*
* 1. 构建 Method Override Graph → Call Graph
* 2. 在 Call Graph 中定位目标解密函数
* 3. 遍历所有调用者,用 MoveAware Reaching Definitions 追踪参数
* 4. 输出调用者 IR + 参数分析到日志文件
*/

#include "CallGraphXrefPass.h"

#include <algorithm>
#include <fstream>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>

#include "CFGMutation.h"
#include "CallGraph.h"
#include "DeterministicContainers.h"
#include "ConfigFiles.h"
#include "ControlFlow.h"
#include "DexClass.h"
#include "DexInstruction.h"
#include "DexUtil.h"
#include "IRCode.h"
#include "LocalDce.h"
#include "IRInstruction.h"
#include "IROpcode.h"
#include "MethodOverrideGraph.h"
#include "PassManager.h"
#include "LiveRange.h"
#include "ReachingDefinitions.h"
#include "Show.h"
#include "Trace.h"
#include "Walkers.h"

namespace {

constexpr const char* TARGET_CLASS = "Lcom/bytedance/mobsec/matrix/a;";
constexpr const char* TARGET_METHOD = "a";
constexpr const char* TARGET_PROTO =
"(IIJLjava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;";
constexpr int64_t TARGET_I_VAL = 16777217; // 0x1000001

// ── 解密算法 ─────────────────────────────────────────────────────────────────

std::string douyin_str_dec(const std::string& key_str,
const std::vector<uint8_t>& enc) {
static const uint8_t INIT_KEY[] =
{0x71, 0x62, 0x13, 0x14, 0x5f, 0x77, 0x63, 0x41, 0x31, 0x30};
constexpr size_t KEY_LEN = 10;
if (key_str.empty() || enc.empty()) return "";
uint8_t key[KEY_LEN];
for (size_t i = 0; i < KEY_LEN; i++) {
key[i] = static_cast<uint8_t>(key_str[i % key_str.size()]) ^ INIT_KEY[i];
}
std::string ret;
ret.reserve(enc.size());
for (size_t i = 0; i < enc.size(); i++) {
ret += static_cast<char>(enc[i] ^ key[i % KEY_LEN]);
}
return ret;
}

// ── 格式化工具 ───────────────────────────────────────────────────────────────

std::string fmt_bytes(const std::vector<uint8_t>& v) {
std::ostringstream oss;
oss << "[";
for (size_t i = 0; i < v.size(); i++) {
if (i) oss << ",";
oss << static_cast<int>(v[i]);
}
oss << "]";
return oss.str();
}

// ── Trace 辅助:格式化 Reaching Defs Domain ─────────────────────────────────

static std::string fmt_defs(const reaching_defs::Domain& defs) {
if (defs.is_top()) return "{TOP}";
if (defs.is_bottom()) return "{BOTTOM}";
const auto& elems = defs.elements();
if (elems.empty()) return "{empty}";
std::ostringstream oss;
oss << "{";
bool first = true;
for (IRInstruction* def : elems) {
if (!first) oss << ", ";
oss << show(def);
first = false;
}
oss << "}";
return oss.str();
}

// 打印 env 中所有非空寄存器的定义状态
static void dump_env_summary(const reaching_defs::Environment& env,
std::ostream& out,
const char* prefix = " ") {
// Environment 是 PatriciaTreeMapAbstractEnvironment
// 遍历所有绑定
if (env.is_top()) {
out << prefix << "(env = TOP)\n";
return;
}
if (env.is_bottom()) {
out << prefix << "(env = BOTTOM)\n";
return;
}
auto bindings = env.bindings();
if (bindings.begin() == bindings.end()) {
out << prefix << "(env = empty)\n";
return;
}
for (auto& [reg, defs] : bindings) {
if (defs.is_top() || defs.is_bottom()) continue;
if (defs.elements().empty()) continue;
out << prefix << "v" << reg << " ← " << fmt_defs(defs) << "\n";
}
}

// ── CFG dump ─────────────────────────────────────────────────────────────────

static void dump_cfg(const cfg::ControlFlowGraph& cfg, std::ostream& out) {
static const char* kEdge[] = {"goto", "branch", "throw", "ghost"};
for (cfg::Block* b : cfg.blocks()) {
out << " B" << b->id() << ": ; preds=[";
bool first = true;
for (cfg::Edge* e : b->preds()) {
if (!first) out << " ";
out << "B" << e->src()->id()
<< "[" << kEdge[static_cast<int>(e->type())] << "]";
first = false;
}
out << "] succs=[";
first = true;
for (cfg::Edge* e : b->succs()) {
if (!first) out << " ";
out << "B" << e->target()->id()
<< "[" << kEdge[static_cast<int>(e->type())] << "]";
first = false;
}
out << "]\n";
for (const MethodItemEntry& mie : *b) {
if (mie.type != MFLOW_OPCODE) continue;
std::string s = show(mie.insn);
auto sp = s.find(' ');
if (sp == std::string::npos) {
out << " " << s << "\n";
} else {
std::string op = s.substr(0, sp);
std::string rest = s.substr(sp + 1);
out << " " << op;
int pad = std::max(1, 30 - static_cast<int>(op.size()));
for (int i = 0; i < pad; ++i) out << ' ';
out << rest << "\n";
}
}
out << "\n";
}
}

// ── 单个调用点的分析结果 ─────────────────────────────────────────────────────

struct InvokeSiteResult {
DexMethod* caller_method{nullptr};
std::string caller_label;
std::string caller_ir;
IRInstruction* invoke_insn{nullptr};

int64_t param_i{-1};
int64_t param_i2{-1};
int64_t param_j{-1};
std::string param_key;
std::vector<uint8_t> param_enc;
bool params_resolved{false};
std::string decrypted;
std::string invoke_text; // 预序列化,避免 mutation 后 use-after-free
};

// ── 从定义指令中提取常量值 ───────────────────────────────────────────────────

// 尝试从一组定义指令中提取唯一的 int 常量
static bool extract_int(const reaching_defs::Domain& defs, int64_t* out) {
if (defs.is_top() || defs.is_bottom()) return false;
const auto& elems = defs.elements();
if (elems.empty()) return false;
bool found = false;
for (IRInstruction* def : elems) {
if (def->opcode() == OPCODE_CONST || def->opcode() == OPCODE_CONST_WIDE) {
int64_t val = def->get_literal();
if (!found) {
*out = val;
found = true;
} else if (val != *out) {
return false; // 多个不同值
}
}
}
return found;
}

// 尝试从一组定义指令中提取唯一的字符串常量
static bool extract_string(const reaching_defs::Domain& defs,
std::string* out) {
if (defs.is_top() || defs.is_bottom()) return false;
const auto& elems = defs.elements();
bool found = false;
for (IRInstruction* def : elems) {
if (def->opcode() == OPCODE_CONST_STRING) {
std::string val(def->get_string()->str());
if (!found) {
*out = val;
found = true;
} else if (val != *out) {
return false; // 多个不同值
}
}
}
return found;
}

// ── 方式 A:CFG 遍历 + 指针同一性检查 ────────────────────────────────────────
// 遍历整个 CFG 查找 fill-array-data 或 aput-byte,用 Reaching Definitions
// 验证数组寄存器指向同一个 NEW_ARRAY 指令(手动构建局部 def-use 关系)
static bool extract_byte_array_cfg(
const reaching_defs::Domain& defs,
const cfg::ControlFlowGraph& cfg,
reaching_defs::MoveAwareFixpointIterator& fp,
std::vector<uint8_t>* out,
std::ostream* trace = nullptr) {
if (defs.is_top() || defs.is_bottom()) {
if (trace) {
*trace << " [byte[]] defs is "
<< (defs.is_top() ? "TOP" : "BOTTOM") << ", skip\n";
}
return false;
}

// 找到 new-array 定义
IRInstruction* new_arr_insn = nullptr;
if (trace) {
*trace << " [byte[]] obj param defs: " << fmt_defs(defs) << "\n";
}
for (IRInstruction* def : defs.elements()) {
if (def->opcode() == OPCODE_NEW_ARRAY) {
new_arr_insn = def;
break;
}
}
if (!new_arr_insn) {
if (trace) {
*trace << " [byte[]] no NEW_ARRAY found in defs, skip\n";
}
return false;
}
if (trace) {
*trace << " [byte[]] anchor NEW_ARRAY insn: "
<< show(new_arr_insn) << " (ptr=" << new_arr_insn << ")\n";
}

// 检查 size=0 的空数组(new byte[0])
{
auto arr_it = cfg.find_insn(new_arr_insn);
if (!arr_it.is_end()) {
auto* blk = arr_it.block();
auto env0 = fp.get_entry_state_at(blk);
for (auto& mie : InstructionIterable(blk)) {
if (mie.insn == new_arr_insn) break;
fp.analyze_instruction(mie.insn, &env0);
}
int64_t arr_size = -1;
if (extract_int(env0.get(new_arr_insn->src(0)), &arr_size) &&
arr_size == 0) {
if (trace) {
*trace << " [byte[]] array size=0, empty byte[]\n";
}
out->clear();
return true;
}
}
}

// 遍历 CFG 查找 fill-array-data 或 aput-byte
out->clear();
bool found_fill = false;

for (auto* block : cfg.blocks()) {
auto env = fp.get_entry_state_at(block);
for (auto& mie : InstructionIterable(block)) {
auto* insn = mie.insn;

if (insn->opcode() == OPCODE_FILL_ARRAY_DATA) {
auto src_defs = env.get(insn->src(0));
if (trace) {
*trace << " [byte[]] B" << block->id()
<< " FILL_ARRAY_DATA src defs: " << fmt_defs(src_defs) << "\n";
}
for (auto* d : src_defs.elements()) {
if (d == new_arr_insn) {
auto payload =
get_fill_array_data_payload<uint8_t>(insn->get_data());
*out = payload;
found_fill = true;
if (trace) {
*trace << " [byte[]] MATCH! ptr identity confirmed "
<< "(d=" << d << " == anchor=" << new_arr_insn << ")\n"
<< " [byte[]] payload size=" << payload.size()
<< " bytes: " << fmt_bytes(payload) << "\n";
}
break;
}
}
}

if (insn->opcode() == OPCODE_APUT_BYTE) {
auto arr_defs = env.get(insn->src(1));
bool is_our_array = false;
for (auto* d : arr_defs.elements()) {
if (d == new_arr_insn) { is_our_array = true; break; }
}
if (is_our_array) {
int64_t idx = -1, val = 0;
if (extract_int(env.get(insn->src(2)), &idx) &&
extract_int(env.get(insn->src(0)), &val) &&
idx >= 0 && idx < 4096) {
if (trace) {
*trace << " [byte[]] APUT_BYTE arr[" << idx
<< "] = " << val << "\n";
}
if (static_cast<size_t>(idx) >= out->size()) {
out->resize(static_cast<size_t>(idx) + 1, 0);
}
(*out)[static_cast<size_t>(idx)] = static_cast<uint8_t>(val);
}
}
}

fp.analyze_instruction(insn, &env);
}
}
if (trace) {
*trace << " [byte[]] result: found_fill=" << found_fill
<< " size=" << out->size() << "\n";
}
return found_fill || !out->empty();
}

// ── 方式 B:Def-Use Chain 直接查询 ──────────────────────────────────────────
// 通过 live_range::MoveAwareChains 构建 def-use 链,直接从 NEW_ARRAY 的
// 定义出发查找所有使用点,无需遍历整个 CFG
static bool extract_byte_array_defuse(
const reaching_defs::Domain& defs,
const cfg::ControlFlowGraph& cfg,
reaching_defs::MoveAwareFixpointIterator& fp,
std::vector<uint8_t>* out,
std::ostream* trace = nullptr) {
if (defs.is_top() || defs.is_bottom()) {
if (trace) {
*trace << " [defuse] defs is "
<< (defs.is_top() ? "TOP" : "BOTTOM") << ", skip\n";
}
return false;
}

// 1. 找到 NEW_ARRAY 定义(锚点)
IRInstruction* new_arr_insn = nullptr;
if (trace) {
*trace << " [defuse] obj param defs: " << fmt_defs(defs) << "\n";
}
for (IRInstruction* def : defs.elements()) {
if (def->opcode() == OPCODE_NEW_ARRAY) {
new_arr_insn = def;
break;
}
}
if (!new_arr_insn) {
if (trace) {
*trace << " [defuse] no NEW_ARRAY found in defs, skip\n";
}
return false;
}
if (trace) {
*trace << " [defuse] anchor NEW_ARRAY: "
<< show(new_arr_insn) << " (ptr=" << new_arr_insn << ")\n";
}

// 检查 size=0 的空数组(new byte[0])
{
auto arr_it = cfg.find_insn(new_arr_insn);
if (!arr_it.is_end()) {
auto* blk = arr_it.block();
auto env0 = fp.get_entry_state_at(blk);
for (auto& mie : InstructionIterable(blk)) {
if (mie.insn == new_arr_insn) break;
fp.analyze_instruction(mie.insn, &env0);
}
int64_t arr_size = -1;
if (extract_int(env0.get(new_arr_insn->src(0)), &arr_size) &&
arr_size == 0) {
if (trace) {
*trace << " [defuse] array size=0, empty byte[]\n";
}
out->clear();
return true;
}
}
}

// 2. 构建 def-use 链
live_range::MoveAwareChains chains(cfg);
auto def_use = chains.get_def_use_chains();

auto it = def_use.find(new_arr_insn);
if (it == def_use.end()) {
if (trace) {
*trace << " [defuse] NEW_ARRAY has no uses in def-use chain\n";
}
return false;
}

const auto& uses = it->second;
if (trace) {
*trace << " [defuse] NEW_ARRAY has " << uses.size() << " uses:\n";
for (const auto& u : UnorderedIterable(uses)) {
*trace << " use: " << show(u.insn)
<< " src_index=" << u.src_index << "\n";
}
}

// 3. 从 uses 中提取数组内容
out->clear();
bool found_fill = false;

for (const auto& use : UnorderedIterable(uses)) {
auto* insn = use.insn;

// FILL_ARRAY_DATA: src[0] 是数组引用
if (insn->opcode() == OPCODE_FILL_ARRAY_DATA && use.src_index == 0) {
auto payload =
get_fill_array_data_payload<uint8_t>(insn->get_data());
*out = payload;
found_fill = true;
if (trace) {
*trace << " [defuse] FILL_ARRAY_DATA matched!"
<< " payload size=" << payload.size()
<< " bytes: " << fmt_bytes(payload) << "\n";
}
break;
}

// APUT_BYTE: src[1] 是数组引用
if (insn->opcode() == OPCODE_APUT_BYTE && use.src_index == 1) {
// 需要从 fp 获取 index 和 value 的常量值
// 找到 insn 所在的 block,获取该点的 env
auto insn_it = cfg.find_insn(insn);
if (insn_it.is_end()) continue;

auto* block = insn_it.block();
auto env = fp.get_entry_state_at(block);
// 推进 env 到 insn 之前
for (auto& mie : InstructionIterable(block)) {
if (mie.insn == insn) break;
fp.analyze_instruction(mie.insn, &env);
}

int64_t idx = -1, val = 0;
if (extract_int(env.get(insn->src(2)), &idx) &&
extract_int(env.get(insn->src(0)), &val) &&
idx >= 0 && idx < 4096) {
if (trace) {
*trace << " [defuse] APUT_BYTE arr[" << idx
<< "] = " << val << "\n";
}
if (static_cast<size_t>(idx) >= out->size()) {
out->resize(static_cast<size_t>(idx) + 1, 0);
}
(*out)[static_cast<size_t>(idx)] = static_cast<uint8_t>(val);
}
}
}

if (trace) {
*trace << " [defuse] result: found_fill=" << found_fill
<< " size=" << out->size() << "\n";
}
return found_fill || !out->empty();
}

static std::vector<InvokeSiteResult> analyze_caller(
DexMethod* caller,
const DexMethod* target_method,
bool use_defuse = false,
std::ostream* trace = nullptr) {

std::vector<InvokeSiteResult> results;
IRCode* code = caller->get_code();
if (!code) return results;

auto& cfg = code->cfg();

// 构建调用者标签
std::string label = std::string(caller->get_class()->get_name()->str()) +
"." + caller->get_name()->str() +
show(caller->get_proto());

// dump 调用者 IR
std::ostringstream ir_oss;
ir_oss << "define " << label << "\n\n";
dump_cfg(cfg, ir_oss);
std::string caller_ir = ir_oss.str();

// 运行 MoveAware Reaching Definitions
reaching_defs::MoveAwareFixpointIterator fp(cfg);
fp.run(reaching_defs::Environment());

if (trace) {
*trace << "\n══════════════════════════════════════════════════════════════\n";
*trace << "TRACE: " << label << "\n";
*trace << "══════════════════════════════════════════════════════════════\n";
*trace << "Fixpoint iteration completed. Walking blocks...\n\n";
}

// 遍历所有指令,找到目标 invoke
for (auto* block : cfg.blocks()) {
auto env = fp.get_entry_state_at(block);

if (trace) {
*trace << "── B" << block->id() << " entry state ──\n";
dump_env_summary(env, *trace);
*trace << "\n";
}

for (auto& mie : InstructionIterable(block)) {
auto* insn = mie.insn;

if (trace) {
*trace << " >> " << show(insn) << "\n";
}

if (opcode::is_an_invoke(insn->opcode())) {
auto* callee_ref = insn->get_method();
bool match =
callee_ref->get_class()->str() == TARGET_CLASS &&
callee_ref->get_name()->str() == TARGET_METHOD &&
show(callee_ref->get_proto()) == TARGET_PROTO;

if (match) {
if (trace) {
*trace << "\n *** TARGET INVOKE MATCHED ***\n";
*trace << " env at invoke point:\n";
dump_env_summary(env, *trace, " ");
}

InvokeSiteResult r;
r.caller_method = caller;
r.caller_label = label;
r.caller_ir = caller_ir;
r.invoke_insn = insn;
r.invoke_text = show(insn);

// 参数布局: src[0]=i, src[1]=i2, src[2]=j, src[3]=key, src[4]=obj
bool ok = true;

// param i (int)
if (trace) {
*trace << " [param i] src(0)=v" << insn->src(0)
<< " defs=" << fmt_defs(env.get(insn->src(0))) << "\n";
}
ok &= extract_int(env.get(insn->src(0)), &r.param_i);
if (trace) {
*trace << " [param i] extract_int → "
<< (ok ? "ok" : "FAIL") << " val=" << r.param_i << "\n";
}

// param i2 (int)
if (trace) {
*trace << " [param i2] src(1)=v" << insn->src(1)
<< " defs=" << fmt_defs(env.get(insn->src(1))) << "\n";
}
ok &= extract_int(env.get(insn->src(1)), &r.param_i2);
if (trace) {
*trace << " [param i2] extract_int → val=" << r.param_i2 << "\n";
}

// param j (long)
if (trace) {
*trace << " [param j] src(2)=v" << insn->src(2)
<< " defs=" << fmt_defs(env.get(insn->src(2))) << "\n";
}
ok &= extract_int(env.get(insn->src(2)), &r.param_j);
if (trace) {
*trace << " [param j] extract_int → val=" << r.param_j << "\n";
}

// param key (String)
if (trace) {
*trace << " [param key] src(3)=v" << insn->src(3)
<< " defs=" << fmt_defs(env.get(insn->src(3))) << "\n";
}
ok &= extract_string(env.get(insn->src(3)), &r.param_key);
if (trace) {
*trace << " [param key] extract_string → \""
<< r.param_key << "\"\n";
}

// param obj (byte[])
if (trace) {
*trace << " [param obj] src(4)=v" << insn->src(4)
<< " defs=" << fmt_defs(env.get(insn->src(4)))
<< " mode=" << (use_defuse ? "defuse" : "cfg") << "\n";
}
if (use_defuse) {
ok &= extract_byte_array_defuse(
env.get(insn->src(4)), cfg, fp, &r.param_enc, trace);
} else {
ok &= extract_byte_array_cfg(
env.get(insn->src(4)), cfg, fp, &r.param_enc, trace);
}

r.params_resolved = ok;

if (ok && r.param_i == TARGET_I_VAL) {
r.decrypted = douyin_str_dec(r.param_key, r.param_enc);
}

if (trace) {
*trace << " resolved=" << (ok ? "YES" : "NO");
if (!r.decrypted.empty()) {
*trace << " decrypted=\"" << r.decrypted << "\"";
}
*trace << "\n\n";
}

results.push_back(std::move(r));
}
}

fp.analyze_instruction(insn, &env);

if (trace) {
// 只对 move/const 类指令显示状态变化
auto op = insn->opcode();
if (opcode::is_a_move(op) || opcode::is_a_literal_const(op) ||
op == OPCODE_CONST_STRING || op == OPCODE_NEW_ARRAY ||
op == IOPCODE_MOVE_RESULT_PSEUDO_OBJECT) {
if (insn->has_dest()) {
*trace << " → v" << insn->dest()
<< " = " << fmt_defs(env.get(insn->dest())) << "\n";
}
}
}
}
}

return results;
}

// ── 移除不逃逸的死数组构造 ────────────────────────────────────────────────────
// LocalDce 无法消除 aput-byte(堆写入有副作用),需要显式移除。
// 用 def-use chain 找到所有 NEW_ARRAY,若其所有 use 都是数组内部写入
// 或 move(不逃逸),整组指令可安全移除。
//
// 注意:MoveAwareChains 虽然穿透 move 追踪 def,但 MOVE 指令本身仍作为
// use 出现在 chain 里。所以必须把 move 也视为"不逃逸",否则
// NEW_ARRAY → aput × N → MOVE → (已删除的 invoke) 这种模式会被漏掉。
// MOVE 本身无副作用,留给后续 LocalDce 清理。

static bool is_array_internal_use(IROpcode op) {
switch (op) {
// 数组写入
case OPCODE_APUT:
case OPCODE_APUT_BOOLEAN:
case OPCODE_APUT_BYTE:
case OPCODE_APUT_CHAR:
case OPCODE_APUT_SHORT:
case OPCODE_APUT_WIDE:
case OPCODE_APUT_OBJECT:
case OPCODE_FILL_ARRAY_DATA:
// move 只是寄存器别名,不造成逃逸
case OPCODE_MOVE_OBJECT:
case OPCODE_MOVE:
case OPCODE_MOVE_WIDE:
return true;
default:
return false;
}
}

static size_t remove_dead_arrays(cfg::ControlFlowGraph& cfg) {
live_range::MoveAwareChains chains(cfg);
auto def_use = chains.get_def_use_chains();

// 收集要移除的指令
std::unordered_set<IRInstruction*> to_remove;

for (const auto& [def_insn, uses] : UnorderedIterable(def_use)) {
if (def_insn->opcode() != OPCODE_NEW_ARRAY) continue;

// 检查所有 use 是否都是数组内部操作(不逃逸)
bool can_remove = true;
for (const auto& use : UnorderedIterable(uses)) {
if (!is_array_internal_use(use.insn->opcode())) {
can_remove = false;
break;
}
}
if (!can_remove) continue;

// 只标记 NEW_ARRAY 和数组写入指令,MOVE 留给 LocalDce
to_remove.insert(def_insn);
for (const auto& use : UnorderedIterable(uses)) {
auto op = use.insn->opcode();
if (!opcode::is_a_move(op)) {
to_remove.insert(use.insn);
}
}
}

if (to_remove.empty()) return 0;

// 通过 CFGMutation 移除
cfg::CFGMutation mutation(cfg);
for (auto* insn : to_remove) {
auto it = cfg.find_insn(insn);
if (!it.is_end()) {
mutation.remove(it);
}
}
mutation.flush();
return to_remove.size();
}

// ── IR 反混淆:替换解密调用为 CONST_STRING ────────────────────────────────────

static size_t deobfuscate_caller(
DexMethod* caller,
const std::vector<InvokeSiteResult>& sites,
std::ostream* report) {
auto& cfg = caller->get_code()->cfg();
cfg::CFGMutation mutation(cfg);
size_t count = 0;

std::string label = std::string(caller->get_class()->get_name()->str()) +
"." + caller->get_name()->str() +
show(caller->get_proto());

for (const auto& r : sites) {
if (!r.params_resolved || r.decrypted.empty()) continue;

auto invoke_it = cfg.find_insn(r.invoke_insn);
if (invoke_it.is_end()) continue;

auto move_it = cfg.move_result_of(invoke_it);
if (move_it.is_end()) {
// 返回值被丢弃(无 MOVE_RESULT)→ 解密函数无副作用,直接删除
mutation.remove(invoke_it);
if (report) {
*report << " [" << count << "] " << r.invoke_text
<< "\n → (removed, return value unused)\n";
}
} else {
auto dest = move_it->insn->dest();
auto* str_insn = new IRInstruction(OPCODE_CONST_STRING);
str_insn->set_string(DexString::make_string(r.decrypted));
auto* move_pseudo =
new IRInstruction(IOPCODE_MOVE_RESULT_PSEUDO_OBJECT);
move_pseudo->set_dest(dest);
mutation.replace(invoke_it, {str_insn, move_pseudo});
if (report) {
*report << " [" << count << "] " << r.invoke_text
<< "\n → const-string \"" << r.decrypted << "\"\n";
}
}
++count;
}

mutation.flush();

size_t dead_removed = 0;
if (count > 0) {
dead_removed = remove_dead_arrays(cfg);
UnorderedSet<DexMethodRef*> pure_methods;
LocalDce(nullptr, pure_methods).dce(cfg, /* normalize_new_instances */ false);
}

// 输出报告:替换摘要 + 清理后的 IR
if (report && count > 0) {
*report << " dead_arrays_removed: " << dead_removed << "\n\n";
dump_cfg(cfg, *report);
*report << "\n";
}

return count;
}

} // namespace

// ── 配置绑定 ─────────────────────────────────────────────────────────────────

void CallGraphXrefPass::bind_config() {
bind("output_path", m_output_path, m_output_path);
bind("trace_dataflow", m_trace_dataflow, m_trace_dataflow);
bind("trace_output", m_trace_output, m_trace_output);
bind("trace_caller_filter", m_trace_caller_filter, m_trace_caller_filter);
bind("use_defuse_chain", m_use_defuse_chain, m_use_defuse_chain);
bind("deobfuscate", m_deobfuscate, m_deobfuscate);
bind("deobfuscate_output", m_deobfuscate_output, m_deobfuscate_output);
bind("mapping_output", m_mapping_output, m_mapping_output);
}

// ── run_pass ─────────────────────────────────────────────────────────────────

void CallGraphXrefPass::run_pass(DexStoresVector& stores,
ConfigFiles& /* conf */,
PassManager& mgr) {
auto scope = build_class_scope(stores);

TRACE(MAIN, 1, "[CallGraphXrefPass] Building method override graph...");
auto override_graph = method_override_graph::build_graph(scope);

TRACE(MAIN, 1, "[CallGraphXrefPass] Building call graph...");
auto cg = call_graph::single_callee_graph(*override_graph, scope);

auto stats = call_graph::get_num_nodes_edges(cg);
TRACE(MAIN, 1, "[CallGraphXrefPass] Call graph: %u nodes, %u edges",
stats.num_nodes, stats.num_edges);

// 在 scope 中查找目标方法
const DexMethod* target = nullptr;
for (auto* cls : scope) {
if (cls->get_name()->str() != TARGET_CLASS) continue;
for (auto* m : cls->get_dmethods()) {
if (m->get_name()->str() == TARGET_METHOD &&
show(m->get_proto()) == TARGET_PROTO) {
target = m;
break;
}
}
if (!target) {
for (auto* m : cls->get_vmethods()) {
if (m->get_name()->str() == TARGET_METHOD &&
show(m->get_proto()) == TARGET_PROTO) {
target = m;
break;
}
}
}
if (target) break;
}

if (!target) {
TRACE(MAIN, 1, "[CallGraphXrefPass] Target method not found in scope");
mgr.set_metric("callsites", 0);
mgr.disable_checker();
return;
}

TRACE(MAIN, 1, "[CallGraphXrefPass] Found target: %s", SHOW(target));

// 通过 Call Graph 获取所有调用者
const auto& callers = cg.get_callers(target);
TRACE(MAIN, 1, "[CallGraphXrefPass] Found %zu callers via call graph",
callers.size());

// 打开 trace 文件(如果启用)
std::unique_ptr<std::ofstream> trace_ofs;
if (m_trace_dataflow) {
trace_ofs = std::make_unique<std::ofstream>(m_trace_output);
*trace_ofs << "# CallGraphXrefPass — Dataflow Trace\n";
*trace_ofs << "# Target: " << TARGET_CLASS << "." << TARGET_METHOD
<< TARGET_PROTO << "\n";
if (!m_trace_caller_filter.empty()) {
*trace_ofs << "# Filter: " << m_trace_caller_filter << "\n";
}
*trace_ofs << "\n";
TRACE(MAIN, 1, "[CallGraphXrefPass] Trace enabled → %s",
m_trace_output.c_str());
}

TRACE(MAIN, 1, "[CallGraphXrefPass] byte[] extraction mode: %s",
m_use_defuse_chain ? "def-use chain" : "CFG traversal");

// 遍历所有调用者,分析每个调用点
std::vector<InvokeSiteResult> all_results;
for (const DexMethod* caller_const : UnorderedIterable(callers)) {
auto* caller = const_cast<DexMethod*>(caller_const);
if (!caller->get_code()) continue;

// 决定是否对这个 caller 启用 trace
std::ostream* trace_ptr = nullptr;
if (trace_ofs) {
if (m_trace_caller_filter.empty()) {
// 空 filter → trace 全部 caller
trace_ptr = trace_ofs.get();
} else {
// 匹配 filter 子串
std::string caller_name =
std::string(caller->get_class()->get_name()->str()) +
"." + caller->get_name()->str();
if (caller_name.find(m_trace_caller_filter) != std::string::npos) {
trace_ptr = trace_ofs.get();
}
}
}

caller->get_code()->build_cfg();
auto sites = analyze_caller(caller, target, m_use_defuse_chain, trace_ptr);
for (auto& s : sites) {
all_results.push_back(std::move(s));
}
}

TRACE(MAIN, 1, "[CallGraphXrefPass] Analyzed %zu invoke sites",
all_results.size());

// 写入结果文件
std::ofstream ofs(m_output_path);
ofs << "# CallGraphXrefPass — Call Graph + Reaching Definitions\n";
ofs << "# Target: " << TARGET_CLASS << "." << TARGET_METHOD
<< TARGET_PROTO << "\n";
ofs << "# Callers (via CG): " << callers.size() << "\n";
ofs << "# Invoke sites: " << all_results.size() << "\n\n";

size_t resolved_count = 0;
size_t decrypted_count = 0;

for (const auto& r : all_results) {
ofs << "════════════════════════════════════════════════════════════\n";
ofs << "Caller: " << r.caller_label << "\n";
ofs << "════════════════════════════════════════════════════════════\n\n";

// 输出调用者 IR
ofs << r.caller_ir << "\n";

// 输出参数分析
ofs << "── Parameter Analysis ──\n";
ofs << " invoke: " << r.invoke_text << "\n";
if (r.params_resolved) {
ofs << " i = " << r.param_i << "\n";
ofs << " i2 = " << r.param_i2 << "\n";
ofs << " j = " << r.param_j << "\n";
ofs << " key = \"" << r.param_key << "\"\n";
ofs << " enc = " << fmt_bytes(r.param_enc) << "\n";
++resolved_count;
if (!r.decrypted.empty()) {
ofs << " >>> DECRYPTED: \"" << r.decrypted << "\"\n";
++decrypted_count;
}
} else {
ofs << " (some params not statically resolved)\n";
if (r.param_i >= 0) ofs << " i = " << r.param_i << "\n";
if (r.param_i2 >= 0) ofs << " i2 = " << r.param_i2 << "\n";
if (r.param_j >= 0) ofs << " j = " << r.param_j << "\n";
if (!r.param_key.empty())
ofs << " key = \"" << r.param_key << "\"\n";
if (!r.param_enc.empty())
ofs << " enc = " << fmt_bytes(r.param_enc) << "\n";
}
ofs << "\n";
}

TRACE(MAIN, 1,
"[CallGraphXrefPass] sites=%zu resolved=%zu decrypted=%zu output=%s",
all_results.size(), resolved_count, decrypted_count,
m_output_path.c_str());

// ── 输出 key → decrypted 映射文件 ──
if (!m_mapping_output.empty() && decrypted_count > 0) {
std::ofstream mfs(m_mapping_output);
mfs << "# CallGraphXrefPass — String Mapping\n";
mfs << "# Format: key_string → decrypted_string (caller)\n\n";
for (const auto& r : all_results) {
if (r.decrypted.empty()) continue;
mfs << "\"" << r.param_key << "\" → \"" << r.decrypted
<< "\" # " << r.caller_label << "\n";
}
TRACE(MAIN, 1, "[CallGraphXrefPass] Mapping → %s (%zu entries)",
m_mapping_output.c_str(), decrypted_count);
}

// ── 反混淆阶段:替换解密调用为 CONST_STRING ──
size_t replaced_count = 0;
if (m_deobfuscate) {
std::unique_ptr<std::ofstream> report_ofs;
if (!m_deobfuscate_output.empty()) {
report_ofs = std::make_unique<std::ofstream>(m_deobfuscate_output);
*report_ofs << "# CallGraphXrefPass — Deobfuscation Report\n";
*report_ofs << "# Target: " << TARGET_CLASS << "." << TARGET_METHOD
<< TARGET_PROTO << "\n\n";
}

std::unordered_map<DexMethod*, std::vector<InvokeSiteResult>> by_caller;
for (auto& r : all_results) {
by_caller[r.caller_method].push_back(r);
}
for (auto& [caller, sites] : by_caller) {
std::ostream* rpt = report_ofs ? report_ofs.get() : nullptr;
std::string label =
std::string(caller->get_class()->get_name()->str()) + "." +
caller->get_name()->str() + show(caller->get_proto());

if (rpt) {
*rpt << "════════════════════════════════════════════════════════════\n";
*rpt << "Caller: " << label << "\n";
*rpt << "════════════════════════════════════════════════════════════\n";
}

replaced_count += deobfuscate_caller(caller, sites, rpt);
}

if (report_ofs) {
TRACE(MAIN, 1,
"[CallGraphXrefPass] Deobfuscation report → %s",
m_deobfuscate_output.c_str());
}
TRACE(MAIN, 1,
"[CallGraphXrefPass] Deobfuscated: replaced %zu invoke sites",
replaced_count);
}

mgr.set_metric("callsites",
static_cast<int64_t>(all_results.size()));
mgr.set_metric("resolved",
static_cast<int64_t>(resolved_count));
mgr.set_metric("decrypted",
static_cast<int64_t>(decrypted_count));
mgr.set_metric("replaced",
static_cast<int64_t>(replaced_count));
mgr.disable_checker();
}

static CallGraphXrefPass s_pass;

实际运行数据:

指标 数值 说明
Call Graph 规模 2,639,335 节点 / 11,513,753 边 全程序调用图
唯一调用者 181 通过 cg.get_callers() 精确获取
调用点总数 562 一个方法可能多次调用解密函数
参数解析成功 519(92.3%) 5 个参数全部静态确定
解密成功 518(92.2%) param_i == 0x1000001 且参数完整
IR 替换成功 518(100%) 含 60 个返回值丢弃的调用(直接删除)

解密出的字符串涵盖:HTTP 头(CookieUser-AgentContent-Type)、设备厂商标识(HUAWEISAMSUNGXIAOMI)、SDK 内部标识(ByteDance-MSSDKDouYinLive)、系统服务名(android.os.ServiceManager)等。

验证方式:用 jadx 打开输出 DEX,搜索已知解密结果(如 "com.uodis.opendevice.aidl.OpenDeviceIdentifierService"),确认原来的 a.a(16777217, 0, 0L, "350312", new byte[]{...}) 已被替换为直接的字符串常量。

至此,反混淆的完整流程走通了:Call Graph 定位调用者 → Reaching Definitions 提取参数 → CFGMutation 替换 IR → Def-Use Chain 清理死代码。四个工具各司其职,缺一不可。

接下来换一个方向——如果我们是混淆的实施者,如何用 ReDex 编写混淆 Pass?


第五部分:实战 — 用 ReDex 编写混淆 Pass

第四部分是”拆”——把混淆还原为明文。第五部分是”装”——把明文变成混淆。两者是同一枚硬币的两面,使用的 IR 操作完全对称。

5.1 常量混淆:ConstObfuscatorPass

最简单的混淆——把整数常量变成”常量 + 运算”的组合,静态分析时无法直接看到原始值。

变换规则:

1
2
3
before:  CONST vX, N
after: CONST vX, (N ^ MASK)
XOR_INT_LIT vX, vX, MASK

例如 CONST v0, 42 变成 CONST v0, 0x60 + XOR_INT_LIT v0, v0, 0x5A。运行时 0x60 ^ 0x5A = 42,语义不变。

实现只需要 CFGMutation 的 insert_after——在每条 CONST 后面插入一条 XOR:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
walk::parallel::code(scope, [&](DexMethod* method, IRCode& code) {
auto& cfg = code.cfg();
cfg::CFGMutation mutation(cfg);

for (auto it = cfg::InstructionIterable(cfg).begin();
it != cfg::InstructionIterable(cfg).end(); ++it) {
auto* insn = it->insn;
if (insn->opcode() != OPCODE_CONST) continue;

int64_t val = insn->get_literal();
int64_t masked = val ^ mask;
insn->set_literal(masked); // 原地修改常量值
//插入xor指令来恢复正确常量值
auto* xor_insn = new IRInstruction(OPCODE_XOR_INT_LIT);
xor_insn->set_dest(insn->dest());
xor_insn->set_src(0, insn->dest());
xor_insn->set_literal(mask);
mutation.insert_after(it, {xor_insn});
}

mutation.flush();
});

注意 walk::parallel::code 的使用——多线程并行处理不同方法,每个方法内部的 CFGMutation 互不干扰。这是第一部分提到的并行遍历工具的实际应用。

5.2 字符串加密:StringEncryptorPass

常量混淆只处理整数。字符串加密更复杂——需要把一条 CONST_STRING 替换为一整组指令:密钥 + 密文数组 + 解密函数调用。

变换规则:

这正是第四部分反混淆的逆操作——第四部分把右边还原为左边,这里把左边变成右边。

实现的核心挑战是临时寄存器分配。DEX 方法的 code_item 头部声明了 registers_size——这个方法总共能使用多少个寄存器。比如 registers_size = 8 意味着只有 v0–v7 合法,指令中出现 v8 会被 DEX verifier 拒绝。

而且 Dalvik 的寄存器布局是参数占据最高位

1
2
3
registers_size = 8,方法有 3 个参数

v0 v1 v2 v3 v4 | 参数(p0 p1 p2)

原始的 CONST_STRING 只用一个寄存器 v0,但替换后需要额外的寄存器存放 key、数组引用等。不能简单地在末尾追加——那会和参数位置冲突。

解决方案:用 transform::remap_registers 将现有参数寄存器整体上移,腾出低位空间:

1
2
3
4
5
6
替换前:v0..v4 是局部变量,v5..v7 是参数
[locals: 0-4] [params: 5-7]

remap_registers(+3):参数上移 3 个位置
[locals: 0-4] [temps: 5-7] [params: 8-10]
↑ 新腾出的临时寄存器

这样 v5、v6、v7 就可以安全使用,不会与参数冲突。

byte[] 密文的构造随机选择两种模式之一——FILL_ARRAY_DATA(一条指令批量填充)或 APUT_BYTE(逐字节填充)。两种模式在反编译器中呈现不同的代码形态,增加分析难度:

1
2
3
4
5
6
7
8
9
// 模式 A(FILL_ARRAY_DATA)→ 反编译为数组字面量
byte[] enc = new byte[]{33, 56, 78, 9, 27, ...};

// 模式 B(APUT_BYTE)→ 反编译为逐行赋值
byte[] enc = new byte[5];
enc[0] = 33;
enc[1] = 56;
enc[2] = 78;
// ...

FILL_ARRAY_DATA 的 payload 需要用 encode_fill_array_data_payload<int8_t>() 构造——这是 ReDex 提供的工具函数,将 vector<int8_t> 编码为 Dalvik 的 fill-array-data 格式。

5.3 对称性:混淆与反混淆是同一套工具的两种用法

回顾两个方向的操作,它们在 IR 层面完全对称:

混淆(StringEncryptorPass) 反混淆(CallGraphXrefPass)
输入 CONST_STRING "hello" INVOKE_STATIC ..., target
输出 key + NEW_ARRAY + FILL_ARRAY_DATA + INVOKE_STATIC CONST_STRING "hello"
核心操作 mutation.replace(const_string_it, {...}) mutation.replace(invoke_it, {...})
寄存器 remap_registers 腾出临时空间 复用原 MOVE_RESULT 的 dest
清理 无需(替换即完成) remove_dead_arrays + LocalDce
分析依赖 无(纯变换) Call Graph + Reaching Defs + Def-Use Chain

关键洞察:混淆不需要分析,反混淆需要。混淆是确定性的——每条 CONST_STRING 都可以无条件替换。反混淆是不确定的——必须先证明参数是常量,才能执行解密。这就是为什么反混淆需要 Reaching Definitions 等分析工具,而混淆不需要。


第六部分:总结

6.1 知识图谱

回顾全文的知识结构

6.2 延伸

通过了解整个架构会发现这个模式几乎和 llvm ir 一致,其实 revng 就实现了类似的架构,二进制文件 -> LLVM IR,LLVM IR -> 二进制文件,但是很久之前尝试过效果不太好,回编译比较困难

References