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) { });
walk::parallel::code 自动跳过没有代码体的抽象方法和 native 方法,多线程并行处理,充分利用多核。
到这里,我们知道了 ReDex 的整体架构:DEX 加载为内存数据结构 → Pass 遍历方法 → 操作 IRCode。下一步的问题是:IRCode 里面到底是什么?
第二部分:ReDex IR — 比 Dalvik 更好操作的中间表示 2.1 从 Dalvik 到 IR:为什么需要一层抽象 Pass 直接操作 Dalvik 字节码会遇到很多”编码层面”的麻烦:
同一个语义有多种编码:const/4、const/16、const/high16、const 都是加载整数常量
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_CONST 用 m_literal,OPCODE_CONST_STRING 用 m_string,OPCODE_INVOKE_STATIC 用 m_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
转换的核心规则:
线性 → CFG :MFLOW_TARGET 标签变成边,GOTO 指令被吸收为 EDGE_GOTO,IF_* 产生两条边(EDGE_GOTO = false 分支,EDGE_BRANCH = true 分支)
CFG → 线性 :边变回 MFLOW_TARGET 标签和显式的 GOTO 指令,基本块按拓扑序拼接
两种模式的转换 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 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; } } 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 MyFixpointIterator fp (cfg) ;fp.run (MyDomain::initial_value ()); 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" 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; auto defs = env.get (insn->src (0 )); for (IRInstruction* def : defs.elements ()) { } 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" auto override_graph = method_override_graph::build_graph (scope);auto cg = call_graph::single_callee_graph (*override_graph, scope);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)) { } }
Def-Use Chain 的典型用途:判断某个值是否”逃逸”——如果一个定义的所有 use 都是特定类型的指令,就可以对这组指令做整体优化或删除。具体应用将在第四部分的实战中展示。
至此,三类分析能力形成了完整的工具箱:Reaching Definitions 追踪值的来源,Call Graph 定位调用关系,Def-Use Chain 追踪值的去向。接下来,我们用这些工具解决一个真实问题。
第四部分:实战 — 用 ReDex 反混淆字符串加密 4.1 目标 某 APP 使用了字符串加密保护。所有敏感字符串在 DEX 中不以明文存在,而是通过一个统一的解密函数在运行时还原:
1 2 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 方向找到唯一的 CONST 或 CONST_STRING 定义:
1 2 3 4 live_range::MoveAwareChains chains (cfg) ;auto use_def = chains.get_use_def_chains (); auto def_use = chains.get_def_use_chains ();
对于 int 参数,从 invoke 的 use 出发找 def:如果唯一定义是 OPCODE_CONST,直接取 get_literal()。对于 string 参数,如果唯一定义是 OPCODE_CONST_STRING,取 get_string()->str()。
但第 5 个参数 obj(byte[] 密文)的提取要复杂得多。byte[] 在 IR 中不是一条指令,而是一组指令的协作:
提取思路——基于 Def-Use Chain,不需要遍历整个 CFG:
从 invoke 的 src[4] 通过 use→def 找到 NEW_ARRAY 定义指令
从 NEW_ARRAY 通过 def→use 找到它的所有使用者
在使用者中筛选 FILL_ARRAY_DATA 或 APUT_BYTE,直接提取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 IRInstruction* new_arr_insn = ; auto it = def_use.find (new_arr_insn);const auto & uses = it->second;for (const auto & use : uses) { if (use.insn->opcode () == OPCODE_FILL_ARRAY_DATA && use.src_index == 0 ) { *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 ) { } }
与遍历整个 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 { 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 ();
两个要点:
CONST_STRING 必须配对 MOVE_RESULT_PSEUDO_OBJECT 。回顾 2.3 节——CONST_STRING 是 may-throw 指令,dest 在伪指令中。漏掉这条伪指令,寄存器未定义,运行时崩溃。
mutation.replace()** 自动移除关联的 MOVE_RESULT**。当锚点指令有 has_move_result_any() 时(如 INVOKE_STATIC 后面的 MOVE_RESULT_OBJECT),replace 会自动跳过并移除它。所以我们只需 replace invoke,MOVE_RESULT_OBJECT 自动消失。
替换完成后,原来为 invoke 准备参数的指令全部变成了死代码。但它们不会自动消失——需要显式清理。
4.5 针对性死代码消除 替换后的死代码分两类:
类型
示例
特点
无副作用指令
CONST、CONST_WIDE、CONST_STRING、MOVE
LocalDce 可自动清理
有副作用指令
NEW_ARRAY、APUT_BYTE、FILL_ARRAY_DATA
堆写入,LocalDce 不处理
LocalDce(Local Dead Code Elimination)是 ReDex 内置的死代码消除服务。它能清理所有”结果未被使用且无副作用”的指令。但 APUT_BYTE 是堆写入——从类型系统看它有副作用,LocalDce 不敢删。
解决方案:两阶段清理。
Phase 1:死数组消除 。这里用到了第三部分介绍的 Def-Use Chain——从 NEW_ARRAY 的定义出发,查找它的所有 use。如果所有 use 都是数组内部操作(APUT_BYTE、FILL_ARRAY_DATA、MOVE),说明数组没有逃逸到外部,整组指令可以安全移除:
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) { } }
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 #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 ; 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 (); } 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 (); } static void dump_env_summary (const reaching_defs::Environment& env, std::ostream& out, const char * prefix = " " ) { 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" ; } } 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; }; 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; } 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 ; } 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" ; } { 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 ; } } } 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 (); } 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 ; } 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" ; } { 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 ; } } } 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" ; } } out->clear (); bool found_fill = false ; for (const auto & use : UnorderedIterable (uses)) { auto * insn = use.insn; 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 ; } if (insn->opcode () == OPCODE_APUT_BYTE && use.src_index == 1 ) { 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); 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 ()); std::ostringstream ir_oss; ir_oss << "define " << label << "\n\n" ; dump_cfg (cfg, ir_oss); std::string caller_ir = ir_oss.str (); 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" ; } 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); bool ok = true ; 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" ; } 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" ; } 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" ; } 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" ; } 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) { 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; } 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: 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 ; 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 ; 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 ; 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 (); } 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 ()) { 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, false ); } if (report && count > 0 ) { *report << " dead_arrays_removed: " << dead_removed << "\n\n" ; dump_cfg (cfg, *report); *report << "\n" ; } return count; } } 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); } void CallGraphXrefPass::run_pass (DexStoresVector& stores, ConfigFiles& , 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); 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)); const auto & callers = cg.get_callers (target); TRACE (MAIN, 1 , "[CallGraphXrefPass] Found %zu callers via call graph" , callers.size ()); 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 ; std::ostream* trace_ptr = nullptr ; if (trace_ofs) { if (m_trace_caller_filter.empty ()) { trace_ptr = trace_ofs.get (); } else { 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" ; 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 ()); 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); } 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 头(Cookie、User-Agent、Content-Type)、设备厂商标识(HUAWEI、SAMSUNG、XIAOMI)、SDK 内部标识(ByteDance-MSSDK、DouYinLive)、系统服务名(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); 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 byte [] enc = new byte []{33 , 56 , 78 , 9 , 27 , ...};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