花指令与 OLLVM 混淆技术深度解析
在 Android Native 层逆向分析中,花指令(Junk Code)和 OLLVM(Obfuscator-LLVM)是两种最常见的代码混淆技术。花指令通过插入无意义的指令干扰反汇编器,而 OLLVM 则在编译阶段对控制流和数据流进行深度混淆。本文将深入分析这两种技术的原理、识别方法和去混淆策略。
1. 花指令基础与原理
1.1 什么是花指令
花指令(Junk Code / Garbage Code)是指插入到程序中的无意义指令序列,其目的是:
干扰反汇编器 :使反汇编器产生错误的解析结果增加分析难度 :混淆真实的程序逻辑对抗静态分析 :阻止自动化分析工具识别代码模式1.2 花指令分类
1.2.1 可执行花指令
这类花指令会被执行,但不影响程序逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; ARM64 可执行花指令示例
nop ; 空操作
mov x0 , x0 ; 自身赋值
add x1 , x1 , #0 ; 加0操作
eor x2 , x2 , #0 ; 异或0操作
and x3 , x3 , #-1 ; 与全1操作
orr x4 , x4 , #0 ; 或0操作
; 更复杂的等价操作
push { x0 } ; 压栈
pop { x0 } ; 出栈(恢复原值)
sub sp , sp , #16 ; 分配栈空间
add sp , sp , # 16 ; 释放栈空间(抵消)
1.2.2 不可达花指令
这类花指令永远不会被执行,用于干扰反汇编:
1
2
3
4
5
6
7
8
9
10
; ARM64 不可达花指令示例
cmp x0 , x0 ; 比较相同值
b.ne junk_code ; 永假条件跳转
; ... 正常代码 ...
b continue
junk_code:
.byte 0xFF , 0xFF , 0xFF , 0xFF ; 垃圾数据
.byte 0x00 , 0x00 , 0x00 , 0x00
continue:
; ... 继续正常执行 ...
1.2.3 数据与代码混合
将数据插入代码段,干扰线性反汇编:
1
2
3
4
5
6
; x86 数据混合示例
jmp skip_data ; 跳过数据区
.byte 0xE8 ; 看起来像 call 指令
.byte 0x90 , 0x90 ; 填充数据
skip_data:
mov eax , ebx ; 正常代码
1.3 花指令实现技术
1.3.1 永假跳转模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// C++ 编译时花指令生成
#define JUNK_CODE_1() \
__asm__ volatile ( \
"cmp x0, x0\n\t" \
"b.ne 1f\n\t" \
"b 2f\n\t" \
"1:\n\t" \
".byte 0xFF, 0xFF, 0xFF, 0xFF\n\t" \
"2:\n\t" \
)
// 使用示例
void protected_function () {
JUNK_CODE_1 ();
// 真实代码
int result = calculate_key ();
JUNK_CODE_1 ();
return result ;
}
1.3.2 不透明谓词(Opaque Predicate)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 不透明谓词:看似条件分支,实际永远为真/假
int opaque_true () {
int x = rand ();
// (x * x) % 2 == 0 永远为真(平方数的奇偶性)
return (( x * x ) % 2 == 0 );
}
int opaque_false () {
int x = rand ();
// (x * (x + 1)) % 2 != 0 永远为假
return (( x * ( x + 1 )) % 2 != 0 );
}
void obfuscated_function () {
if ( opaque_true ()) {
// 真实代码路径
real_logic ();
} else {
// 垃圾代码,永远不会执行
garbage_code_1 ();
garbage_code_2 ();
}
}
1.3.3 多字节指令欺骗
1
2
3
4
5
; x86 多字节指令欺骗
jmp short skip + 1 ; 跳到下一条指令的中间
skip:
.byte 0xE8 ; 看起来是 CALL 指令开始
mov eax , 1 ; 实际执行的指令
1.4 花指令对反汇编的影响
线性扫描反汇编器(Linear Sweep) :
从代码段起始顺序解析 遇到花指令会产生错误解析 IDA 早期版本使用此方法 递归下降反汇编器(Recursive Descent) :
跟随控制流解析代码 对不可达花指令有更好的处理能力 IDA Pro、Ghidra 使用此方法 2. 花指令识别与去除
2.1 静态识别方法
2.1.1 IDA Pro 脚本识别花指令
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
# IDA Python 脚本:识别可疑的花指令模式
import idautils
import idc
import idaapi
class JunkCodeDetector :
def __init__ ( self ):
self . junk_patterns = []
def detect_nop_sleds ( self , func_ea ):
"""检测 NOP 滑板"""
nop_count = 0
for head in idautils . FuncItems ( func_ea ):
mnem = idc . print_insn_mnem ( head )
if mnem == "NOP" or mnem == "nop" :
nop_count += 1
else :
if nop_count > 3 :
print ( f "[!] NOP sled detected at 0x { head - nop_count : x } , count: { nop_count } " )
self . junk_patterns . append (( head - nop_count , nop_count , "NOP_SLED" ))
nop_count = 0
return self . junk_patterns
def detect_dead_code ( self , func_ea ):
"""检测死代码(永远不会执行的代码)"""
func = idaapi . get_func ( func_ea )
if not func :
return []
# 获取函数的控制流图
flowchart = idaapi . FlowChart ( func )
reachable_blocks = set ()
# BFS 遍历可达基本块
queue = [ flowchart [ 0 ] . start_ea ]
while queue :
block_ea = queue . pop ( 0 )
if block_ea in reachable_blocks :
continue
reachable_blocks . add ( block_ea )
for block in flowchart :
if block . start_ea == block_ea :
for succ in block . succs ():
queue . append ( succ . start_ea )
break
# 找出不可达的基本块
dead_blocks = []
for block in flowchart :
if block . start_ea not in reachable_blocks :
print ( f "[!] Dead code block at 0x { block . start_ea : x } " )
dead_blocks . append ( block . start_ea )
return dead_blocks
def detect_opaque_predicates ( self , func_ea ):
"""检测不透明谓词"""
suspicious_patterns = []
for head in idautils . FuncItems ( func_ea ):
mnem = idc . print_insn_mnem ( head )
# 检测 cmp reg, reg 后跟条件跳转
if mnem . lower () == "cmp" :
op1 = idc . print_operand ( head , 0 )
op2 = idc . print_operand ( head , 1 )
if op1 == op2 :
next_insn = idc . next_head ( head )
next_mnem = idc . print_insn_mnem ( next_insn )
if next_mnem . lower () . startswith ( "j" ) or next_mnem . lower () . startswith ( "b" ):
print ( f "[!] Opaque predicate at 0x { head : x } : { mnem } { op1 } , { op2 } " )
suspicious_patterns . append (( head , "OPAQUE_PREDICATE" ))
return suspicious_patterns
def analyze_function_junk_code ( func_ea ):
"""分析函数中的花指令"""
detector = JunkCodeDetector ()
print ( f "[*] Analyzing function at 0x { func_ea : x } " )
nop_sleds = detector . detect_nop_sleds ( func_ea )
dead_blocks = detector . detect_dead_code ( func_ea )
opaque_preds = detector . detect_opaque_predicates ( func_ea )
return {
'nop_sleds' : nop_sleds ,
'dead_blocks' : dead_blocks ,
'opaque_predicates' : opaque_preds
}
# 使用示例
# results = analyze_function_junk_code(idc.here())
2.1.2 基于模式匹配的检测
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
# 花指令模式签名库
ARM64_JUNK_PATTERNS = [
# (pattern_bytes, mask, description)
( b ' \x1f\x00\x00\xeb ' , b ' \xff\xff\xff\xff ' , 'cmp x0, x0' ),
( b ' \x00\x00\x00\x14 ' , b ' \x00\x00\x00\x1f ' , 'unconditional branch to next' ),
( b ' \xe0\x03\x00\xaa ' , b ' \xff\xff\xff\xff ' , 'mov x0, x0' ),
]
X86_JUNK_PATTERNS = [
( b ' \x90 ' , b ' \xff ' , 'nop' ),
( b ' \x89\xc0 ' , b ' \xff\xff ' , 'mov eax, eax' ),
( b ' \x31\xc0\x31\xc0 ' , b ' \xff\xff\xff\xff ' , 'xor eax, eax; xor eax, eax' ),
( b ' \xeb\x00 ' , b ' \xff\xff ' , 'jmp $+2 (next instruction)' ),
]
def scan_junk_patterns ( binary_data , patterns , base_addr = 0 ):
"""扫描二进制数据中的花指令模式"""
findings = []
for offset in range ( len ( binary_data )):
for pattern , mask , desc in patterns :
if offset + len ( pattern ) > len ( binary_data ):
continue
match = True
for i in range ( len ( pattern )):
if ( binary_data [ offset + i ] & mask [ i ]) != ( pattern [ i ] & mask [ i ]):
match = False
break
if match :
findings . append ({
'address' : base_addr + offset ,
'pattern' : pattern . hex (),
'description' : desc
})
return findings
2.2 动态去花方法
2.2.1 Frida 动态跟踪执行路径
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
// Frida 脚本:跟踪实际执行的指令,区分花指令
function trace_real_execution ( module_name , func_offset , trace_count ) {
var module = Process . findModuleByName ( module_name );
if ( ! module ) {
console . log ( "Module not found: " + module_name );
return ;
}
var func_addr = module . base . add ( func_offset );
var executed_addresses = new Set ();
var instruction_count = 0 ;
Stalker . follow ( Process . getCurrentThreadId (), {
transform : function ( iterator ) {
var instruction = iterator . next ();
while ( instruction !== null ) {
var addr = instruction . address ;
// 检查是否在目标模块范围内
if ( addr . compare ( module . base ) >= 0 &&
addr . compare ( module . base . add ( module . size )) < 0 ) {
iterator . putCallout ( function ( context ) {
var pc = context . pc ;
if ( ! executed_addresses . has ( pc . toString ())) {
executed_addresses . add ( pc . toString ());
var insn = Instruction . parse ( pc );
console . log ( "[EXEC] " + pc + ": " + insn . mnemonic + " " + insn . opStr );
instruction_count ++ ;
if ( instruction_count >= trace_count ) {
Stalker . unfollow ();
}
}
});
}
iterator . keep ();
instruction = iterator . next ();
}
}
});
// 调用目标函数
var target_func = new NativeFunction ( func_addr , 'void' , []);
target_func ();
Stalker . unfollow ();
Stalker . flush ();
console . log ( "\n[*] Total unique instructions executed: " + executed_addresses . size );
return Array . from ( executed_addresses );
}
// 对比静态反汇编结果与动态执行结果
function identify_junk_by_execution ( module_name , func_offset ) {
var module = Process . findModuleByName ( module_name );
var func_addr = module . base . add ( func_offset );
// 获取静态反汇编的所有地址
var static_addresses = [];
var current = func_addr ;
for ( var i = 0 ; i < 100 ; i ++ ) { // 假设函数不超过100条指令
try {
var insn = Instruction . parse ( current );
static_addresses . push ( current . toString ());
current = current . add ( insn . size );
} catch ( e ) {
break ;
}
}
// 获取动态执行的地址
var executed = trace_real_execution ( module_name , func_offset , 1000 );
// 找出从未执行的指令(潜在花指令)
var junk_candidates = static_addresses . filter ( addr => ! executed . includes ( addr ));
console . log ( "\n[*] Potential junk code (never executed):" );
junk_candidates . forEach ( function ( addr ) {
var insn = Instruction . parse ( ptr ( addr ));
console . log ( " " + addr + ": " + insn . mnemonic + " " + insn . opStr );
});
return junk_candidates ;
}
2.2.2 Unicorn 模拟执行去花
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
from unicorn import *
from unicorn.arm64_const import *
from capstone import *
import struct
class JunkCodeRemover :
def __init__ ( self , binary_data , base_addr = 0x10000 ):
self . binary = binary_data
self . base_addr = base_addr
self . executed_addresses = set ()
self . md = Cs ( CS_ARCH_ARM64 , CS_MODE_ARM )
def hook_code ( self , uc , address , size , user_data ):
"""记录执行的每条指令"""
self . executed_addresses . add ( address )
# 打印执行的指令
code = uc . mem_read ( address , size )
for insn in self . md . disasm ( bytes ( code ), address ):
print ( f "[EXEC] 0x { insn . address : x } : { insn . mnemonic } { insn . op_str } " )
def emulate_function ( self , func_offset , max_instructions = 1000 ):
"""模拟执行函数,收集执行路径"""
try :
# 初始化 Unicorn 引擎 (ARM64)
mu = Uc ( UC_ARCH_ARM64 , UC_MODE_ARM )
# 映射内存
mu . mem_map ( self . base_addr , 2 * 1024 * 1024 ) # 2MB
mu . mem_write ( self . base_addr , self . binary )
# 设置栈
stack_addr = self . base_addr + 0x100000
mu . reg_write ( UC_ARM64_REG_SP , stack_addr )
# 设置代码 Hook
mu . hook_add ( UC_HOOK_CODE , self . hook_code )
# 开始模拟
func_addr = self . base_addr + func_offset
mu . emu_start ( func_addr , func_addr + 0x10000 , count = max_instructions )
except UcError as e :
print ( f "Emulation error: { e } " )
return self . executed_addresses
def identify_junk_code ( self , func_offset , func_size ):
"""识别函数中的花指令"""
# 静态反汇编获取所有指令地址
static_addresses = set ()
func_binary = self . binary [ func_offset : func_offset + func_size ]
for insn in self . md . disasm ( func_binary , self . base_addr + func_offset ):
static_addresses . add ( insn . address )
# 模拟执行获取实际执行的指令
self . emulate_function ( func_offset )
# 差集即为花指令
junk_addresses = static_addresses - self . executed_addresses
print ( f " \n [*] Static instructions: { len ( static_addresses ) } " )
print ( f "[*] Executed instructions: { len ( self . executed_addresses ) } " )
print ( f "[*] Junk instructions: { len ( junk_addresses ) } " )
# 输出花指令详情
print ( " \n [*] Junk code details:" )
for addr in sorted ( junk_addresses ):
offset = addr - self . base_addr
code = self . binary [ offset : offset + 4 ]
for insn in self . md . disasm ( code , addr ):
print ( f " 0x { insn . address : x } : { insn . mnemonic } { insn . op_str } " )
return junk_addresses
def patch_junk_code ( self , junk_addresses , output_path ):
"""将花指令替换为 NOP"""
patched_binary = bytearray ( self . binary )
for addr in junk_addresses :
offset = addr - self . base_addr
# ARM64 NOP = 0x1F 0x20 0x03 0xD5
patched_binary [ offset : offset + 4 ] = b ' \x1f\x20\x03\xd5 '
with open ( output_path , 'wb' ) as f :
f . write ( patched_binary )
print ( f "[*] Patched binary saved to { output_path } " )
# 使用示例
# with open('target.so', 'rb') as f:
# binary = f.read()
# remover = JunkCodeRemover(binary)
# junk = remover.identify_junk_code(func_offset=0x1234, func_size=0x200)
# remover.patch_junk_code(junk, 'target_cleaned.so')
2.3 IDA Pro 自动去花插件
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
# IDA Pro 花指令自动清除插件
import idaapi
import idc
import idautils
class JunkCodeCleaner ( idaapi . plugin_t ):
flags = idaapi . PLUGIN_UNL
comment = "Automatic Junk Code Cleaner"
help = "Clean junk code patterns"
wanted_name = "Junk Cleaner"
wanted_hotkey = "Ctrl-Shift-J"
def init ( self ):
return idaapi . PLUGIN_OK
def run ( self , arg ):
func_ea = idc . get_screen_ea ()
func = idaapi . get_func ( func_ea )
if not func :
print ( "[!] Not inside a function" )
return
self . clean_function ( func . start_ea )
def clean_function ( self , func_ea ):
"""清理函数中的花指令"""
cleaned_count = 0
# 1. 清理 NOP 滑板
cleaned_count += self . remove_nop_sleds ( func_ea )
# 2. 清理不透明谓词
cleaned_count += self . remove_opaque_predicates ( func_ea )
# 3. 清理死代码
cleaned_count += self . remove_dead_code ( func_ea )
# 4. 重新分析函数
idaapi . reanalyze_callers ( func_ea , True )
print ( f "[*] Cleaned { cleaned_count } junk patterns in function 0x { func_ea : x } " )
def remove_nop_sleds ( self , func_ea ):
"""移除 NOP 滑板"""
removed = 0
nop_start = None
nop_count = 0
for head in idautils . FuncItems ( func_ea ):
mnem = idc . print_insn_mnem ( head ) . lower ()
if mnem == "nop" :
if nop_start is None :
nop_start = head
nop_count += 1
else :
if nop_count > 3 :
# 将多余的 NOP 转换为数据
for i in range ( nop_count - 1 ):
idc . del_items ( nop_start + i )
removed += nop_count - 1
nop_start = None
nop_count = 0
return removed
def remove_opaque_predicates ( self , func_ea ):
"""移除不透明谓词"""
removed = 0
for head in idautils . FuncItems ( func_ea ):
mnem = idc . print_insn_mnem ( head ) . lower ()
if mnem == "cmp" :
op1 = idc . print_operand ( head , 0 )
op2 = idc . print_operand ( head , 1 )
if op1 == op2 :
# cmp reg, reg 总是相等
next_insn = idc . next_head ( head )
next_mnem = idc . print_insn_mnem ( next_insn ) . lower ()
# 根据条件类型 patch
if next_mnem in [ "jne" , "jnz" , "b.ne" ]:
# 条件永假,删除跳转目标的代码块
target = idc . get_operand_value ( next_insn , 0 )
self . mark_as_junk ( target )
removed += 1
elif next_mnem in [ "je" , "jz" , "b.eq" ]:
# 条件永真,可以转换为无条件跳转
idc . patch_byte ( next_insn , 0xEB ) # jmp short
removed += 1
return removed
def remove_dead_code ( self , func_ea ):
"""移除死代码"""
func = idaapi . get_func ( func_ea )
if not func :
return 0
flowchart = idaapi . FlowChart ( func )
reachable = set ()
# BFS 找可达块
queue = [ flowchart [ 0 ] . start_ea ]
while queue :
block_ea = queue . pop ( 0 )
if block_ea in reachable :
continue
reachable . add ( block_ea )
for block in flowchart :
if block . start_ea == block_ea :
for succ in block . succs ():
queue . append ( succ . start_ea )
# 标记不可达块为数据
removed = 0
for block in flowchart :
if block . start_ea not in reachable :
for head in range ( block . start_ea , block . end_ea ):
idc . del_items ( head )
removed += 1
return removed
def mark_as_junk ( self , address ):
"""将地址标记为花指令"""
idc . set_cmt ( address , "JUNK CODE - Opaque predicate target" , 0 )
def term ( self ):
pass
def PLUGIN_ENTRY ():
return JunkCodeCleaner ()
3. OLLVM 混淆技术原理
3.1 OLLVM 简介
OLLVM (Obfuscator-LLVM) 是基于 LLVM 编译器框架的代码混淆工具,在编译时对 LLVM IR 进行变换,产生高度混淆的机器码。
主要混淆技术 :
控制流平坦化 (Control Flow Flattening, CFF) 指令替换 (Instruction Substitution) 虚假控制流 (Bogus Control Flow, BCF) 字符串加密 (String Encryption) 3.2 控制流平坦化 (CFF)
3.2.1 原理
将正常的控制流结构(if-else、switch、循环)转换为一个巨大的 switch-case 分发器:
原始代码 :
1
2
3
4
5
6
7
8
void original_func ( int x ) {
if ( x > 10 ) {
func_a ();
} else {
func_b ();
}
func_c ();
}
平坦化后 :
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
void flattened_func ( int x ) {
int state = 0 ; // 状态变量
while ( 1 ) {
switch ( state ) {
case 0 : // 入口
if ( x > 10 ) {
state = 1 ; // 去 func_a
} else {
state = 2 ; // 去 func_b
}
break ;
case 1 : // func_a 块
func_a ();
state = 3 ; // 去 func_c
break ;
case 2 : // func_b 块
func_b ();
state = 3 ; // 去 func_c
break ;
case 3 : // func_c 块
func_c ();
state = 4 ; // 退出
break ;
case 4 : // 出口
return ;
}
}
}
3.2.2 CFF 反汇编特征
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
; ARM64 控制流平坦化特征
_flattened_func:
; 初始化状态变量
mov w8 , #0 ; state = 0
.Lmain_loop:
; 主分发器 switch
cmp w8 , #4
b.hi .Ldefault
adrp x9 , .Ljump_table
add x9 , x9 , : lo12 :. Ljump_table
ldr x9 , [ x9 , x8 , lsl #3]
br x9
.Lcase_0:
; 入口块
cmp w0 , #10
csel w8 , w8 , w8 , gt ; 条件更新状态
b .Lmain_loop
.Lcase_1:
; func_a 块
bl func_a
mov w8 , #3
b .Lmain_loop
; ... 其他 case ...
.Ljump_table:
.quad .Lcase_0
.quad .Lcase_1
.quad .Lcase_2
.quad .Lcase_3
.quad .Lcase_4
3.3 虚假控制流 (BCF)
3.3.1 原理
插入永远不会执行的虚假基本块,增加控制流图复杂度:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void bcf_func ( int x ) {
// 不透明谓词(永真)
int opaque = ( x * x >= 0 ); // 平方数永远非负
if ( opaque ) {
real_logic (); // 真实代码
} else {
fake_logic_1 (); // 永远不执行
fake_logic_2 ();
}
// 另一个不透明谓词(永假)
if (( x * ( x + 1 )) % 2 != 0 ) { // 连续整数乘积永远是偶数
garbage_code (); // 永远不执行
}
}
3.3.2 BCF 识别特征
存在大量条件分支,但分支目标从未被执行 不透明谓词表达式(如 x*x >= 0) 虚假代码块通常包含随机生成的指令序列 3.4 指令替换
3.4.1 算术运算替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 原始:a + b
// 替换 1:a - (-b)
// 替换 2:(a ^ b) + 2 * (a & b)
// 替换 3:2 * (a | b) - (a ^ b)
// 原始:a - b
// 替换:a + (~b + 1)
// 替换:(a ^ b) - 2 * (~a & b)
// 原始:a ^ b
// 替换:(~a & b) | (a & ~b)
// 替换:(a | b) & (~a | ~b)
// 原始:a & b
// 替换:(~(~a | ~b))
// 替换:((a ^ b) ^ (a | b))
3.4.2 反汇编中的替换特征
1
2
3
4
5
6
; 原始: ADD X0, X1, X2 (x0 = x1 + x2)
; 替换后: 使用 XOR 和 AND 的组合
eor x8 , x1 , x2 ; x8 = x1 ^ x2
and x9 , x1 , x2 ; x9 = x1 & x2
lsl x9 , x9 , #1 ; x9 = (x1 & x2) << 1 = 2*(x1&x2)
add x0 , x8 , x9 ; x0 = ( x1 ^ x2 ) + 2 *( x1 & x2 ) = x1 + x2
3.5 字符串加密
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// OLLVM 字符串加密示例
// 原始字符串在编译时被加密,运行时解密
// 编译后的加密数据
static unsigned char encrypted_str [] = {
0x8a , 0x45 , 0x9c , 0x3f , 0x2d , 0x77 , 0x4e , 0x1b
};
// 解密函数(通常内联或混淆)
char * decrypt_string () {
static char decrypted [ 64 ];
unsigned char key = 0xAA ;
for ( int i = 0 ; i < sizeof ( encrypted_str ); i ++ ) {
decrypted [ i ] = encrypted_str [ i ] ^ key ;
key = rotate_left ( key , 3 ); // 密钥轮换
}
return decrypted ;
}
4. OLLVM 去混淆实战
💡 思路一句话 : OLLVM 去混淆的核心是「分层剥离」— 先用 IDAPython 清理指令级花指令(NOP 垃圾指令)→ 再用 Z3 消除不透明谓词 → 最后用符号执行恢复平坦化的控制流 → 逐层还原,每层都有对应的自动化工具。
4.1 控制流平坦化去混淆
4.1.1 基于符号执行的去平坦化
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
# 使用 angr 进行符号执行去平坦化
import angr
import claripy
from collections import defaultdict
class CFGDeflattener :
def __init__ ( self , binary_path , func_addr ):
self . proj = angr . Project ( binary_path , auto_load_libs = False )
self . func_addr = func_addr
self . state_var = None
self . real_cfg = defaultdict ( list )
def identify_dispatcher ( self ):
"""识别主分发器"""
cfg = self . proj . analyses . CFGFast ()
func = cfg . functions . get ( self . func_addr )
if not func :
print ( f "Function not found at 0x { self . func_addr : x } " )
return None
# 寻找入度最高的基本块(可能是分发器)
block_in_degree = defaultdict ( int )
for block in func . blocks :
for succ in block . vex . constant_jump_targets :
block_in_degree [ succ ] += 1
dispatcher = max ( block_in_degree , key = block_in_degree . get )
print ( f "[*] Identified dispatcher at 0x { dispatcher : x } " )
return dispatcher
def symbolic_execution ( self , start_addr , max_steps = 1000 ):
"""符号执行追踪状态转移"""
state = self . proj . factory . blank_state ( addr = start_addr )
# 创建符号状态变量
state_sym = claripy . BVS ( 'state' , 32 )
transitions = []
visited = set ()
simgr = self . proj . factory . simulation_manager ( state )
step_count = 0
while simgr . active and step_count < max_steps :
simgr . step ()
step_count += 1
for s in simgr . active :
current_block = s . addr
if current_block in visited :
continue
visited . add ( current_block )
# 记录状态转移
if hasattr ( s , 'history' ) and len ( s . history . bbl_addrs ) > 1 :
prev_block = s . history . bbl_addrs [ - 2 ]
transitions . append (( prev_block , current_block ))
return transitions
def recover_cfg ( self ):
"""恢复原始控制流图"""
dispatcher = self . identify_dispatcher ()
if not dispatcher :
return None
# 符号执行获取转移关系
transitions = self . symbolic_execution ( self . func_addr )
# 过滤掉通过分发器的边
real_transitions = []
for src , dst in transitions :
if src != dispatcher and dst != dispatcher :
real_transitions . append (( src , dst ))
print ( " \n [*] Recovered CFG edges:" )
for src , dst in real_transitions :
print ( f " 0x { src : x } -> 0x { dst : x } " )
return real_transitions
def generate_patched_binary ( self , output_path ):
"""生成修复后的二进制"""
real_cfg = self . recover_cfg ()
# 这里需要实现实际的 patch 逻辑
# 1. 移除分发器块
# 2. 将状态赋值替换为直接跳转
# 3. 重新链接基本块
print ( f "[*] Patched binary saved to { output_path } " )
# 使用示例
# deflattener = CFGDeflattener("target.so", 0x1234)
# deflattener.generate_patched_binary("target_deflattened.so")
4.1.2 IDA Pro Microcode 去平坦化
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
# IDA Pro Microcode API 去平坦化
import ida_hexrays
import ida_funcs
import idautils
class MicrocodeDeflattener ( ida_hexrays . minsn_visitor_t ):
def __init__ ( self ):
ida_hexrays . minsn_visitor_t . __init__ ( self )
self . state_var = None
self . state_assignments = {} # {block_addr: next_state}
self . dispatcher_addr = None
def visit_minsn ( self ):
"""访问每条微码指令"""
insn = self . curins
# 查找状态变量赋值: mov state_var, imm
if insn . opcode == ida_hexrays . m_mov :
if insn . d . t == ida_hexrays . mop_r : # 目标是寄存器
if insn . l . t == ida_hexrays . mop_n : # 源是立即数
state_value = insn . l . nnn . value
block_addr = self . blk . head
self . state_assignments [ block_addr ] = state_value
return 0
def deflattening_pass ( cfunc ):
"""去平坦化优化 Pass"""
mba = cfunc . mba
# 1. 识别状态变量和分发器
visitor = MicrocodeDeflattener ()
for i in range ( mba . qty ):
blk = mba . get_mblock ( i )
visitor . blk = blk
blk . for_all_insns ( visitor )
print ( f "[*] Found { len ( visitor . state_assignments ) } state assignments" )
# 2. 构建真实的控制流
real_cfg = {}
for block_addr , next_state in visitor . state_assignments . items ():
# 找到 next_state 对应的目标块
for i in range ( mba . qty ):
blk = mba . get_mblock ( i )
# 检查块的入口状态
if matches_state ( blk , next_state ):
real_cfg [ block_addr ] = blk . start
break
# 3. Patch 控制流
for src_addr , dst_addr in real_cfg . items ():
patch_jump ( src_addr , dst_addr )
return cfunc
def matches_state ( blk , state_value ):
"""检查基本块是否对应某个状态值"""
# 分析块的条件判断
tail = blk . tail
if tail and tail . opcode == ida_hexrays . m_jcnd :
# 检查条件是否比较状态变量
pass
return False
def patch_jump ( src_addr , dst_addr ):
"""将条件跳转替换为无条件跳转"""
# 实现 patch 逻辑
pass
# 注册优化 Pass
# ida_hexrays.install_microcode_filter(deflattening_pass)
4.2 虚假控制流去除
4.2.1 基于数据流分析
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
# 不透明谓词检测与消除
import ida_hexrays
import idc
class OpaquPredicateDetector :
# 已知的不透明谓词模式
OPAQUE_PATTERNS = [
# (模式描述, 检测函数, 结果)
( "x * x >= 0" , lambda x : True , True ), # 平方数非负
( "(x & 1) ^ 1 == 0 || (x & 1) == 0" , lambda x : True , True ), # 永真
( "x * (x + 1) % 2 != 0" , lambda x : False , False ), # 连续整数乘积是偶数
( "(x | x) == x" , lambda x : True , True ), # 自身或
( "(x & x) == x" , lambda x : True , True ), # 自身与
]
def __init__ ( self , cfunc ):
self . cfunc = cfunc
self . opaque_conditions = []
def analyze ( self ):
"""分析所有条件表达式"""
class ConditionVisitor ( ida_hexrays . ctree_visitor_t ):
def __init__ ( self , detector ):
ida_hexrays . ctree_visitor_t . __init__ ( self ,
ida_hexrays . CV_FAST )
self . detector = detector
def visit_expr ( self , expr ):
# 检查 if 条件
if expr . op == ida_hexrays . cot_if :
cond = expr . x
if self . detector . is_opaque ( cond ):
self . detector . opaque_conditions . append ( expr . ea )
return 0
visitor = ConditionVisitor ( self )
visitor . apply_to ( self . cfunc . body , None )
return self . opaque_conditions
def is_opaque ( self , cond_expr ):
"""检查条件是否是不透明谓词"""
# 模式 1: x * x >= 0
if self . matches_square_pattern ( cond_expr ):
return True
# 模式 2: x == x
if self . matches_self_comparison ( cond_expr ):
return True
# 模式 3: 常量折叠可判断
if self . is_constant_foldable ( cond_expr ):
return True
return False
def matches_square_pattern ( self , expr ):
"""检测 x * x >= 0 模式"""
if expr . op == ida_hexrays . cot_sge : # >=
left = expr . x
right = expr . y
# 右侧是 0
if right . op == ida_hexrays . cot_num and right . n . _value == 0 :
# 左侧是乘法
if left . op == ida_hexrays . cot_mul :
# 乘法两侧相同
if self . exprs_equal ( left . x , left . y ):
return True
return False
def matches_self_comparison ( self , expr ):
"""检测 x == x 模式"""
if expr . op in [ ida_hexrays . cot_eq , ida_hexrays . cot_le ,
ida_hexrays . cot_ge ]:
if self . exprs_equal ( expr . x , expr . y ):
return True
return False
def exprs_equal ( self , e1 , e2 ):
"""检查两个表达式是否相等"""
if e1 . op != e2 . op :
return False
if e1 . op == ida_hexrays . cot_var :
return e1 . v . idx == e2 . v . idx
# 递归检查子表达式
return False
def is_constant_foldable ( self , expr ):
"""检查表达式是否可以常量折叠"""
# 尝试符号求值
try :
result = self . symbolic_eval ( expr )
return result is not None and isinstance ( result , bool )
except :
return False
def remove_opaque_predicates ( self ):
"""移除检测到的不透明谓词"""
for addr in self . opaque_conditions :
print ( f "[*] Removing opaque predicate at 0x { addr : x } " )
# 根据谓词的恒定结果,简化控制流
# 永真 -> 删除 else 分支
# 永假 -> 删除 if 分支
# 使用示例
# cfunc = ida_hexrays.decompile(idc.here())
# detector = OpaquPredicateDetector(cfunc)
# opaques = detector.analyze()
# detector.remove_opaque_predicates()
4.3 指令替换还原
4.3.1 代数化简
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
# 基于 Z3 的表达式化简
from z3 import *
class ExpressionSimplifier :
def __init__ ( self ):
self . solver = Solver ()
def simplify_add ( self , expr ):
"""化简加法替换模式"""
# 模式: (a ^ b) + 2 * (a & b) -> a + b
a , b = BitVecs ( 'a b' , 32 )
# 原始表达式
obfuscated = ( a ^ b ) + 2 * ( a & b )
# 简化形式
simplified = a + b
# 验证等价性
self . solver . add ( obfuscated != simplified )
if self . solver . check () == unsat :
print ( "[*] Equivalence verified: (a^b) + 2*(a&b) == a+b" )
return True
return False
def simplify_xor ( self , expr ):
"""化简 XOR 替换模式"""
a , b = BitVecs ( 'a b' , 32 )
# 模式: (~a & b) | (a & ~b) -> a ^ b
obfuscated = ( ~ a & b ) | ( a & ~ b )
simplified = a ^ b
self . solver . reset ()
self . solver . add ( obfuscated != simplified )
if self . solver . check () == unsat :
print ( "[*] Equivalence verified: (~a&b)|(a&~b) == a^b" )
return True
return False
def pattern_match_and_simplify ( self , mba ):
"""在 microcode 中匹配和简化混淆模式"""
simplified_count = 0
for i in range ( mba . qty ):
blk = mba . get_mblock ( i )
insn = blk . head
while insn :
# 检测 XOR + AND + LSL + ADD 序列 (加法替换)
if self . is_add_obfuscation ( insn ):
self . simplify_to_add ( insn )
simplified_count += 1
insn = insn . next
return simplified_count
# 模式替换规则
SUBSTITUTION_RULES = [
# (混淆模式, 简化结果)
( "(a ^ b) + 2 * (a & b)" , "a + b" ),
( "(a ^ b) + ((a & b) << 1)" , "a + b" ),
( "a - (~b + 1)" , "a + b" ),
( "(~a & b) | (a & ~b)" , "a ^ b" ),
( "(a | b) & (~a | ~b)" , "a ^ b" ),
( "~(~a | ~b)" , "a & b" ),
( "~(~a & ~b)" , "a | b" ),
]
4.4 字符串解密
4.4.1 Frida 动态提取
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
// Frida 脚本:Hook OLLVM 字符串解密函数
function hook_string_decryption ( module_name ) {
var module = Process . findModuleByName ( module_name );
if ( ! module ) {
console . log ( "Module not found" );
return ;
}
// 搜索解密函数特征
// OLLVM 字符串解密通常有以下特征:
// 1. 循环结构
// 2. XOR 或其他位操作
// 3. 访问全局数据段
var decrypted_strings = [];
// Hook 常见的字符串使用函数
var strcmp = Module . findExportByName ( "libc.so" , "strcmp" );
var strstr = Module . findExportByName ( "libc.so" , "strstr" );
var strlen = Module . findExportByName ( "libc.so" , "strlen" );
if ( strcmp ) {
Interceptor . attach ( strcmp , {
onEnter : function ( args ) {
var s1 = args [ 0 ]. readCString ();
var s2 = args [ 1 ]. readCString ();
if ( s1 && ! decrypted_strings . includes ( s1 )) {
decrypted_strings . push ( s1 );
console . log ( "[strcmp] s1: " + s1 );
}
if ( s2 && ! decrypted_strings . includes ( s2 )) {
decrypted_strings . push ( s2 );
console . log ( "[strcmp] s2: " + s2 );
}
}
});
}
// Hook 目标模块中可能的解密函数
Memory . scan ( module . base , module . size ,
"?? ?? ?? ?? 4A ?? ?? ?? 91" , // ARM64 XOR 模式
{
onMatch : function ( address , size ) {
console . log ( "[*] Potential decrypt at: " + address );
// 尝试 Hook 这个位置
try {
Interceptor . attach ( address , {
onLeave : function ( retval ) {
if ( ! retval . isNull ()) {
try {
var str = retval . readCString ();
if ( str && str . length > 0 && str . length < 256 ) {
console . log ( "[DECRYPT] " + str );
decrypted_strings . push ( str );
}
} catch ( e ) {}
}
}
});
} catch ( e ) {
console . log ( "Hook failed: " + e );
}
},
onComplete : function () {
console . log ( "[*] Scan complete" );
}
}
);
return decrypted_strings ;
}
// 主动触发字符串解密
function trigger_decryption ( module_name ) {
var module = Process . findModuleByName ( module_name );
// 遍历模块导出函数,调用可能触发解密的函数
var exports = module . enumerateExports ();
exports . forEach ( function ( exp ) {
if ( exp . type === 'function' && exp . name . includes ( 'init' )) {
console . log ( "[*] Calling: " + exp . name );
try {
var func = new NativeFunction ( exp . address , 'void' , []);
func ();
} catch ( e ) {
console . log ( "Call failed: " + e );
}
}
});
}
// 导出解密字符串到文件
function export_strings ( strings , output_file ) {
var file = new File ( output_file , 'w' );
strings . forEach ( function ( s ) {
file . write ( s + '\n' );
});
file . close ();
console . log ( "[*] Exported " + strings . length + " strings to " + output_file );
}
4.4.2 静态解密脚本
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
# IDA Python 静态字符串解密
import idc
import idautils
import idaapi
class OLLVMStringDecryptor :
def __init__ ( self ):
self . encrypted_strings = []
self . decrypt_func_addr = None
def find_encrypted_data ( self ):
"""查找加密字符串数据段"""
# 搜索 .rodata 或 .data 段中的加密数据
for seg in idautils . Segments ():
seg_name = idc . get_segm_name ( seg )
if seg_name in [ '.rodata' , '.data' , '__const' ]:
self . scan_segment_for_encrypted ( seg )
def scan_segment_for_encrypted ( self , seg_start ):
"""扫描段中的加密数据"""
seg_end = idc . get_segm_end ( seg_start )
addr = seg_start
while addr < seg_end :
# 查找被引用的数据
refs = list ( idautils . DataRefsTo ( addr ))
if refs :
# 检查是否像加密字符串
data = idc . get_bytes ( addr , 32 )
if self . looks_encrypted ( data ):
self . encrypted_strings . append ({
'address' : addr ,
'data' : data ,
'refs' : refs
})
addr = idc . next_head ( addr )
def looks_encrypted ( self , data ):
"""启发式判断数据是否是加密字符串"""
if not data :
return False
# 检查熵值
entropy = self . calculate_entropy ( data )
if entropy > 6.0 : # 高熵值可能是加密数据
return True
# 检查是否有可打印字符
printable_ratio = sum ( 1 for b in data if 32 <= b <= 126 ) / len ( data )
if printable_ratio < 0.3 : # 可打印字符少,可能是加密的
return True
return False
def calculate_entropy ( self , data ):
"""计算数据熵"""
from collections import Counter
import math
if not data :
return 0
counter = Counter ( data )
length = len ( data )
entropy = 0
for count in counter . values ():
if count > 0 :
p = count / length
entropy -= p * math . log2 ( p )
return entropy
def find_decrypt_function ( self ):
"""查找解密函数"""
# 特征1: 被多次调用
# 特征2: 参数是加密数据地址
# 特征3: 返回值是字符串指针
for func_ea in idautils . Functions ():
func = idaapi . get_func ( func_ea )
if not func :
continue
# 检查函数特征
xrefs = list ( idautils . CodeRefsTo ( func_ea , True ))
if len ( xrefs ) < 3 :
continue
# 检查函数是否访问加密数据
if self . accesses_encrypted_data ( func_ea ):
print ( f "[*] Potential decrypt function at 0x { func_ea : x } " )
self . decrypt_func_addr = func_ea
return func_ea
return None
def accesses_encrypted_data ( self , func_ea ):
"""检查函数是否访问加密数据"""
for item in self . encrypted_strings :
for ref in item [ 'refs' ]:
# 检查引用是否在函数内
func = idaapi . get_func ( ref )
if func and func . start_ea == func_ea :
return True
return False
def emulate_decrypt ( self , encrypted_addr , key = 0xAA ):
"""模拟解密"""
data = idc . get_bytes ( encrypted_addr , 64 )
if not data :
return None
decrypted = []
for i , b in enumerate ( data ):
dec_byte = b ^ key
if dec_byte == 0 :
break
decrypted . append ( chr ( dec_byte ))
return '' . join ( decrypted )
def decrypt_all ( self ):
"""解密所有找到的加密字符串"""
results = []
for item in self . encrypted_strings :
decrypted = self . emulate_decrypt ( item [ 'address' ])
if decrypted :
results . append ({
'address' : item [ 'address' ],
'decrypted' : decrypted
})
# 添加注释
idc . set_cmt ( item [ 'address' ], f "Decrypted: { decrypted } " , 0 )
return results
# 使用示例
# decryptor = OLLVMStringDecryptor()
# decryptor.find_encrypted_data()
# decryptor.find_decrypt_function()
# results = decryptor.decrypt_all()
# for r in results:
# print(f"0x{r['address']:x}: {r['decrypted']}")
5. 高级去混淆工具与技术
5.1 开源去混淆工具
5.1.1 D-810 (IDA Pro 插件)
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
# D-810 是一个强大的 OLLVM 去混淆 IDA 插件
# 安装: 将 d810 目录放入 IDA plugins 目录
# D-810 主要功能:
# 1. 控制流平坦化去除
# 2. 虚假控制流去除
# 3. 指令替换还原
# 4. MBA (Mixed Boolean-Arithmetic) 表达式化简
# 使用方法:
# 1. 在 IDA Pro 中加载目标二进制
# 2. Edit -> Plugins -> D-810
# 3. 选择要处理的函数
# 4. 配置去混淆选项
# 5. 运行分析
# D-810 配置示例 (d810_config.json)
d810_config = {
"optimization_passes" : [
"ConstantFolding" ,
"DeadCodeElimination" ,
"ControlFlowUnflattening" ,
"BogusControlFlowRemoval" ,
"InstructionSubstitution" ,
"MBASimplification"
],
"max_iterations" : 10 ,
"debug_output" : True
}
5.1.2 SATURN (符号执行去混淆)
1
2
3
4
5
6
7
8
# SATURN 使用示例
# 安装
git clone https://github.com/pcy190/saturn
cd saturn
pip install -r requirements.txt
# 运行去混淆
python saturn.py --binary target.so --function 0x1234 --output deflattened.so
5.1.3 Miasm (二进制分析框架)
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
# 使用 Miasm 进行控制流分析和去混淆
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
def analyze_with_miasm ( binary_path , func_addr ):
# 加载二进制
loc_db = LocationDB ()
cont = Container . from_stream ( open ( binary_path , 'rb' ), loc_db )
# 获取机器架构
machine = Machine ( cont . arch )
dis_engine = machine . dis_engine ( cont . bin_stream , loc_db = loc_db )
# 反汇编函数
asmcfg = dis_engine . dis_multiblock ( func_addr )
# 构建控制流图
for block in asmcfg . blocks :
print ( f "Block at 0x { block . loc_key . offset : x } " )
for line in block . lines :
print ( f " { line } " )
return asmcfg
# 符号执行分析
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.expression.expression import ExprId
def symbolic_analyze ( asmcfg , machine ):
# 创建 IR 分析器
ira = machine . ira ( asmcfg . loc_db )
# 转换为 IR
ircfg = ira . new_ircfg_from_asmcfg ( asmcfg )
# 符号执行
sb = SymbolicExecutionEngine ( ira , {})
# 分析每个块的符号状态
for loc_key in ircfg . walk_depth_first_forward ( ircfg . heads ()[ 0 ]):
irblock = ircfg . get_block ( loc_key )
if irblock :
sb . eval_updt_irblock ( irblock )
print ( f "State after block 0x { loc_key . offset : x } :" )
for symbol , value in sb . symbols . items ():
print ( f " { symbol } = { value } " )
5.2 基于机器学习的去混淆
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
# 使用机器学习识别混淆模式
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from capstone import *
class MLObfuscationDetector :
def __init__ ( self ):
self . classifier = RandomForestClassifier ( n_estimators = 100 )
self . md = Cs ( CS_ARCH_ARM64 , CS_MODE_ARM )
self . md . detail = True
def extract_features ( self , func_binary ):
"""从函数二进制中提取特征"""
features = {
'instruction_count' : 0 ,
'unique_opcodes' : set (),
'branch_ratio' : 0 ,
'arithmetic_ratio' : 0 ,
'memory_ratio' : 0 ,
'nop_count' : 0 ,
'cfg_complexity' : 0 ,
'entropy' : 0
}
instructions = list ( self . md . disasm ( func_binary , 0 ))
features [ 'instruction_count' ] = len ( instructions )
branch_count = 0
arithmetic_count = 0
memory_count = 0
for insn in instructions :
features [ 'unique_opcodes' ] . add ( insn . mnemonic )
if insn . mnemonic . startswith ( 'b' ):
branch_count += 1
elif insn . mnemonic in [ 'add' , 'sub' , 'mul' , 'xor' , 'and' , 'orr' ]:
arithmetic_count += 1
elif insn . mnemonic in [ 'ldr' , 'str' , 'ldp' , 'stp' ]:
memory_count += 1
elif insn . mnemonic == 'nop' :
features [ 'nop_count' ] += 1
if features [ 'instruction_count' ] > 0 :
features [ 'branch_ratio' ] = branch_count / features [ 'instruction_count' ]
features [ 'arithmetic_ratio' ] = arithmetic_count / features [ 'instruction_count' ]
features [ 'memory_ratio' ] = memory_count / features [ 'instruction_count' ]
features [ 'unique_opcodes' ] = len ( features [ 'unique_opcodes' ])
features [ 'entropy' ] = self . calculate_entropy ( func_binary )
return self . features_to_vector ( features )
def features_to_vector ( self , features ):
"""将特征字典转换为向量"""
return np . array ([
features [ 'instruction_count' ],
features [ 'unique_opcodes' ],
features [ 'branch_ratio' ],
features [ 'arithmetic_ratio' ],
features [ 'memory_ratio' ],
features [ 'nop_count' ],
features [ 'entropy' ]
])
def calculate_entropy ( self , data ):
"""计算数据熵"""
from collections import Counter
import math
if not data :
return 0
counter = Counter ( data )
length = len ( data )
entropy = 0
for count in counter . values ():
if count > 0 :
p = count / length
entropy -= p * math . log2 ( p )
return entropy
def train ( self , samples , labels ):
"""训练分类器
samples: 函数二进制列表
labels: 0=未混淆, 1=花指令, 2=CFF, 3=BCF, 4=指令替换
"""
X = np . array ([ self . extract_features ( s ) for s in samples ])
self . classifier . fit ( X , labels )
def predict ( self , func_binary ):
"""预测混淆类型"""
features = self . extract_features ( func_binary )
prediction = self . classifier . predict ([ features ])[ 0 ]
labels = {
0 : "No obfuscation" ,
1 : "Junk code" ,
2 : "Control flow flattening" ,
3 : "Bogus control flow" ,
4 : "Instruction substitution"
}
return labels . get ( prediction , "Unknown" )
# 使用示例
# detector = MLObfuscationDetector()
# detector.train(training_samples, training_labels)
# result = detector.predict(target_function_binary)
6. 实战案例分析
💡 思路一句话 : 拿到加固 SO → IDA 打开判断混淆类型 → 先跑 d810 自动去混淆 → 不够的部分用 Frida Stalker 动态追踪补全 → 最终还原核心算法。从自动化工具开始,手动分析作为补充。
6.1 案例:某加固 SO 的去混淆分析
6.1.1 初步分析
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
# 1. 提取目标 SO 文件
# adb pull /data/app/com.target.app/lib/arm64-v8a/libnative.so
# 2. IDA Pro 加载分析
import idaapi
import idc
def initial_analysis ():
# 统计函数混淆情况
stats = {
'total_functions' : 0 ,
'suspected_cff' : 0 ,
'suspected_bcf' : 0 ,
'suspected_junk' : 0
}
for func_ea in idautils . Functions ():
stats [ 'total_functions' ] += 1
func = idaapi . get_func ( func_ea )
if not func :
continue
# 检查控制流平坦化特征
if has_dispatcher_pattern ( func_ea ):
stats [ 'suspected_cff' ] += 1
# 检查虚假控制流特征
if has_bcf_pattern ( func_ea ):
stats [ 'suspected_bcf' ] += 1
# 检查花指令特征
if has_junk_pattern ( func_ea ):
stats [ 'suspected_junk' ] += 1
print ( f "Analysis Results:" )
print ( f " Total functions: { stats [ 'total_functions' ] } " )
print ( f " Suspected CFF: { stats [ 'suspected_cff' ] } " )
print ( f " Suspected BCF: { stats [ 'suspected_bcf' ] } " )
print ( f " Suspected Junk: { stats [ 'suspected_junk' ] } " )
return stats
def has_dispatcher_pattern ( func_ea ):
"""检测控制流平坦化分发器模式"""
func = idaapi . get_func ( func_ea )
flowchart = idaapi . FlowChart ( func )
# 检查是否存在高入度的基本块(分发器)
in_degree = {}
for block in flowchart :
for pred in block . preds ():
in_degree [ block . start_ea ] = in_degree . get ( block . start_ea , 0 ) + 1
max_in_degree = max ( in_degree . values ()) if in_degree else 0
return max_in_degree > 5 # 入度大于5可能是分发器
def has_bcf_pattern ( func_ea ):
"""检测虚假控制流模式"""
# 检查是否有大量从未执行的基本块
# 这需要配合动态分析
return False
def has_junk_pattern ( func_ea ):
"""检测花指令模式"""
nop_count = 0
total_count = 0
for head in idautils . FuncItems ( func_ea ):
total_count += 1
mnem = idc . print_insn_mnem ( head ) . lower ()
if mnem == 'nop' :
nop_count += 1
return nop_count > 10 or ( total_count > 0 and nop_count / total_count > 0.1 )
6.1.2 控制流恢复
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
// Frida 脚本:动态恢复控制流
function recover_control_flow ( module_name , func_offset ) {
var module = Process . findModuleByName ( module_name );
var func_addr = module . base . add ( func_offset );
var state_transitions = [];
var current_state = null ;
Stalker . follow ( Process . getCurrentThreadId (), {
transform : function ( iterator ) {
var instruction = iterator . next ();
while ( instruction !== null ) {
var addr = instruction . address ;
// 在目标函数范围内
if ( addr . compare ( func_addr ) >= 0 &&
addr . compare ( func_addr . add ( 0x1000 )) < 0 ) {
// 记录基本块转移
iterator . putCallout ( function ( context ) {
var pc = context . pc ;
var block_addr = get_block_start ( pc );
if ( current_state !== null && current_state !== block_addr ) {
state_transitions . push ({
from : current_state ,
to : block_addr . toString ()
});
}
current_state = block_addr . toString ();
});
}
iterator . keep ();
instruction = iterator . next ();
}
}
});
// 调用目标函数多次以覆盖不同路径
var test_func = new NativeFunction ( func_addr , 'int' , [ 'int' ]);
for ( var i = 0 ; i < 100 ; i ++ ) {
try {
test_func ( i );
} catch ( e ) {}
}
Stalker . unfollow ();
// 分析并输出恢复的 CFG
console . log ( "\n[*] Recovered Control Flow:" );
var unique_transitions = [... new Set ( state_transitions . map ( JSON . stringify ))]. map ( JSON . parse );
unique_transitions . forEach ( function ( t ) {
console . log ( " " + t . from + " -> " + t . to );
});
// 生成 DOT 格式图
generate_dot_graph ( unique_transitions );
return unique_transitions ;
}
function get_block_start ( addr ) {
// 简化:返回对齐到 4 字节的地址
return ptr ( addr . and ( ptr ( 0xFFFFFFFFFFFFFFFC )));
}
function generate_dot_graph ( transitions ) {
var dot = "digraph CFG {\n" ;
dot += " node [shape=box];\n" ;
transitions . forEach ( function ( t ) {
dot += ' "' + t . from + '" -> "' + t . to + '";\n' ;
});
dot += "}\n" ;
console . log ( "\n[*] DOT Graph:" );
console . log ( dot );
// 保存到文件
var file = new File ( "/data/local/tmp/cfg.dot" , "w" );
file . write ( dot );
file . close ();
}
6.1.3 完整去混淆流程
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
# 完整的去混淆分析流程
class DeobfuscationPipeline :
def __init__ ( self , binary_path , output_path ):
self . binary_path = binary_path
self . output_path = output_path
self . results = {}
def run ( self ):
"""执行完整的去混淆流程"""
print ( "[*] Starting deobfuscation pipeline" )
# 1. 初步分析
print ( " \n [1/6] Initial analysis..." )
self . initial_analysis ()
# 2. 花指令去除
print ( " \n [2/6] Removing junk code..." )
self . remove_junk_code ()
# 3. 控制流平坦化去除
print ( " \n [3/6] Deflattening control flow..." )
self . deflatten_cff ()
# 4. 虚假控制流去除
print ( " \n [4/6] Removing bogus control flow..." )
self . remove_bcf ()
# 5. 指令替换还原
print ( " \n [5/6] Restoring instruction substitution..." )
self . restore_instructions ()
# 6. 字符串解密
print ( " \n [6/6] Decrypting strings..." )
self . decrypt_strings ()
# 生成报告
self . generate_report ()
def initial_analysis ( self ):
"""初步分析混淆类型"""
# 使用之前的分析代码
pass
def remove_junk_code ( self ):
"""去除花指令"""
# 使用 Unicorn 模拟执行识别并去除
pass
def deflatten_cff ( self ):
"""去除控制流平坦化"""
# 使用符号执行恢复原始 CFG
pass
def remove_bcf ( self ):
"""去除虚假控制流"""
# 识别并消除不透明谓词
pass
def restore_instructions ( self ):
"""还原指令替换"""
# 使用 Z3 进行代数化简
pass
def decrypt_strings ( self ):
"""解密加密字符串"""
# 使用 Frida 动态提取或静态分析
pass
def generate_report ( self ):
"""生成分析报告"""
report = """
# 去混淆分析报告
## 目标文件
{binary_path}
## 分析结果
### 1. 花指令
- 检测数量: {junk_count}
- 去除状态: {junk_status}
### 2. 控制流平坦化
- 受影响函数: {cff_count}
- 恢复状态: {cff_status}
### 3. 虚假控制流
- 检测数量: {bcf_count}
- 去除状态: {bcf_status}
### 4. 指令替换
- 检测数量: {sub_count}
- 还原状态: {sub_status}
### 5. 加密字符串
- 解密数量: {str_count}
## 输出文件
{output_path}
""" . format (
binary_path = self . binary_path ,
output_path = self . output_path ,
junk_count = self . results . get ( 'junk_count' , 0 ),
junk_status = self . results . get ( 'junk_status' , 'N/A' ),
cff_count = self . results . get ( 'cff_count' , 0 ),
cff_status = self . results . get ( 'cff_status' , 'N/A' ),
bcf_count = self . results . get ( 'bcf_count' , 0 ),
bcf_status = self . results . get ( 'bcf_status' , 'N/A' ),
sub_count = self . results . get ( 'sub_count' , 0 ),
sub_status = self . results . get ( 'sub_status' , 'N/A' ),
str_count = self . results . get ( 'str_count' , 0 )
)
print ( report )
with open ( 'deobfuscation_report.md' , 'w' ) as f :
f . write ( report )
# 使用示例
# pipeline = DeobfuscationPipeline("target.so", "target_clean.so")
# pipeline.run()
7. OLLVM 在 Android ARM64 上的特征模式
7.1 ARM64 控制流平坦化特征
ARM64 架构下控制流平坦化(FLA)的分发器有几种典型模式。最常见的是 CMP + B.EQ 链 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ARM64 FLA 典型分发器模式
// 特征: CMP + B.EQ 链或 TBZ/TBNZ 链
_dispatcher:
LDR W8, [SP, #0x20] // 加载状态变量
CMP W8, #0x1234 // 比较状态值
B.EQ block_A // 跳转到对应块
CMP W8, #0x5678
B.EQ block_B
CMP W8, #0x9ABC
B.EQ block_C
B _dispatcher // 默认回到分发器
block_A:
// ... 真实代码 ...
MOV W8, #0x5678 // 更新状态变量 → 下一个块是 block_B
STR W8, [SP, #0x20]
B _dispatcher // 返回分发器
除了最基本的 CMP 链之外,还有几种更复杂的分发变体:
Hash-based dispatch(Hikari 风格) :使用 CRC32 指令对状态变量进行哈希,再通过跳转表间接分发。这种方式使得状态值与实际跳转目标之间的关系更难静态分析。
1
2
3
4
5
6
7
// Hash - based dispatch variant ( Hikari style )
LDR W8 , [ SP , #state_var]
CRC32W W9 , WZR , W8 // Hash the state
AND W9 , W9 , #0xF // Mask to table size
ADR X10 , jump_table
LDR X10 , [ X10 , X9 , LSL #3] // Load target from table
BR X10 // Indirect branch
Table-based dispatch(跳转表分发) :利用 ARM64 的 ADR + LDR + BR 组合,通过一张跳转表实现分发。这种模式在 IDA 中通常表现为无法识别的 switch 语句。
1
2
3
4
5
6
7
8
9
// Table - based dispatch
LDR W8 , [ SP , #state_var]
SUB W8 , W8 , #base_state // 归一化状态值
CMP W8 , #table_size
B . HS _default // 越界检查
ADR X9 , jump_table
LDRSW X10 , [ X9 , X8 , UXTW #2] // 加载偏移量 (32位有符号扩展)
ADD X9 , X9 , X10 // 计算目标地址
BR X9 // 跳转
CSEL-based dispatch(条件选择分发) :对于只有两个后继块的情况,编译器可能使用 CSEL 指令代替分支,使控制流在汇编层面看起来是线性的。
1
2
3
4
5
6
7
8
// CSEL - based dispatch ( 两路分发 )
LDR W8 , [ SP , #state_var]
MOV W9 , #0x1234 // 状态值 A
MOV W10 , #0x5678 // 状态值 B
CMP W0 , #0 // 条件判断
CSEL W8 , W9 , W10 , EQ // 根据条件选择下一个状态
STR W8 , [ SP , #state_var]
B _dispatcher
7.2 ARM64 虚假控制流特征
虚假控制流(BCF)在 ARM64 上的核心特征是不透明谓词 + 永远不会执行的分支 。以下是典型模式:
1
2
3
4
5
6
7
8
9
10
// BCF 典型模式: 使用 CSEL + 不透明谓词
LDR W8, [X19] // 加载某个值
MUL W9, W8, W8 // W9 = x * x
AND W9, W9, #1 // W9 = x² mod 2
CBNZ W9, fake_branch // x² mod 2 永远为 0, 永远不跳
// real code continues here...
B continue
fake_branch:
// 垃圾代码
continue:
常见的 ARM64 不透明谓词模式包括:
MADD/MSUB 谓词 :利用乘法结果的数学恒等式构造永真/永假条件。
1
2
3
4
5
6
// MADD 不透明谓词 : x * ( x + 1 ) 永远是偶数
LDR W8 , [ SP , #var]
ADD W9 , W8 , #1
MADD W10 , W8 , W9 , WZR // W10 = x * ( x + 1 )
AND W10 , W10 , #1 // W10 = (x*(x+1)) mod 2
CBNZ W10 , fake_path // 永远不跳转
UBFX/SBFX 位域操作 :通过位域提取构造复杂的不透明表达式,增加分析难度。
1
2
3
4
5
6
// UBFX 不透明谓词
LDR W8 , [ SP , #var]
MUL W9 , W8 , W8 // x ²
UBFX W10 , W9 , #0, #1 // 提取最低位 (等价于 AND #1)
CBZ W10 , real_path // x ² 的最低位永远是 0 → 永真跳转
B fake_path // 永远不执行
CCMP 链(多条件标志) :使用 ARM64 特有的条件比较指令链来构造复杂的条件组合,使得不透明谓词在反编译器中呈现为多个条件的逻辑组合,大幅增加分析难度。
1
2
3
4
5
6
// CCMP 链不透明谓词
LDR W8 , [ SP , #var]
MUL W9 , W8 , W8 // W9 = x ²
CMP W9 , #0 // 比较 x² 与 0
CCMP W8 , W8 , #0, GE // 如果 x²>=0 (永真), 再比较 x==x
B . NE fake_path // x == x 永真 , NE 永假 → 永远不跳
7.3 ARM64 指令替换特征
指令替换(Instruction Substitution)将简单算术运算展开为等价的复杂位运算组合。以下是 ARM64 上常见的替换模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 原始: ADD X0, X1, X2
// SUB 替换方案 1: 双补码加法
NEG X3, X2 // X3 = -X2
SUB X0, X1, X3 // X0 = X1 - (-X2) = X1 + X2
// SUB 替换方案 2: MBA 展开
AND X3, X1, X2 // X3 = X1 & X2
EOR X4, X1, X2 // X4 = X1 ^ X2
ADD X0, X4, X3, LSL #1 // X0 = (X1^X2) + 2*(X1&X2) = X1+X2
// SUB 替换方案 3: 位运算展开 (更深层)
ORN X3, X1, X2 // X3 = X1 | ~X2
MVN X4, X1 // X4 = ~X1
AND X4, X4, X2 // X4 = ~X1 & X2
MVN X5, X3 // X5 = ~(X1 | ~X2) = ~X1 & X2 (same as X4)
// ... 多步运算最终等价于 ADD
在实际分析中,指令替换的深度可能达到 3-4 层嵌套,导致原本 1 条指令展开为 10-20 条。识别的关键在于:观察是否存在大量 EOR/AND/ORR/MVN/ORN 组合,且最终结果写入一个寄存器后被后续代码直接使用。
7.4 ARM64 MBA 表达式识别
Mixed Boolean-Arithmetic(MBA)表达式是 OLLVM 指令替换的核心技术,将算术运算和布尔运算混合,使得 Z3 等 SMT 求解器也难以直接化简。以下是实际 APP 中常见的 MBA 模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// MBA: a + b = (a ^ b) + 2 * (a & b)
EOR X3, X0, X1 // X3 = a ^ b
AND X4, X0, X1 // X4 = a & b
ADD X5, X3, X4, LSL #1 // X5 = (a^b) + 2*(a&b) = a + b
// MBA: a - b = (a ^ b) - 2 * (~a & b)
EOR X3, X0, X1 // X3 = a ^ b
BIC X4, X1, X0 // X4 = ~a & b (BIC = bit clear)
SUB X5, X3, X4, LSL #1 // X5 = (a^b) - 2*(~a&b) = a - b
// 嵌套 MBA (2层): 更复杂的等价替换
// a + b = ((a | b) + (a & b))
ORR X3, X0, X1 // X3 = a | b
AND X4, X0, X1 // X4 = a & b
ADD X5, X3, X4 // X5 = (a|b) + (a&b) = a + b
更深层次的嵌套 MBA 会将上述基本模式中的子表达式再次展开。例如 (a ^ b) 可能被展开为 (~a & b) | (a & ~b),而 (a & b) 可能被展开为 ~(~a | ~b),导致一个简单的加法最终变成十几条位运算指令。
MBA 表达式的自动识别方法 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用 Z3 自动识别和化简 ARM64 MBA 表达式
from z3 import *
def verify_mba_pattern ( expr_func , expected_func , bits = 64 ):
"""验证一个 MBA 表达式是否等价于某个简单运算"""
a , b = BitVecs ( 'a b' , bits )
s = Solver ()
s . add ( expr_func ( a , b ) != expected_func ( a , b ))
return s . check () == unsat # unsat = 等价
# 常见 MBA 模式验证
patterns = [
( "(a^b) + 2*(a&b)" , lambda a , b : ( a ^ b ) + 2 * ( a & b ), lambda a , b : a + b ),
( "(a^b) - 2*(~a&b)" , lambda a , b : ( a ^ b ) - 2 * ( ~ a & b ), lambda a , b : a - b ),
( "(a|b) + (a&b)" , lambda a , b : ( a | b ) + ( a & b ), lambda a , b : a + b ),
( "(a|b) - (a^b)" , lambda a , b : ( a | b ) - ( a ^ b ), lambda a , b : a & b ),
( "(a&b) | (a^b)" , lambda a , b : ( a & b ) | ( a ^ b ), lambda a , b : a | b ),
]
for name , expr , expected in patterns :
result = verify_mba_pattern ( expr , expected )
# print(f"{name} == a+b/a-b/...: {result}")
8. OLLVM 变体在 Android 生态中的分布
8.1 主流 OLLVM 变体识别指南
Android 生态中存在多种 OLLVM 变体,每种都有独特的特征。准确识别变体类型有助于选择最合适的去混淆策略。
原版 OLLVM 特征:
单层 while-switch 分发器 不透明谓词仅使用简单数学恒等式(如 x*x >= 0) 指令替换深度通常为 1-2 层 字符串不加密 状态变量使用单一整型变量 基本块之间通过直接赋值状态值进行转移 Hikari 特征:
嵌套分发器(2-3 层 switch),外层分发器选择函数区域,内层分发器选择具体基本块 FunctionWrapper:所有外部函数调用通过 trampoline 间接调用,增加交叉引用分析难度 字符串加密:.init_array 中有批量解密函数,通常在 SO 加载时自动执行 AntiClassDump:ObjC metadata 被打乱(iOS 特有,Android 端无此特征) IndirectBranch:使用 BLR/BR 替代 BL/B,所有分支变成间接跳转 状态变量可能使用 CRC32 哈希进行分发 Pluto-Obfuscator 特征:
TrapAngr pass:插入针对 angr 的反分析代码,使符号执行陷入死循环或路径爆炸 GlobalEncryption:全局变量在 constructor 中解密,.init_array 中包含全局变量解密函数 Flattening 使用更复杂的状态变量更新方式(可能使用位运算如 XOR、旋转) 间接跳转使用 ADRP+ADD+BR 模式,页面相对寻址增加重定位分析难度 MBA 表达式替换深度可达 3 层 商业方案特征:
梆梆加固 :通常 VMP + OLLVM 叠加,SO 文件的 .text 段部分或全部被 VMP 保护,剩余部分使用 OLLVM 混淆爱加密 :自定义 OLLVM 变体 + 字符串加密,特征是 .init_array 数量异常多且解密函数有固定签名360 加固 :多层壳 + 选择性 OLLVM,核心算法函数使用 OLLVM,外围函数不混淆网易易盾 :OLLVM + 环境检测,混淆强度较高,同时嵌入多种反调试和反 Hook 检测8.2 版本识别自动化脚本
以下脚本通过分析 SO 文件的结构特征,自动推断其使用的 OLLVM 变体:
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
import lief
def identify_ollvm_variant ( so_path ):
"""自动识别 SO 文件使用的 OLLVM 变体"""
binary = lief . parse ( so_path )
features = {
'has_init_array' : False ,
'init_array_count' : 0 ,
'indirect_branch_ratio' : 0 ,
'avg_basic_blocks_per_func' : 0 ,
'has_crc32_dispatch' : False ,
'has_nested_switch' : False ,
}
# 检查 .init_array
init_array = binary . get_section ( '.init_array' )
if init_array :
features [ 'has_init_array' ] = True
features [ 'init_array_count' ] = init_array . size // 8 # ARM64 pointer size
# 检查是否有大量间接跳转 (Hikari IndirectBranch 特征)
text_section = binary . get_section ( '.text' )
if text_section :
text_data = bytes ( text_section . content )
br_count = 0
bl_count = 0
total_insn = len ( text_data ) // 4 # ARM64 固定 4 字节指令
for i in range ( 0 , len ( text_data ) - 3 , 4 ):
insn = int . from_bytes ( text_data [ i : i + 4 ], 'little' )
# BR Xn: 1101011 0000 11111 000000 Rn 00000
if ( insn & 0xFFFFFC1F ) == 0xD61F0000 :
br_count += 1
# BL: 100101 imm26
if ( insn >> 26 ) == 0b100101 :
bl_count += 1
if total_insn > 0 :
features [ 'indirect_branch_ratio' ] = br_count / total_insn
# 判断变体
if features [ 'init_array_count' ] > 20 :
return "Hikari (大量 init_array 暗示字符串加密)"
elif features [ 'has_crc32_dispatch' ]:
return "Hikari (CRC32 哈希分发)"
elif features [ 'indirect_branch_ratio' ] > 0.05 :
return "Hikari (高间接跳转比例暗示 IndirectBranch pass)"
elif features [ 'init_array_count' ] > 5 :
return "Pluto/Armariris (中等 init_array)"
elif features [ 'has_init_array' ] and features [ 'init_array_count' ] <= 5 :
return "原版 OLLVM 或轻度混淆"
else :
return "未检测到明显 OLLVM 特征,可能是未混淆或使用 VMP"
# 使用
# result = identify_ollvm_variant("libnative.so")
# print(f"检测结果: {result}")
9. Android 版本差异对花指令/OLLVM 分析的影响
9.1 NDK 编译工具链变化
不同 NDK 版本使用的编译器和默认配置不同,直接影响 OLLVM 变体的兼容性和混淆效果:
NDK 版本 默认编译器 LLVM 版本 默认 ABI 对 OLLVM 的影响 r16 Clang 5.0 5.0 armeabi-v7a 与原版 OLLVM 4.0 兼容 r17-r20 Clang 8.0 8.0 armeabi-v7a 需要 OLLVM 适配到 LLVM 8 r21-r22 Clang 11 11 arm64-v8a Hikari/Pluto 主要适配版本 r23-r24 Clang 14 14 arm64-v8a LLD 默认链接器,影响 section 布局 r25-r26 Clang 17 17 arm64-v8a 需要最新 OLLVM 变体适配 r27+ Clang 18+ 18+ arm64-v8a 16KB 页面对齐支持
关键变化点:
NDK r21 起 arm64-v8a 成为默认 ABI ,逆向分析中遇到的 SO 文件绝大多数是 ARM64NDK r23 起 LLD 成为默认链接器 ,影响 ELF section 布局和重定位方式OLLVM 变体需要针对 LLVM 版本重新移植 ,高版本 LLVM IR 变化可能导致旧版 OLLVM pass 不兼容9.2 Execute-Only Memory (XOM) 影响 (Android 10+)
Android 10 引入了 Execute-Only Memory(XOM)特性,将 .text 段设置为仅可执行、不可读。这对逆向分析有重大影响:
XOM 的影响:
Memory.readByteArray() 直接读取代码段可能失败(权限被拒绝)内存 dump 工具无法直接读取代码内容 基于内存扫描的特征匹配(如搜索花指令模式)可能受阻 Frida Stalker 不受影响(使用 ptrace 机制绕过) 绕过方案:
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
// Frida: XOM 对 Stalker 无影响 (ptrace 绕过)
// 但 Memory.readByteArray 在某些设备上可能失败
// 安全的读取方式:
function safeReadCode ( addr , size ) {
try {
return Memory . readByteArray ( addr , size );
} catch ( e ) {
// Fallback: 使用 /proc/self/mem
var fd = new File ( "/proc/self/mem" , "r" );
fd . seek ( addr . toInt32 ());
var data = fd . readBytes ( size );
fd . close ();
return data ;
}
}
// 更稳健的方案: 通过 mprotect 修改权限
function readCodeWithMprotect ( addr , size ) {
var pageSize = 4096 ;
var pageAddr = addr . and ( ptr ( ~ ( pageSize - 1 ))); // 页对齐
var mprotect = new NativeFunction (
Module . findExportByName ( null , "mprotect" ),
'int' , [ 'pointer' , 'uint' , 'int' ]
);
// PROT_READ | PROT_EXEC = 0x5
var result = mprotect ( pageAddr , size + ( addr . sub ( pageAddr ). toInt32 ()), 0x5 );
if ( result === 0 ) {
return Memory . readByteArray ( addr , size );
}
return null ;
}
9.3 16KB 页面大小 (Android 14+)
Android 14 开始支持 16KB 页面大小(主要在 ARM64 设备上),这对 ELF 加载和二进制分析有以下影响:
ELF 段对齐变化:
ELF 的 PT_LOAD 段对齐从 4KB(0x1000)变为 16KB(0x4000) SO 文件体积可能因对齐填充而增大 文件偏移与内存偏移的映射关系发生变化 对逆向分析的影响:
IDA Pro / Ghidra 加载 SO 时需要确认页面大小设置 手动计算虚拟地址与文件偏移的映射时需使用 16KB 对齐 Frida 中 Module.base 仍然准确,但计算 section 偏移时需注意对齐差异 使用 inline hook / 二进制 patch 时,patch 点所在的页面边界可能不同 IDA Pro 加载配置调整:
在 Load File 对话框中确认 segment alignment 设置 对于 16KB 对齐的 SO,使用 idaapi.get_segm_by_sel() 验证段边界 9.4 SELinux 策略对动态分析的限制
Android 各版本持续收紧 SELinux 策略,对动态分析工具的影响逐步加大:
各版本关键限制:
Android 版本 SELinux 变化 对动态分析的影响 5.0-6.0 Enforcing 模式成为默认 ptrace 需要 allow 规则 7.0 neverallow 规则增加 部分自定义 policy 被拒 8.0 Treble 架构分离 vendor 域策略独立 9.0 限制 debugfs 部分内核调试功能失效 10.0 XOM + scoped storage 代码段不可读 + 文件访问受限 11.0 更严格的 exec 限制 /data/local/tmp 执行权限收紧12.0+ 限制 /proc 访问 /proc/pid/mem 读取受限14.0+ 16KB 页面 + 更多 neverallow 内存布局变化
对 Frida 的具体影响:
frida-server 需要以 root 权限运行并位于 SELinux 允许执行的路径注入目标进程时可能因 SELinux domain transition 规则被阻止 /proc/pid/maps 和 /proc/pid/mem 的访问可能受限Magisk 的 SELinux 绕过机制:
Magisk 通过 magiskpolicy 工具在启动时注入自定义 SELinux 规则 使用 magisk --denylist 管理隐藏列表,避免被目标 APP 检测 MagiskHide/Zygisk 可以在 fork 子进程时临时修改 SELinux context 对于 Frida 分析,通常需要配合 Magisk 的 SELinux 规则修改: 1
2
3
4
5
6
7
8
# 通过 Magisk 添加允许 Frida 运行的 SELinux 规则
magiskpolicy --live "allow su * process ptrace"
magiskpolicy --live "allow su * file { read write execute }"
# 检查当前 SELinux 状态
getenforce
# 临时设置为 Permissive (需要 root)
setenforce 0
注意 :在生产环境中,大量 APP 会检测 SELinux 状态。将 SELinux 设为 Permissive 可能触发 APP 的安全检测。建议使用 Magisk 的精细化规则控制而非全局关闭。
总结
花指令与 OLLVM 对比
特性 花指令 OLLVM 作用层级 汇编/机器码 LLVM IR 主要目标 干扰反汇编 混淆控制流和数据流 实现复杂度 低-中 高 分析难度 中 高 性能影响 小 中-大 去混淆方法 模式匹配、动态执行 符号执行、数据流分析
去混淆策略建议
分层处理 :先去花指令,再处理 OLLVM动静结合 :静态分析识别模式,动态执行验证工具组合 :IDA Pro + Frida + 符号执行引擎迭代优化 :多轮分析,逐步恢复原始逻辑自动化流程 :建立可复用的分析 Pipeline推荐工具清单
工具 用途 链接 D-810 IDA Pro OLLVM 去混淆插件 GitHub Miasm 二进制分析框架 GitHub angr 符号执行引擎 GitHub Triton 动态符号执行 GitHub Unicorn CPU 模拟器 GitHub Z3 SMT 求解器 GitHub Frida 动态插桩框架 frida.re
学习路径
理解编译原理和 LLVM IR 掌握逆向工程基础(IDA Pro、汇编语言) 学习符号执行和约束求解 实践各类去混淆工具 分析真实的混淆样本