# 实战去除安卓应用中的多种混淆

之前写过了混淆 pass 和去除案例,但多为 windows 上的一些样本,所以这篇文章写了三个安卓去混淆案例

# 案例 1

应用为小 * 书,版本是目前最新(936084)

目标去除函数是 sub_81CC4 函数,这是 sheid 生成算法中比较关键的一个函数,但被大量复杂混淆,无法分析其中逻辑。我们以它为例,演示如何去除这种混淆

在之前的文章中使用 idc 脚本自动断点的方式来处理间接跳转,安卓逆向中可以使用 firda 来代替。

先用 idc 脚本搜集想要 hook 的跳转地址和寄存器

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
#include <idc.idc>

static main()
{
auto seg;
for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg))
{
auto seg_name = get_segm_name(seg);
if (seg_name == ".text")
{
auto current_addr = seg;
auto end_addr = get_next_seg(seg);
auto lower_op;
while (current_addr != BADADDR && current_addr < end_addr)
{
auto insn_name = print_insn_mnem(current_addr);
auto op = print_operand(current_addr,0);
if(insn_name == "BR" && strstr(op,"X") != -1)
{
auto fp = fopen("indirect_jmp.txt", "a");
lower_op = tolower(op);
fprintf(fp, "{ off: 0x%X, reg : \"%s\"},\n", current_addr, lower_op);
fclose(fp);
}
current_addr = next_head(current_addr, end_addr);
}
}
}
msg("success");
}

再写 firda 脚本 hook 值即可

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
function hex(num)
{
if(typeof num === "number")
{
return "0x" + num.toString(16);
}
return ptr(num).toString();
}

function getContextRegister(context, reg) {
if (reg === "x30") {
return context.lr;
}
return context[reg];
}

function hook(module)
{
const base = module.base;
const start_addr = base.add(0x81d00);
const end_addr = base.add(0x84100);
const sites = [
{ off: 0x81d88, reg: "x30" },
{ off: 0x81d98, reg: "x27" },
{ off: 0x81ea4, reg: "x11" },
{ off: 0x81f64, reg: "x1" },
{ off: 0x81f74, reg: "x8" },
{ off: 0x82024, reg: "x11" },
{ off: 0x8207c, reg: "x1" },
{ off: 0x82118, reg: "x6" },
{ off: 0x821dc, reg: "x1" },
{ off: 0x822d4, reg: "x11" },
{ off: 0x82334, reg: "x6" },
{ off: 0x824b8, reg: "x8" },
{ off: 0x82778, reg: "x13" },
{ off: 0x8285c, reg: "x8" },
{ off: 0x8287c, reg: "x8" },
{ off: 0x82938, reg: "x1" },
{ off: 0x82958, reg: "x8" },
{ off: 0x82968, reg: "x8" },
{ off: 0x82a14, reg: "x1" },
{ off: 0x82b08, reg: "x11" },
{ off: 0x82b28, reg: "x8" },
{ off: 0x82be8, reg: "x1" },
{ off: 0x82c64, reg: "x1" },
{ off: 0x82cfc, reg: "x6" },
{ off: 0x82dfc, reg: "x1" },
{ off: 0x82e1c, reg: "x8" },
{ off: 0x82e2c, reg: "x8" },
{ off: 0x82ea4, reg: "x6" },
{ off: 0x82f60, reg: "x1" },
{ off: 0x82fe0, reg: "x8" },
{ off: 0x83098, reg: "x1" },
{ off: 0x8318c, reg: "x11" },
{ off: 0x831c4, reg: "x6" },
{ off: 0x831e4, reg: "x8" },
{ off: 0x831f4, reg: "x8" },
{ off: 0x83230, reg: "x6" },
{ off: 0x83338, reg: "x11" },
{ off: 0x833f0, reg: "x11" },
{ off: 0x83498, reg: "x6" },
{ off: 0x834d4, reg: "x6" },
{ off: 0x83594, reg: "x8" },
{ off: 0x835a8, reg: "x30" },
{ off: 0x835b8, reg: "x8" },
{ off: 0x835f8, reg: "x1" },
{ off: 0x83688, reg: "x6" },
{ off: 0x8378c, reg: "x1" },
{ off: 0x83880, reg: "x11" },
{ off: 0x83904, reg: "x6" },
{ off: 0x83a14, reg: "x11" },
{ off: 0x83a34, reg: "x8" },
{ off: 0x83a44, reg: "x8" },
{ off: 0x83b04, reg: "x1" },
{ off: 0x83b24, reg: "x8" },
{ off: 0x83bb0, reg: "x8" },
{ off: 0x83bf4, reg: "x1" },
{ off: 0x83c40, reg: "x1" },
{ off: 0x83c50, reg: "x8" },
{ off: 0x83d0c, reg: "x1" },
{ off: 0x83dfc, reg: "x11" },
{ off: 0x83ea4, reg: "x1" },
{ off: 0x83f58, reg: "x1" },
{ off: 0x84054, reg: "x11" }
];
for(const site of sites)
{
const addr = base.add(site.off);
Interceptor.attach(addr,{
onEnter(){
const target = getContextRegister(this.context,site.reg);
const target_offset = ptr(target).sub(base);
console.log(hex(site.off) + " -> " + hex(target_offset));
}
});
}
}


function main()
{
const timer = setInterval(function(){
const module = Process.findModuleByName("libxyass.so");
if(module != null)
{
clearInterval(timer);
hook(module);
}
},10);
}

setImmediate(main);

部分输出如下

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
1 0x81d88 -> 0x831e8
2 0x831f4 -> 0x831f8
3 0x83230 -> 0x831e8
4 0x831f4 -> 0x8349c
5 0x834d4 -> 0x831c8
6 0x831e4 -> 0x82e20
7 0x82e2c -> 0x82f64
8 0x82fe0 -> 0x81d8c
9 0x81d98 -> 0x82080
10 0x82118 -> 0x83a18
11 0x83a34 -> 0x835ac
12 0x835b8 -> 0x83884
13 0x83904 -> 0x82e20
14 0x82e2c -> 0x83190
15 0x831c4 -> 0x8295c
16 0x81d88 -> 0x831e8
17 0x831f4 -> 0x831f8
18 0x82968 -> 0x82c68
19 0x82cfc -> 0x83a18
20 0x83a34 -> 0x835ac
21 0x835b8 -> 0x835fc
22 0x83230 -> 0x831e8
23 0x831f4 -> 0x8349c
24 0x83688 -> 0x831c8
25 0x834d4 -> 0x831c8
26 0x831e4 -> 0x82e20
27 0x831e4 -> 0x82e20
28 0x82e2c -> 0x82e30
29 0x82e2c -> 0x82f64
30 0x81d88 -> 0x831e8
31 0x82ea4 -> 0x81f68
32 0x81f74 -> 0x822d8
33 0x82334 -> 0x83b08
34 0x831f4 -> 0x831f8
35 0x82fe0 -> 0x81d8c
36 0x81d98 -> 0x82080
37 0x83b24 -> 0x83a38
38 0x83230 -> 0x831e8
39 0x831f4 -> 0x8349c
40 0x834d4 -> 0x831c8
41 0x831e4 -> 0x82e20
42 0x83a44 -> 0x83b28
43 0x82118 -> 0x83a18
44 0x82e2c -> 0x82f64
45 0x82fe0 -> 0x81d8c
46 0x83a34 -> 0x835ac
47 0x835b8 -> 0x83884
48 0x81d98 -> 0x82080
49 0x82118 -> 0x83a18
50 0x83bb0 -> 0x831e8
51 0x831f4 -> 0x833f4
52 0x81d88 -> 0x831e8
53 0x831f4 -> 0x831f8
54 0x81d88 -> 0x831e8
55 0x831f4 -> 0x831f8
56 0x81d88 -> 0x831e8
57 0x831f4 -> 0x831f8
58 0x83904 -> 0x82e20
59 0x83230 -> 0x831e8
60 0x83a34 -> 0x835ac
61 0x83230 -> 0x831e8
62 0x83498 -> 0x8295c
63 0x82968 -> 0x8296c
64 0x82a14 -> 0x8295c
65 0x831f4 -> 0x8349c
66 0x834d4 -> 0x831c8
67 0x831f4 -> 0x8349c
68 0x83230 -> 0x831e8
69 0x82e2c -> 0x83190
70 0x831c4 -> 0x8295c
71 0x835b8 -> 0x83884
72 0x831e4 -> 0x82e20
73 0x834d4 -> 0x831c8
74 0x831f4 -> 0x8349c
75 0x834d4 -> 0x831c8
76 0x82968 -> 0x82c68
77 0x83904 -> 0x82e20
78 0x81d88 -> 0x831e8
79 0x831f4 -> 0x831f8
80 0x831e4 -> 0x82e20
81 0x82e2c -> 0x82f64
82 0x831e4 -> 0x82e20
83 0x82cfc -> 0x83a18
84 0x83a34 -> 0x835ac
85 0x82e2c -> 0x82f64
86 0x83230 -> 0x831e8
87 0x82968 -> 0x82bec
88 0x82fe0 -> 0x81d8c
89 0x81d98 -> 0x82080
90 0x82e2c -> 0x83190
91 0x831c4 -> 0x8295c
92 0x82968 -> 0x82c68
93 0x82cfc -> 0x83a18
94 0x83a34 -> 0x835ac
95 0x82fe0 -> 0x81d8c
96 0x81d88 -> 0x831e8
97 0x831f4 -> 0x831f8
98 0x83230 -> 0x831e8
99 0x831f4 -> 0x8349c
100 0x834d4 -> 0x831c8

这个表同时也是执行跳转的顺序,对接下来的恢复很有帮助。

得到跳转表之后,因为循环比较多,所以对跳转表进行一个去重,最后只剩下 100 条跳转地址

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
0x82c64 -> 0x81d8c
0x82e2c -> 0x83190
0x83d0c -> 0x8293c
0x8285c -> 0x82860
0x8207c -> 0x831e8
0x81d98 -> 0x81d9c
0x82e1c -> 0x8295c
0x83c50 -> 0x83d10
0x833f0 -> 0x8293c
0x81f74 -> 0x82880
0x81d98 -> 0x8211c
0x83880 -> 0x83a38
0x831f4 -> 0x834d8
0x83c50 -> 0x83c54
0x83a44 -> 0x83bb4
0x82e2c -> 0x82e30
0x82e2c -> 0x82ea8
0x83b04 -> 0x83b08
0x81d98 -> 0x821e0
0x81ea4 -> 0x8293c
0x82b08 -> 0x81f68
0x82968 -> 0x82d00
0x82dfc -> 0x81f68
0x82958 -> 0x81d8c
0x82334 -> 0x83b08
0x81d88 -> 0x831e8
0x83ea4 -> 0x83a18
0x82968 -> 0x82a18
0x835b8 -> 0x835fc
0x83338 -> 0x835ac
0x82e2c -> 0x82f64
0x83a44 -> 0x83f5c
0x8287c -> 0x83c44
0x835f8 -> 0x81f68
0x82968 -> 0x82c68
0x81f64 -> 0x81f68
0x83a44 -> 0x83bf8
0x81f74 -> 0x824bc
0x81d98 -> 0x82080
0x831f4 -> 0x8349c
0x82e2c -> 0x82fe4
0x83688 -> 0x831c8
0x822d4 -> 0x82e00
0x824b8 -> 0x831c8
0x83904 -> 0x82e20
0x83a14 -> 0x82860
0x83498 -> 0x8295c
0x82968 -> 0x82b2c
0x82fe0 -> 0x81d8c
0x835b8 -> 0x83790
0x83b24 -> 0x83a38
0x82968 -> 0x82bec
0x82118 -> 0x83a18
0x81f74 -> 0x8277c
0x831f4 -> 0x831f8
0x83a34 -> 0x835ac
0x835b8 -> 0x83884
0x82cfc -> 0x83a18
0x83a44 -> 0x83b28
0x831c4 -> 0x8295c
0x83dfc -> 0x831e8
0x81f74 -> 0x82338
0x835b8 -> 0x8368c
0x82778 -> 0x831e8
0x83a44 -> 0x83a48
0x83a44 -> 0x83ea8
0x81d98 -> 0x82028
0x81d98 -> 0x81ea8
0x82e2c -> 0x8309c
0x8318c -> 0x83a18
0x83c40 -> 0x83c44
0x82a14 -> 0x8295c
0x82be8 -> 0x83b08
0x83f58 -> 0x82e20
0x831f4 -> 0x833f4
0x8285c -> 0x831e8
0x83230 -> 0x831e8
0x82938 -> 0x83b08
0x83c50 -> 0x83e00
0x835b8 -> 0x83908
0x83bb0 -> 0x831e8
0x821dc -> 0x8295c
0x82b28 -> 0x81f68
0x83c50 -> 0x84058
0x83bf4 -> 0x835ac
0x835b8 -> 0x835bc
0x81f74 -> 0x822d8
0x82968 -> 0x8296c
0x83594 -> 0x83a38
0x83098 -> 0x8293c
0x82f60 -> 0x82860
0x831f4 -> 0x83234
0x82ea4 -> 0x81f68
0x831f4 -> 0x8333c
0x8378c -> 0x8295c
0x84054 -> 0x82b0c
0x834d4 -> 0x831c8
0x831e4 -> 0x82e20
0x81f74 -> 0x81f78
0x82024 -> 0x82e20

从第一个跳转地址开始看起,

1
0x81d88 -> 0x831e8

0x831e8 对应的跳转地址是 0x831F4

但是观察去重后的跳转表, 0x831F4 会跳到多个不同地址。这样无法找出唯一对应的地址。而这处跳转的计算和寄存器 x27 有关

返回 0x81d88 处观察 x27

x27 是定值,最终跳转的肯定也是一个唯一固定的地址。再观察顺序跳转表

1
2
3
4
5
1 0x81d88 -> 0x831e8
2 0x831f4 -> 0x831f8
......
16 0x81d88 -> 0x831e8
17 0x831f4 -> 0x831f8

由此可以得到最终跳转关系为:0x81d88 -> 0x831e8 -> 0x831f8

0x831e8 这个块只是一个分发块,没有实际作用,所以这其实是一种控制流平坦化 + 间接跳转混淆

接下来继续找类似这种结构的跳转关系

跳入分发块:

1
2
3
4
5
6
0x81d88 -> 0x831e8
0x83dfc -> 0x831e8
0x82778 -> 0x831e8
0x8285c -> 0x831e8
0x83230 -> 0x831e8
0x83bb0 -> 0x831e8

跳出分发块:

1
2
3
4
5
6
0x831f4 -> 0x834d8
0x831f4 -> 0x8349c
0x831f4 -> 0x831f8
0x831f4 -> 0x833f4
0x831f4 -> 0x83234
0x831f4 -> 0x8333c

实际执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x81d88 -> 0x831e8
0x831f4 -> 0x831f8

0x83dfc -> 0x831e8
0x831f4 -> 0x8333c

0x82778 -> 0x831e8
0x831f4 -> 0x83234

0x8285c -> 0x831e8
0x831f4 -> 0x833f4

0x83230 -> 0x831e8
0x831f4 -> 0x8349c

0x83bb0 -> 0x831e8
0x831f4 -> 0x833f4

这样就可以得到最终的执行顺序:

1
2
3
4
5
6
0x81d88 -> 0x831f8
0x83dfc -> 0x8333c
0x82778 -> 0x83234
0x8285c -> 0x833f4
0x83230 -> 0x8349c
0x83bb0 -> 0x833f4

写一个脚本批量重复这个过程即可得到真实跳转关系

不过在这个过程中我们可以发现神奇的一点

最终跳转的地址就是上面赋值用来计算的地址。这样的话都不需要根据 trace 表来分析, 直接根据特征写脚本 patch 就可以了(谜底就在谜面上这一块

搞定了这种 fla + idr 的 A -> 分发块 -> B 跳转关系,还有多目标块 A -> B -> C / A -> B -> D 需要修复,这种是因为条件跳转导致的

上面的两个函数是要跳转的目标,条件是 NE,但是我们很难确定哪个是满足条件后的跳转目标,这个时候可以再 trace 一遍。因为这个混淆中所有的条件跳转结尾都是

hook 打印这个地方的值

1
2
0x82fd8 w8=0x7e2fa2f8 w30=0x7e2fa2f8
0x82fe0 -> 0x81d8c

w8 和 w30 相同,说明不满足 NE

观察顺序跳转表

1
2
0x82fe0 -> 0x81d8c
0x81d98 -> 0x82080

不满足 NE 条件时最终会跳到 0x82080 这个函数

可知 B.NE 0x84058 B 0x82080

这样两种混淆就全部解决了。

去混淆后的部分代码如下

逻辑清晰,可识别出算法

# 案例 2

应用为 * 幸咖啡,版本是网上找的一个旧版

以 md5 算法的外层函数为示例。

观察 cfg 图,可以看出明显是控制流平坦化

去除控制流平坦化只有三步
1. 计算基本块下一跳转块的 case 值

2. 遍历分发块找到对应地址

3.patch

先来看一下基本块和分发块的特征

基本块

单后继

1
2
3
MOV             W9, #0x3E04
STUR X0, [X29,#var_20]
MOVK W9, #0xBE87,LSL#16

双后继

1
2
3
4
MOV             W9, #0x5C511C13
CMP X8, #0
MOV W8, #0x1B0C631C
CSEL W9, W8, W9, EQ

分发块

1
2
3
MOV             W9, #0x8ECCC728
CMP W8, W9
B.EQ loc_4411C

1
2
3
4
MOV             W9, #0xF8D34334
CMP W8, W9
MOV W9, #0xFF680FE2
B.EQ loc_43C34

如果用同一种 LLVM Pass 来混淆函数的话,汇编层面往往指令相同。

简单观察特征之后就可以发现,这个函数基本块单后继都是 mov + stur + movk 的这种结构,多后继块都是 cesl + b 的结构(多后继的典型特征就是 csel /cmov 汇编),所以根据特征不难定位后继块,可以写一个脚本提取出 case 值和跳转地址

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
#include <idc.idc>

static cmpInsn(current_addr,target_insn,target_op1,target_op2,target_op3)
{
auto res = 1;
if(target_insn != 0)
{
auto insn_name = print_insn_mnem(current_addr);
if(insn_name != target_insn)
{
res = 0;
}
}
if(target_op1 != 0)
{
auto op1 = print_operand(current_addr,0);
if(op1 != target_op1)
{
res = 0;
}
}
if(target_op2 != 0)
{
auto op2 = print_operand(current_addr,1);
if(op2 != target_op2)
{
res = 0;
}
}
if(target_op3 != 0)
{
auto op3 = print_operand(current_addr,2);
if(op3 != target_op3)
{
res = 0;
}
}
return res;
}

static calCaseVal(current_addr,end_addr)
{
auto case_low = get_operand_value(current_addr,1);
auto case_high = get_operand_value(next_head(next_head(current_addr,end_addr),end_addr),1);
auto caseVal = (case_high << 16) | case_low;
return caseVal;
}

static main()
{
auto current_addr = 0x43BBC;
auto end_addr = 0x443D4;
auto start_addr = current_addr;
while (current_addr != BADADDR && current_addr < end_addr)
{
auto current_res = cmpInsn(current_addr,"MOV","W9",0,0);
auto next_res = cmpInsn(next_head(current_addr,end_addr),"STUR",0,0,0);
auto last_addr = next_head(next_head(current_addr,end_addr),end_addr);
auto last_res = cmpInsn(last_addr,"MOVK","W9",0,0);
if(current_res && next_res && last_res)
{
auto caseVal = calCaseVal(current_addr,end_addr);
auto B_addr = next_head(last_addr,end_addr);
msg("{'basicBlockAddr' : 0x%X, 'nextCaseVal' : 0x%X},\n",B_addr,caseVal);
}

if(cmpInsn(current_addr,"MOV","W9",0,0) && cmpInsn(next_head(current_addr,end_addr),"B",0,0,0))
{
msg("{'basicBlockAddr' : 0x%X, 'nextCaseVal' : 0x%X},\n",next_head(current_addr,end_addr),get_operand_value(current_addr,1));
}

current_addr = next_head(current_addr, end_addr);
}
}

输出:

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
{'basicBlockAddr' : 0x43CC4, 'nextCaseVal' : 0x452944A5},
{'basicBlockAddr' : 0x43D3C, 'nextCaseVal' : 0x35F42CE9},
{'basicBlockAddr' : 0x43D9C, 'nextCaseVal' : 0xB22FA3D0},
{'basicBlockAddr' : 0x43DFC, 'nextCaseVal' : 0x78E716D8},
{'basicBlockAddr' : 0x43E4C, 'nextCaseVal' : 0x1C536006},
{'basicBlockAddr' : 0x43E9C, 'nextCaseVal' : 0x43F23AAF},
{'basicBlockAddr' : 0x43EF8, 'nextCaseVal' : 0x22DF66B8},
{'basicBlockAddr' : 0x43F34, 'nextCaseVal' : 0x3D42BA4E},
{'basicBlockAddr' : 0x43F64, 'nextCaseVal' : 0x93A75DE8},
{'basicBlockAddr' : 0x43FAC, 'nextCaseVal' : 0xB50E4CF3},
{'basicBlockAddr' : 0x43FDC, 'nextCaseVal' : 0x1C536006},
{'basicBlockAddr' : 0x44058, 'nextCaseVal' : 0xECEBC62F},
{'basicBlockAddr' : 0x440CC, 'nextCaseVal' : 0x78E716D8},
{'basicBlockAddr' : 0x44118, 'nextCaseVal' : 0xD721AE9D},
{'basicBlockAddr' : 0x44128, 'nextCaseVal' : 0x4CF7C786},
{'basicBlockAddr' : 0x44148, 'nextCaseVal' : 0xB22FA3D0},
{'basicBlockAddr' : 0x44158, 'nextCaseVal' : 0x93A75DE8},
{'basicBlockAddr' : 0x44164, 'nextCaseVal' : 0xBBBE05E4},
{'basicBlockAddr' : 0x44174, 'nextCaseVal' : 0x43F23AAF},
{'basicBlockAddr' : 0x44180, 'nextCaseVal' : 0x8ECCC728},
{'basicBlockAddr' : 0x441DC, 'nextCaseVal' : 0x43EDB2A0},
{'basicBlockAddr' : 0x441E8, 'nextCaseVal' : 0xB62C5792},
{'basicBlockAddr' : 0x441F4, 'nextCaseVal' : 0xCA9DBB1F},
{'basicBlockAddr' : 0x44298, 'nextCaseVal' : 0x4B62C933},
{'basicBlockAddr' : 0x442BC, 'nextCaseVal' : 0xBE873E04},
{'basicBlockAddr' : 0x442D8, 'nextCaseVal' : 0x2F804C20},
{'basicBlockAddr' : 0x44334, 'nextCaseVal' : 0xEFEBCA04},
{'basicBlockAddr' : 0x44350, 'nextCaseVal' : 0x97F01E89},

这样就提取出了函数中所有单后继基本块跳转地址和下一个基本块的 case 值

稍微修改一下得到多后继的

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
#include <idc.idc>

static cmpInsn(current_addr,target_insn,target_op1,target_op2,target_op3)
{
auto res = 1;
if(target_insn != 0)
{
auto insn_name = print_insn_mnem(current_addr);
if(insn_name != target_insn)
{
res = 0;
}
}
if(target_op1 != 0)
{
auto op1 = print_operand(current_addr,0);
if(op1 != target_op1)
{
res = 0;
}
}
if(target_op2 != 0)
{
auto op2 = print_operand(current_addr,1);
if(op2 != target_op2)
{
res = 0;
}
}
if(target_op3 != 0)
{
auto op3 = print_operand(current_addr,2);
if(op3 != target_op3)
{
res = 0;
}
}
return res;
}

static findCase(current_addr,reg,start_addr)
{
while(current_addr != BADADDR)
{
auto insn = print_insn_mnem(current_addr);
auto op = print_operand(current_addr,0);
if(insn == "MOV" && op == reg)
{
auto caseVal = get_operand_value(current_addr,1);
return caseVal;
}
current_addr = prev_head(current_addr,start_addr);
}
}

static main()
{
auto current_addr = 0x43BBC;
auto end_addr = 0x443D4;
auto start_addr = current_addr;
while (current_addr != BADADDR && current_addr < end_addr)
{
auto current_insn = print_insn_mnem(current_addr);
auto next_insn = print_insn_mnem(next_head(current_addr,end_addr));
if(current_insn == "CSEL" && next_insn == "B")
{
auto cond = print_operand(current_addr,3);
auto eq_reg = print_operand(current_addr,1);
auto other_reg = print_operand(current_addr,2);
auto eq_case = findCase(current_addr,eq_reg,start_addr);
auto other_case = findCase(current_addr,other_reg,start_addr);
msg("{'basicBlockAddr' : 0x%X, 'eqCaseVal' : 0x%X, 'otherCaseVal' : 0x%X, 'cond' : '%s'},\n",next_head(current_addr,end_addr),eq_case,other_case,cond);
}

current_addr = next_head(current_addr, end_addr);
}
}

输出:

1
2
{'basicBlockAddr' : 0x4409C, 'eqCaseVal' : 0x1B0C631C, 'otherCaseVal' : 0x5C511C13, 'cond' : 'EQ'},
{'basicBlockAddr' : 0x44384, 'eqCaseVal' : 0xEE5D4D0B, 'otherCaseVal' : 0xF8D34334, 'cond' : 'NE'},

分发块全部以 mov w9,imm + cmp w8,w9 这样的结构开头。不过有一种分发块需要注意

1
2
3
4
MOV             W9, #0xF8D34334
CMP W8, W9
MOV W9, #0xFF680FE2
B.EQ loc_43C34

如果 case 值和这里的 0xF8D34334 相等,那么最后会跳转到 loc_43C34 ,这是什么地方呢

是开始分发的地方。所以这里的逻辑其实是 basicBlock -> dispatchBlock -> dispatchBlock -> basicBlock,而不是经典的 basicBlock -> dispatchBlock -> basicBlock。多分发了一次。

写出 idc 脚本提取值

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
#include <idc.idc>

static findJmp(addr,end_addr)
{
while(addr < end_addr && addr != BADADDR)
{
addr = next_head(addr,end_addr);
auto insn_name = print_insn_mnem(addr);
if(strstr(insn_name,"B") != -1)
{
break;
}
}

return addr;
}


static main()
{
auto current_addr = 0x43BBC;
auto end_addr = 0x443D4;
while (current_addr != BADADDR && current_addr < end_addr)
{
auto insn_name = print_insn_mnem(current_addr);
auto op1 = print_operand(current_addr,0);
if(insn_name == "MOV" && op1 == "W9")
{
auto next_addr = next_head(current_addr,end_addr);
auto next_insn = print_insn_mnem(next_addr);
auto next_op1 = print_operand(next_addr,0);
auto next_op2 = print_operand(next_addr,1);
if(next_insn == "CMP" && next_op1 == "W8" && next_op2 == "W9")
{
auto real_addr = next_head(next_addr,end_addr);
auto real_insn = print_insn_mnem(real_addr);
auto real_op1 = print_operand(real_addr,0);
auto real_op2 = get_operand_value(real_addr,1);
auto real_case = 0;
if(real_insn == "MOV" && real_op1 == "W9")
{
real_case = real_op2;
}
auto B_addr = findJmp(next_addr,end_addr);
auto cond = print_insn_mnem(B_addr);
auto eq_jmp = print_operand(B_addr,0);
auto other_jmp = next_head(B_addr,end_addr);
msg("{'addr' : 0x%X, 'caseVal' : 0x%s, 'cond' : '%s', 'eq_jmp' : %s, 'other_jmp' : 0x%X, 'real_case' : 0x%X},\n", current_addr,toupper(ltoa(get_operand_value(current_addr, 1), 16)),cond,eq_jmp,other_jmp,real_case);
}
}
current_addr = next_head(current_addr, end_addr);
}
}

输出:

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
{'addr' : 0x43C38, 'caseVal' : 0xF8D34333, 'cond' : 'B.LE', 'eq_jmp' : loc_43CC8, 'other_jmp' : 0x43C48, 'real_case' : 0x0},
{'addr' : 0x43C48, 'caseVal' : 0x3D42BA4D, 'cond' : 'B.GT', 'eq_jmp' : loc_43D40, 'other_jmp' : 0x43C58, 'real_case' : 0x0},
{'addr' : 0x43C58, 'caseVal' : 0x22DF66B7, 'cond' : 'B.GT', 'eq_jmp' : loc_43E50, 'other_jmp' : 0x43C68, 'real_case' : 0x0},
{'addr' : 0x43C68, 'caseVal' : 0x17F41D4, 'cond' : 'B.LE', 'eq_jmp' : loc_43FE0, 'other_jmp' : 0x43C78, 'real_case' : 0x0},
{'addr' : 0x43C78, 'caseVal' : 0x17F41D5, 'cond' : 'B.EQ', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43C90, 'real_case' : 0xFF680FE2},
{'addr' : 0x43C90, 'caseVal' : 0x1B0C631C, 'cond' : 'B.EQ', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43CA8, 'real_case' : 0x35F42CE9},
{'addr' : 0x43CA8, 'caseVal' : 0x1C536006, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43CBC, 'real_case' : 0x89},
{'addr' : 0x43CC8, 'caseVal' : 0xC105D5A4, 'cond' : 'B.LE', 'eq_jmp' : loc_43DA0, 'other_jmp' : 0x43CD8, 'real_case' : 0x0},
{'addr' : 0x43CD8, 'caseVal' : 0xECEBC62E, 'cond' : 'B.GT', 'eq_jmp' : loc_43E00, 'other_jmp' : 0x43CE8, 'real_case' : 0x0},
{'addr' : 0x43CE8, 'caseVal' : 0xCA9DBB1E, 'cond' : 'B.LE', 'eq_jmp' : loc_43F68, 'other_jmp' : 0x43CF8, 'real_case' : 0x0},
{'addr' : 0x43CF8, 'caseVal' : 0xCA9DBB1F, 'cond' : 'B.EQ', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43D10, 'real_case' : 0xB50E4CF3},
{'addr' : 0x43D10, 'caseVal' : 0xD721AE9D, 'cond' : 'B.EQ', 'eq_jmp' : loc_442D0, 'other_jmp' : 0x43D20, 'real_case' : 0x0},
{'addr' : 0x43D20, 'caseVal' : 0xE4648FAD, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43D34, 'real_case' : 0x89},
{'addr' : 0x43D40, 'caseVal' : 0x4CF7C785, 'cond' : 'B.GT', 'eq_jmp' : loc_43EA0, 'other_jmp' : 0x43D50, 'real_case' : 0x0},
{'addr' : 0x43D50, 'caseVal' : 0x43F23AAE, 'cond' : 'B.LE', 'eq_jmp' : loc_44020, 'other_jmp' : 0x43D60, 'real_case' : 0x0},
{'addr' : 0x43D60, 'caseVal' : 0x43F23AAF, 'cond' : 'B.EQ', 'eq_jmp' : loc_441E0, 'other_jmp' : 0x43D70, 'real_case' : 0x0},
{'addr' : 0x43D70, 'caseVal' : 0x452944A5, 'cond' : 'B.EQ', 'eq_jmp' : loc_44338, 'other_jmp' : 0x43D80, 'real_case' : 0x0},
{'addr' : 0x43D80, 'caseVal' : 0x4B62C933, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43D94, 'real_case' : 0x89},
{'addr' : 0x43DA0, 'caseVal' : 0xB22FA3CF, 'cond' : 'B.GT', 'eq_jmp' : loc_43EFC, 'other_jmp' : 0x43DB0, 'real_case' : 0x0},
{'addr' : 0x43DB0, 'caseVal' : 0x93A75DE7, 'cond' : 'B.LE', 'eq_jmp' : loc_43F38, 'other_jmp' : 0x43DC0, 'real_case' : 0x0},
{'addr' : 0x43DC0, 'caseVal' : 0x93A75DE8, 'cond' : 'B.EQ', 'eq_jmp' : loc_44178, 'other_jmp' : 0x43DD0, 'real_case' : 0x0},
{'addr' : 0x43DD0, 'caseVal' : 0x97F01E89, 'cond' : 'B.EQ', 'eq_jmp' : loc_442C0, 'other_jmp' : 0x43DE0, 'real_case' : 0x0},
{'addr' : 0x43DE0, 'caseVal' : 0x9A4F670B, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43DF4, 'real_case' : 0x89},
{'addr' : 0x43E00, 'caseVal' : 0xEFEBCA03, 'cond' : 'B.LE', 'eq_jmp' : loc_43FB0, 'other_jmp' : 0x43E10, 'real_case' : 0x0},
{'addr' : 0x43E10, 'caseVal' : 0xEFEBCA04, 'cond' : 'B.EQ', 'eq_jmp' : loc_44184, 'other_jmp' : 0x43E20, 'real_case' : 0x0},
{'addr' : 0x43E20, 'caseVal' : 0xF1508DA1, 'cond' : 'B.EQ', 'eq_jmp' : loc_442DC, 'other_jmp' : 0x43E30, 'real_case' : 0x0},
{'addr' : 0x43E30, 'caseVal' : 0xF50BDBE9, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43E44, 'real_case' : 0x89},
{'addr' : 0x43E50, 'caseVal' : 0x34D451D4, 'cond' : 'B.LE', 'eq_jmp' : loc_4405C, 'other_jmp' : 0x43E60, 'real_case' : 0x0},
{'addr' : 0x43E60, 'caseVal' : 0x34D451D5, 'cond' : 'B.EQ', 'eq_jmp' : loc_441EC, 'other_jmp' : 0x43E70, 'real_case' : 0x0},
{'addr' : 0x43E70, 'caseVal' : 0x35F42CE9, 'cond' : 'B.EQ', 'eq_jmp' : loc_44348, 'other_jmp' : 0x43E80, 'real_case' : 0x0},
{'addr' : 0x43E80, 'caseVal' : 0x3BC74C43, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43E94, 'real_case' : 0x89},
{'addr' : 0x43EA0, 'caseVal' : 0x5C511C12, 'cond' : 'B.LE', 'eq_jmp' : loc_440A0, 'other_jmp' : 0x43EB0, 'real_case' : 0x0},
{'addr' : 0x43EB0, 'caseVal' : 0x5C511C13, 'cond' : 'B.EQ', 'eq_jmp' : loc_441F8, 'other_jmp' : 0x43EC0, 'real_case' : 0x0},
{'addr' : 0x43EC0, 'caseVal' : 0x72910E37, 'cond' : 'B.EQ', 'eq_jmp' : loc_44354, 'other_jmp' : 0x43ED0, 'real_case' : 0x0},
{'addr' : 0x43ED0, 'caseVal' : 0x78E716D8, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43EE4, 'real_case' : 0x89},
{'addr' : 0x43EFC, 'caseVal' : 0xB62C5791, 'cond' : 'B.LE', 'eq_jmp' : loc_440D0, 'other_jmp' : 0x43F0C, 'real_case' : 0x0},
{'addr' : 0x43F0C, 'caseVal' : 0xBBBE05E4, 'cond' : 'B.EQ', 'eq_jmp' : loc_43C04, 'other_jmp' : 0x43F1C, 'real_case' : 0x0},
{'addr' : 0x43F1C, 'caseVal' : 0xBE873E04, 'cond' : 'B.NE', 'eq_jmp' : loc_44388, 'other_jmp' : 0x43F2C, 'real_case' : 0x0},
{'addr' : 0x43F38, 'caseVal' : 0x8ECCC728, 'cond' : 'B.EQ', 'eq_jmp' : loc_4411C, 'other_jmp' : 0x43F48, 'real_case' : 0x0},
{'addr' : 0x43F48, 'caseVal' : 0x913BCE29, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43F5C, 'real_case' : 0x89},
{'addr' : 0x43F68, 'caseVal' : 0xC105D5A5, 'cond' : 'B.EQ', 'eq_jmp' : loc_4412C, 'other_jmp' : 0x43F78, 'real_case' : 0x0},
{'addr' : 0x43F78, 'caseVal' : 0xC3C03B1F, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43F8C, 'real_case' : 0x89},
{'addr' : 0x43FB0, 'caseVal' : 0xECEBC62F, 'cond' : 'B.EQ', 'eq_jmp' : loc_4414C, 'other_jmp' : 0x43FC0, 'real_case' : 0x0},
{'addr' : 0x43FC0, 'caseVal' : 0xEE5D4D0B, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43FD4, 'real_case' : 0x89},
{'addr' : 0x43FE0, 'caseVal' : 0xF8D34334, 'cond' : 'B.EQ', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x43FF8, 'real_case' : 0xFF680FE2},
{'addr' : 0x43FF8, 'caseVal' : 0xFF680FE2, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x4400C, 'real_case' : 0x89},
{'addr' : 0x44020, 'caseVal' : 0x3D42BA4E, 'cond' : 'B.EQ', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x44038, 'real_case' : 0xEFEBCA04},
{'addr' : 0x44038, 'caseVal' : 0x43EDB2A0, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x4404C, 'real_case' : 0x89},
{'addr' : 0x4405C, 'caseVal' : 0x22DF66B8, 'cond' : 'B.EQ', 'eq_jmp' : loc_4415C, 'other_jmp' : 0x4406C, 'real_case' : 0x0},
{'addr' : 0x4406C, 'caseVal' : 0x2F804C20, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x44080, 'real_case' : 0x89},
{'addr' : 0x440A0, 'caseVal' : 0x4CF7C786, 'cond' : 'B.EQ', 'eq_jmp' : loc_44168, 'other_jmp' : 0x440B0, 'real_case' : 0x0},
{'addr' : 0x440B0, 'caseVal' : 0x4E7CBDE0, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x440C4, 'real_case' : 0x89},
{'addr' : 0x440D0, 'caseVal' : 0xB22FA3D0, 'cond' : 'B.EQ', 'eq_jmp' : loc_4429C, 'other_jmp' : 0x440E0, 'real_case' : 0x0},
{'addr' : 0x440E0, 'caseVal' : 0xB50E4CF3, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x440F4, 'real_case' : 0x89},
{'addr' : 0x44388, 'caseVal' : 0xB62C5792, 'cond' : 'B.NE', 'eq_jmp' : loc_43C34, 'other_jmp' : 0x4439C, 'real_case' : 0x89},

现在我们有了这两张表,应该怎么使用呢,以具体的例子观察一下

1
2
{basicBlockAddr : 0x43CC4, nextCaseVal : 0x452944A5}
{addr : 0x43D70, caseVal : 0x452944A5, cond : B.EQ, eq_jmp : loc_44338, other_jmp : 0x43D80}

所以 0x43CC4 -> 0x44338

再具体的观察一下,可以发现所有的 case 值对应的条件都是 B.EQ/ B.NE,条件跳转完全是吓唬人的,没有起到实际作用。之前我文章中提到的那种魔改控制流平坦化混淆才是真的使用大于小于来分发 case 值。

使用 python 脚本批量处理这两个表

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

basic_blocks = [
{'basicBlockAddr' : 0x43CC4, 'nextCaseVal' : 0x452944A5},
{'basicBlockAddr' : 0x43D3C, 'nextCaseVal' : 0x35F42CE9},
{'basicBlockAddr' : 0x43D9C, 'nextCaseVal' : 0xB22FA3D0},
{'basicBlockAddr' : 0x43DFC, 'nextCaseVal' : 0x78E716D8},
{'basicBlockAddr' : 0x43E4C, 'nextCaseVal' : 0x1C536006},
{'basicBlockAddr' : 0x43E9C, 'nextCaseVal' : 0x43F23AAF},
{'basicBlockAddr' : 0x43EF8, 'nextCaseVal' : 0x22DF66B8},
{'basicBlockAddr' : 0x43F34, 'nextCaseVal' : 0x3D42BA4E},
{'basicBlockAddr' : 0x43F64, 'nextCaseVal' : 0x93A75DE8},
{'basicBlockAddr' : 0x43FAC, 'nextCaseVal' : 0xB50E4CF3},
{'basicBlockAddr' : 0x43FDC, 'nextCaseVal' : 0x1C536006},
{'basicBlockAddr' : 0x44058, 'nextCaseVal' : 0xECEBC62F},
{'basicBlockAddr' : 0x440CC, 'nextCaseVal' : 0x78E716D8},
{'basicBlockAddr' : 0x44118, 'nextCaseVal' : 0xD721AE9D},
{'basicBlockAddr' : 0x44128, 'nextCaseVal' : 0x4CF7C786},
{'basicBlockAddr' : 0x44148, 'nextCaseVal' : 0xB22FA3D0},
{'basicBlockAddr' : 0x44158, 'nextCaseVal' : 0x93A75DE8},
{'basicBlockAddr' : 0x44164, 'nextCaseVal' : 0xBBBE05E4},
{'basicBlockAddr' : 0x44174, 'nextCaseVal' : 0x43F23AAF},
{'basicBlockAddr' : 0x44180, 'nextCaseVal' : 0x8ECCC728},
{'basicBlockAddr' : 0x441DC, 'nextCaseVal' : 0x43EDB2A0},
{'basicBlockAddr' : 0x441E8, 'nextCaseVal' : 0xB62C5792},
{'basicBlockAddr' : 0x441F4, 'nextCaseVal' : 0xCA9DBB1F},
{'basicBlockAddr' : 0x44298, 'nextCaseVal' : 0x4B62C933},
{'basicBlockAddr' : 0x442BC, 'nextCaseVal' : 0xBE873E04},
{'basicBlockAddr' : 0x442D8, 'nextCaseVal' : 0x2F804C20},
{'basicBlockAddr' : 0x44334, 'nextCaseVal' : 0xEFEBCA04},
{'basicBlockAddr' : 0x44350, 'nextCaseVal' : 0x97F01E89},
]

dispatch_blocks = [
{'addr' : 0x43C38, 'caseVal' : 0xF8D34333, 'cond' : 'B.LE', 'eq_jmp' : 0x43CC8, 'other_jmp' : 0x43C48, 'real_case' : 0x0},
{'addr' : 0x43C48, 'caseVal' : 0x3D42BA4D, 'cond' : 'B.GT', 'eq_jmp' : 0x43D40, 'other_jmp' : 0x43C58, 'real_case' : 0x0},
{'addr' : 0x43C58, 'caseVal' : 0x22DF66B7, 'cond' : 'B.GT', 'eq_jmp' : 0x43E50, 'other_jmp' : 0x43C68, 'real_case' : 0x0},
{'addr' : 0x43C68, 'caseVal' : 0x17F41D4, 'cond' : 'B.LE', 'eq_jmp' : 0x43FE0, 'other_jmp' : 0x43C78, 'real_case' : 0x0},
{'addr' : 0x43C78, 'caseVal' : 0x17F41D5, 'cond' : 'B.EQ', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43C90, 'real_case' : 0xFF680FE2},
{'addr' : 0x43C90, 'caseVal' : 0x1B0C631C, 'cond' : 'B.EQ', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43CA8, 'real_case' : 0x35F42CE9},
{'addr' : 0x43CA8, 'caseVal' : 0x1C536006, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43CBC, 'real_case' : 0x89},
{'addr' : 0x43CC8, 'caseVal' : 0xC105D5A4, 'cond' : 'B.LE', 'eq_jmp' : 0x43DA0, 'other_jmp' : 0x43CD8, 'real_case' : 0x0},
{'addr' : 0x43CD8, 'caseVal' : 0xECEBC62E, 'cond' : 'B.GT', 'eq_jmp' : 0x43E00, 'other_jmp' : 0x43CE8, 'real_case' : 0x0},
{'addr' : 0x43CE8, 'caseVal' : 0xCA9DBB1E, 'cond' : 'B.LE', 'eq_jmp' : 0x43F68, 'other_jmp' : 0x43CF8, 'real_case' : 0x0},
{'addr' : 0x43CF8, 'caseVal' : 0xCA9DBB1F, 'cond' : 'B.EQ', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43D10, 'real_case' : 0xB50E4CF3},
{'addr' : 0x43D10, 'caseVal' : 0xD721AE9D, 'cond' : 'B.EQ', 'eq_jmp' : 0x442D0, 'other_jmp' : 0x43D20, 'real_case' : 0x0},
{'addr' : 0x43D20, 'caseVal' : 0xE4648FAD, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43D34, 'real_case' : 0x89},
{'addr' : 0x43D40, 'caseVal' : 0x4CF7C785, 'cond' : 'B.GT', 'eq_jmp' : 0x43EA0, 'other_jmp' : 0x43D50, 'real_case' : 0x0},
{'addr' : 0x43D50, 'caseVal' : 0x43F23AAE, 'cond' : 'B.LE', 'eq_jmp' : 0x44020, 'other_jmp' : 0x43D60, 'real_case' : 0x0},
{'addr' : 0x43D60, 'caseVal' : 0x43F23AAF, 'cond' : 'B.EQ', 'eq_jmp' : 0x441E0, 'other_jmp' : 0x43D70, 'real_case' : 0x0},
{'addr' : 0x43D70, 'caseVal' : 0x452944A5, 'cond' : 'B.EQ', 'eq_jmp' : 0x44338, 'other_jmp' : 0x43D80, 'real_case' : 0x0},
{'addr' : 0x43D80, 'caseVal' : 0x4B62C933, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43D94, 'real_case' : 0x89},
{'addr' : 0x43DA0, 'caseVal' : 0xB22FA3CF, 'cond' : 'B.GT', 'eq_jmp' : 0x43EFC, 'other_jmp' : 0x43DB0, 'real_case' : 0x0},
{'addr' : 0x43DB0, 'caseVal' : 0x93A75DE7, 'cond' : 'B.LE', 'eq_jmp' : 0x43F38, 'other_jmp' : 0x43DC0, 'real_case' : 0x0},
{'addr' : 0x43DC0, 'caseVal' : 0x93A75DE8, 'cond' : 'B.EQ', 'eq_jmp' : 0x44178, 'other_jmp' : 0x43DD0, 'real_case' : 0x0},
{'addr' : 0x43DD0, 'caseVal' : 0x97F01E89, 'cond' : 'B.EQ', 'eq_jmp' : 0x442C0, 'other_jmp' : 0x43DE0, 'real_case' : 0x0},
{'addr' : 0x43DE0, 'caseVal' : 0x9A4F670B, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43DF4, 'real_case' : 0x89},
{'addr' : 0x43E00, 'caseVal' : 0xEFEBCA03, 'cond' : 'B.LE', 'eq_jmp' : 0x43FB0, 'other_jmp' : 0x43E10, 'real_case' : 0x0},
{'addr' : 0x43E10, 'caseVal' : 0xEFEBCA04, 'cond' : 'B.EQ', 'eq_jmp' : 0x44184, 'other_jmp' : 0x43E20, 'real_case' : 0x0},
{'addr' : 0x43E20, 'caseVal' : 0xF1508DA1, 'cond' : 'B.EQ', 'eq_jmp' : 0x442DC, 'other_jmp' : 0x43E30, 'real_case' : 0x0},
{'addr' : 0x43E30, 'caseVal' : 0xF50BDBE9, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43E44, 'real_case' : 0x89},
{'addr' : 0x43E50, 'caseVal' : 0x34D451D4, 'cond' : 'B.LE', 'eq_jmp' : 0x4405C, 'other_jmp' : 0x43E60, 'real_case' : 0x0},
{'addr' : 0x43E60, 'caseVal' : 0x34D451D5, 'cond' : 'B.EQ', 'eq_jmp' : 0x441EC, 'other_jmp' : 0x43E70, 'real_case' : 0x0},
{'addr' : 0x43E70, 'caseVal' : 0x35F42CE9, 'cond' : 'B.EQ', 'eq_jmp' : 0x44348, 'other_jmp' : 0x43E80, 'real_case' : 0x0},
{'addr' : 0x43E80, 'caseVal' : 0x3BC74C43, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43E94, 'real_case' : 0x89},
{'addr' : 0x43EA0, 'caseVal' : 0x5C511C12, 'cond' : 'B.LE', 'eq_jmp' : 0x440A0, 'other_jmp' : 0x43EB0, 'real_case' : 0x0},
{'addr' : 0x43EB0, 'caseVal' : 0x5C511C13, 'cond' : 'B.EQ', 'eq_jmp' : 0x441F8, 'other_jmp' : 0x43EC0, 'real_case' : 0x0},
{'addr' : 0x43EC0, 'caseVal' : 0x72910E37, 'cond' : 'B.EQ', 'eq_jmp' : 0x44354, 'other_jmp' : 0x43ED0, 'real_case' : 0x0},
{'addr' : 0x43ED0, 'caseVal' : 0x78E716D8, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43EE4, 'real_case' : 0x89},
{'addr' : 0x43EFC, 'caseVal' : 0xB62C5791, 'cond' : 'B.LE', 'eq_jmp' : 0x440D0, 'other_jmp' : 0x43F0C, 'real_case' : 0x0},
{'addr' : 0x43F0C, 'caseVal' : 0xBBBE05E4, 'cond' : 'B.EQ', 'eq_jmp' : 0x43C04, 'other_jmp' : 0x43F1C, 'real_case' : 0x0},
{'addr' : 0x43F1C, 'caseVal' : 0xBE873E04, 'cond' : 'B.NE', 'eq_jmp' : 0x44388, 'other_jmp' : 0x43F2C, 'real_case' : 0x0},
{'addr' : 0x43F38, 'caseVal' : 0x8ECCC728, 'cond' : 'B.EQ', 'eq_jmp' : 0x4411C, 'other_jmp' : 0x43F48, 'real_case' : 0x0},
{'addr' : 0x43F48, 'caseVal' : 0x913BCE29, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43F5C, 'real_case' : 0x89},
{'addr' : 0x43F68, 'caseVal' : 0xC105D5A5, 'cond' : 'B.EQ', 'eq_jmp' : 0x4412C, 'other_jmp' : 0x43F78, 'real_case' : 0x0},
{'addr' : 0x43F78, 'caseVal' : 0xC3C03B1F, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43F8C, 'real_case' : 0x89},
{'addr' : 0x43FB0, 'caseVal' : 0xECEBC62F, 'cond' : 'B.EQ', 'eq_jmp' : 0x4414C, 'other_jmp' : 0x43FC0, 'real_case' : 0x0},
{'addr' : 0x43FC0, 'caseVal' : 0xEE5D4D0B, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43FD4, 'real_case' : 0x89},
{'addr' : 0x43FE0, 'caseVal' : 0xF8D34334, 'cond' : 'B.EQ', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x43FF8, 'real_case' : 0xFF680FE2},
{'addr' : 0x43FF8, 'caseVal' : 0xFF680FE2, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x4400C, 'real_case' : 0x89},
{'addr' : 0x44020, 'caseVal' : 0x3D42BA4E, 'cond' : 'B.EQ', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x44038, 'real_case' : 0xEFEBCA04},
{'addr' : 0x44038, 'caseVal' : 0x43EDB2A0, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x4404C, 'real_case' : 0x89},
{'addr' : 0x4405C, 'caseVal' : 0x22DF66B8, 'cond' : 'B.EQ', 'eq_jmp' : 0x4415C, 'other_jmp' : 0x4406C, 'real_case' : 0x0},
{'addr' : 0x4406C, 'caseVal' : 0x2F804C20, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x44080, 'real_case' : 0x89},
{'addr' : 0x440A0, 'caseVal' : 0x4CF7C786, 'cond' : 'B.EQ', 'eq_jmp' : 0x44168, 'other_jmp' : 0x440B0, 'real_case' : 0x0},
{'addr' : 0x440B0, 'caseVal' : 0x4E7CBDE0, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x440C4, 'real_case' : 0x89},
{'addr' : 0x440D0, 'caseVal' : 0xB22FA3D0, 'cond' : 'B.EQ', 'eq_jmp' : 0x4429C, 'other_jmp' : 0x440E0, 'real_case' : 0x0},
{'addr' : 0x440E0, 'caseVal' : 0xB50E4CF3, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x440F4, 'real_case' : 0x89},
{'addr' : 0x44388, 'caseVal' : 0xB62C5792, 'cond' : 'B.NE', 'eq_jmp' : 0x43C34, 'other_jmp' : 0x4439C, 'real_case' : 0x89},
]

for basic_block in basic_blocks:
nextCaseVal = basic_block.get("nextCaseVal")
for dispatch_block in dispatch_blocks:
caseVal = dispatch_block.get("caseVal")
if caseVal == nextCaseVal:
cond = dispatch_block.get("cond")
realCaseVal =dispatch_block.get("real_case")
if realCaseVal != 0 and realCaseVal != 0x89:
nextCaseVal = realCaseVal
for dispatch_block in dispatch_blocks:
caseVal = dispatch_block.get("caseVal")
if caseVal == nextCaseVal:
cond = dispatch_block.get("cond")
if cond == "B.NE":
branch = dispatch_block.get("other_jmp")
if cond == "B.EQ":
branch = dispatch_block.get("eq_jmp")
else:
if cond == "B.NE":
branch = dispatch_block.get("other_jmp")
if cond == "B.EQ":
branch = dispatch_block.get("eq_jmp")
print(f"jmp {hex(basic_block.get("basicBlockAddr"))} -> {hex(branch)}")

我 python 水平一般,写的脚本比较丑陋,大家见谅 (≧﹏ ≦)

输出跳转关系表如下

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
jmp 0x43cc4 -> 0x44338
jmp 0x43d3c -> 0x44348
jmp 0x43d9c -> 0x4429c
jmp 0x43dfc -> 0x43ee4
jmp 0x43e4c -> 0x43cbc
jmp 0x43e9c -> 0x441e0
jmp 0x43ef8 -> 0x4415c
jmp 0x43f34 -> 0x44184
jmp 0x43f64 -> 0x44178
jmp 0x43fac -> 0x440f4
jmp 0x43fdc -> 0x43cbc
jmp 0x44058 -> 0x4414c
jmp 0x440cc -> 0x43ee4
jmp 0x44118 -> 0x442d0
jmp 0x44128 -> 0x44168
jmp 0x44148 -> 0x4429c
jmp 0x44158 -> 0x44178
jmp 0x44164 -> 0x43c04
jmp 0x44174 -> 0x441e0
jmp 0x44180 -> 0x4411c
jmp 0x441dc -> 0x4404c
jmp 0x441e8 -> 0x4439c
jmp 0x441f4 -> 0x440f4
jmp 0x44298 -> 0x43d94
jmp 0x442bc -> 0x43f2c
jmp 0x442d8 -> 0x44080
jmp 0x44334 -> 0x44184
jmp 0x44350 -> 0x442c0

根据这张表 patch 代码即可。效果如下

恢复后的 cfg 图

这是 MD5 函数的外层函数,内层算法混淆和这种是一样的,这里就不再展示了

# 案例 3

目标为 tx 的反作弊 so 文件 libtersafe.so,被间接跳转和控制流平坦化保护。由于不好动态,所以我们纯静态的去除这两种混淆

间接跳转

先观察是如何进行间接跳转混淆的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:000000000021094C                 MOV             W8, #0x5A63
.text:0000000000210950 MOV W9, #0x9281
.text:0000000000210954 CMP W1, #0
.text:0000000000210958 MOVK W8, #0xD73F,LSL#16
.text:000000000021095C MOVK W9, #0x69A0,LSL#16
.text:0000000000210960 ADRL X23, off_527780
.text:0000000000210968 ADD W10, W8, #0x11
.text:000000000021096C ADD W11, W9, #0x11
.text:0000000000210970 EOR W8, W10, W8
.text:0000000000210974 EOR W10, W11, W9
.text:0000000000210978 MOV X25, X2
.text:000000000021097C CSEL W8, W8, W10, EQ
.text:0000000000210980 MOV W26, W1
.text:0000000000210984 STP W11, W9, [X29,#var_8]
.text:0000000000210988 LDR X8, [X23,W8,UXTW#3]
.text:000000000021098C BR X8

X23 是函数表,根据计算出的偏移对其进行查找

偏移的计算公式为 (imm + num) ^ imm。除了加法外还有其他计算,sub、or 等

每个函数只对函数表赋一次值,如果看到附近其它块也以 X23 为基址寻找跳转函数,大概率都是 21094C 这个函数的

计算的值不一定是常数,还有可能是通过寄存器计算。

通过观察我们可以发现,不是所有的块都对函数表进行了赋值,被间接跳转切割的块全部使用同一个寄存器作为跳转表,因此,同一个函数的块都是相连在一起的。

知道此函数计算公式为 (imm cal num) ^ imm 之后,就可以写脚本提取数值模拟计算,找到要跳转的地址。、

因为脚本比较复杂,之后的脚本就都使用 ai 生成了

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
import re

import ida_bytes
import ida_funcs
import ida_kernwin
import ida_lines
import idc


START_EA = 0x21092C
END_EA = 0x210F2C
TABLE_EA = 0x527780
PTR_SIZE = 8
TABLE_COUNT = 0x100
ADD_COMMENTS = True

MASK32 = 0xFFFFFFFF
MAX_BACKTRACK = 0x400
STOP_MNEMS = {"br", "blr", "ret"}


def _msg(s):
try:
ida_kernwin.msg(s + "\n")
except Exception:
print(s)


def mnem(ea):
return (idc.print_insn_mnem(ea) or "").lower()


def op_text(ea, n):
text = idc.print_operand(ea, n) or ""
return ida_lines.tag_remove(text).strip()


def norm_reg(reg):
reg = (reg or "").strip().lower()
reg = reg.replace(",", "")
if reg.startswith("x") and reg[1:].isdigit():
return "w" + reg[1:]
if reg == "xzr":
return "wzr"
return reg


def reg64(reg):
reg = norm_reg(reg)
if reg.startswith("w") and reg[1:].isdigit():
return "x" + reg[1:]
if reg == "wzr":
return "xzr"
return reg


def is_reg_operand(ea, n):
return idc.get_operand_type(ea, n) == idc.o_reg


def imm_operand(ea, n):
typ = idc.get_operand_type(ea, n)
if typ in (idc.o_imm, idc.o_near, idc.o_far):
return idc.get_operand_value(ea, n) & MASK32
return None


def mov_shift(ea):
# IDA may keep the LSL as part of operand 1 or only in the rendered line.
for n in range(1, 5):
text = op_text(ea, n)
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", text, re.I)
if m:
return int(m.group(1), 0)
line = ida_lines.tag_remove(idc.generate_disasm_line(ea, 0) or "")
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", line, re.I)
return int(m.group(1), 0) if m else 0


def prev_head(ea, start):
p = idc.prev_head(ea, start)
return None if p == idc.BADADDR or p < start else p


def next_head(ea, end):
n = idc.next_head(ea, end)
return None if n == idc.BADADDR or n >= end else n


def operand_value(ea, n, start, depth, seen):
if is_reg_operand(ea, n):
reg = norm_reg(op_text(ea, n))
if reg in ("wzr", "xzr"):
return 0
return resolve_reg(reg, ea, start, depth + 1, seen)
return imm_operand(ea, n)


def apply_binop(insn, a, b):
if a is None or b is None:
return None
if insn == "add":
return (a + b) & MASK32
if insn == "sub":
return (a - b) & MASK32
if insn == "orr":
return (a | b) & MASK32
if insn == "eor":
return (a ^ b) & MASK32
return None


def resolve_reg(reg, before_ea, start, depth=0, seen=None):
"""Recover a 32-bit constant value for reg immediately before before_ea."""
reg = norm_reg(reg)
if reg in ("wzr", "xzr"):
return 0
if seen is None:
seen = set()
key = (reg, before_ea)
if key in seen or depth > 32:
return None
seen.add(key)

ea = prev_head(before_ea, start)
low_limit = max(start, before_ea - MAX_BACKTRACK)
while ea is not None and ea >= low_limit:
insn = mnem(ea)
if insn in STOP_MNEMS:
return None

dst = norm_reg(op_text(ea, 0))
if dst == reg:
if insn in ("mov", "movz"):
imm = imm_operand(ea, 1)
if imm is not None:
return (imm << mov_shift(ea)) & MASK32
if is_reg_operand(ea, 1):
return resolve_reg(op_text(ea, 1), ea, start, depth + 1, seen)
return None

if insn == "movn":
imm = imm_operand(ea, 1)
if imm is None:
return None
return (~(imm << mov_shift(ea))) & MASK32

if insn == "movk":
imm = imm_operand(ea, 1)
if imm is None:
return None
old = resolve_reg(reg, ea, start, depth + 1, seen)
if old is None:
old = 0
shift = mov_shift(ea)
mask = (0xFFFF << shift) & MASK32
return ((old & ~mask) | ((imm & 0xFFFF) << shift)) & MASK32

if insn in ("add", "sub", "orr", "eor"):
a = operand_value(ea, 1, start, depth, seen)
b = operand_value(ea, 2, start, depth, seen)
return apply_binop(insn, a, b)

return None

ea = prev_head(ea, start)
return None


def csel_operands(ea):
dst = norm_reg(op_text(ea, 0))
src_true = norm_reg(op_text(ea, 1))
src_false = norm_reg(op_text(ea, 2))
cond = op_text(ea, 3).lower()
if not cond:
line = idc.generate_disasm_line(ea, 0) or ""
parts = [p.strip().lower() for p in line.split(",")]
cond = parts[-1] if len(parts) >= 4 else "?"
return dst, src_true, src_false, cond


def operand_mentions_reg(text, reg):
regs = set(re.findall(r"\b[wx](?:\d+|zr)\b", text.lower()))
wanted = {norm_reg(reg), reg64(reg)}
return bool(regs & wanted)


def find_table_load_and_br(csel_ea, end, index_reg):
ea = next_head(csel_ea, end)
load_ea = None
branch_reg = None

while ea is not None:
insn = mnem(ea)

if insn == "ldr" and operand_mentions_reg(op_text(ea, 1), index_reg):
load_ea = ea
branch_reg = reg64(op_text(ea, 0))

if insn == "br":
br_reg = reg64(op_text(ea, 0))
if branch_reg and br_reg == branch_reg:
return load_ea, ea, branch_reg
return None, ea, None

if insn in ("b", "ret", "blr"):
return None, ea, None

ea = next_head(ea, end)

return None, None, None


def read_table_target(index):
if index is None:
return None
if index < 0 or index >= TABLE_COUNT:
return None
ea = TABLE_EA + (index & MASK32) * PTR_SIZE
if PTR_SIZE == 8:
return ida_bytes.get_qword(ea)
return ida_bytes.get_dword(ea)


def value_from_regs(ea, n, regs):
if is_reg_operand(ea, n):
item = regs.get(norm_reg(op_text(ea, n)))
return item[0] if is_const_item(item) else None
return imm_operand(ea, n)


def is_const_item(item):
return isinstance(item, tuple) and len(item) == 3 and isinstance(item[0], int)


def const_item(value, assigned=False, calculated=False):
if value is None:
return None
return (value & MASK32, assigned, calculated)


def item_value(item):
return item[0] if is_const_item(item) else None


def item_has_assignment_and_calc(item):
return is_const_item(item) and item[1] and item[2]


def csel_source_item(regs, reg):
reg = norm_reg(reg)
if reg in ("wzr", "xzr"):
return const_item(0)
return regs.get(reg)


def item_is_wzr_const(item):
return is_const_item(item) and item[0] == 0 and not item[1] and not item[2]


def csel_pair_ready(item_true, item_false):
true_ready = item_has_assignment_and_calc(item_true)
false_ready = item_has_assignment_and_calc(item_false)
true_zero = item_is_wzr_const(item_true)
false_zero = item_is_wzr_const(item_false)
return (true_ready and (false_ready or false_zero)) or (false_ready and (true_ready or true_zero))


def merge_calc_item(value, *items):
assigned = any(is_const_item(item) and item[1] for item in items)
return const_item(value, assigned=assigned, calculated=True)


def set_reg_value(regs, reg, value):
reg = norm_reg(reg)
if reg in ("wzr", "xzr"):
return
if value is None:
regs.pop(reg, None)
else:
regs[reg] = value


def table_choice_from_mem(ea, regs):
mem = op_text(ea, 1)
for reg, value in regs.items():
if isinstance(value, tuple) and value[0] == "choice" and operand_mentions_reg(mem, reg):
return value
return None


def add_note(ea, text):
if not ADD_COMMENTS:
return
old = idc.get_cmt(ea, 0) or ""
if text in old:
return
new = (old + "\n" + text).strip() if old else text
idc.set_cmt(ea, new, 0)


def analyze_range(start=START_EA, end=END_EA):
regs = {}
pending_loads = {}
rows = []
ea = start

while ea is not None and ea < end:
insn = mnem(ea)
dst = norm_reg(op_text(ea, 0))

if insn in ("mov", "movz"):
imm = imm_operand(ea, 1)
if imm is not None:
set_reg_value(regs, dst, const_item(imm << mov_shift(ea), assigned=True))
elif is_reg_operand(ea, 1):
src_item = regs.get(norm_reg(op_text(ea, 1)))
set_reg_value(regs, dst, src_item if is_const_item(src_item) else None)
else:
set_reg_value(regs, dst, None)

elif insn == "movn":
imm = imm_operand(ea, 1)
set_reg_value(regs, dst, None if imm is None else const_item(~(imm << mov_shift(ea)), assigned=True))

elif insn == "movk":
imm = imm_operand(ea, 1)
old_item = regs.get(dst)
old = item_value(old_item)
if old is None:
old = 0
if imm is None:
set_reg_value(regs, dst, None)
else:
shift = mov_shift(ea)
mask = (0xFFFF << shift) & MASK32
set_reg_value(
regs,
dst,
const_item(((old & ~mask) | ((imm & 0xFFFF) << shift)) & MASK32, assigned=True),
)

elif insn in ("add", "sub", "orr", "eor"):
item_a = regs.get(norm_reg(op_text(ea, 1))) if is_reg_operand(ea, 1) else const_item(imm_operand(ea, 1))
item_b = regs.get(norm_reg(op_text(ea, 2))) if is_reg_operand(ea, 2) else const_item(imm_operand(ea, 2))
value = apply_binop(insn, item_value(item_a), item_value(item_b))
set_reg_value(regs, dst, None if value is None else merge_calc_item(value, item_a, item_b))

elif insn == "csel":
_, src_true, src_false, cond = csel_operands(ea)
item_true = csel_source_item(regs, src_true)
item_false = csel_source_item(regs, src_false)
if csel_pair_ready(item_true, item_false):
set_reg_value(regs, dst, ("choice", cond, item_value(item_true), item_value(item_false), ea))
else:
set_reg_value(regs, dst, None)

elif insn == "ldr":
choice = table_choice_from_mem(ea, regs)
if choice is not None:
_, cond, idx_true, idx_false, csel_ea = choice
target_true = read_table_target(idx_true)
target_false = read_table_target(idx_false)
pending_loads[reg64(dst)] = (csel_ea, cond, idx_true, idx_false, target_true, target_false, ea)
set_reg_value(regs, dst, None)
else:
set_reg_value(regs, dst, None)

elif insn == "br":
br_reg = reg64(op_text(ea, 0))
pending = pending_loads.pop(br_reg, None)
if pending is not None:
csel_ea, cond, idx_true, idx_false, target_true, target_false, load_ea = pending
ok = target_true is not None and target_false is not None
rows.append((csel_ea, cond, idx_true, idx_false, target_true, target_false, load_ea, ea, ok))
if ok:
note = "case: if %s -> 0x%X; else -> 0x%X" % (cond, target_true, target_false)
add_note(csel_ea, note)
add_note(ea, "table br from CSEL 0x%X: %s" % (csel_ea, note))
pending_loads.clear()
regs.clear()

elif insn in ("bl", "blr"):
# AArch64 calls may clobber caller-saved registers. Keep x19-x28 constants.
for i in range(19):
regs.pop("w%d" % i, None)

ea = next_head(ea, end)

return rows


def print_rows(rows):
for row in rows:
csel_ea, cond, idx_t, idx_f, tgt_t, tgt_f, ldr_ea, br_ea, ok = row
if not ok:
continue
_msg(
"0x%X %s 0x%X, B 0x%X"
% (
br_ea,
cond.upper(),
tgt_t,
tgt_f,
)
)


def main():
func = ida_funcs.get_func(START_EA)
if func:
_msg("function: %s 0x%X-0x%X" % (idc.get_func_name(func.start_ea), START_EA, END_EA))
_msg("table: 0x%X, ptr_size=%d" % (TABLE_EA, PTR_SIZE))
rows = analyze_range(START_EA, END_EA)
print_rows(rows)
_msg("resolved %d/%d CSEL table branches" % (sum(1 for r in rows if r[-1]), len(rows)))


if __name__ == "__main__":
main()

输出

1
2
3
4
5
6
7
8
9
10
11
0x21098C EQ 0x210EF8, B 0x210990
0x2109D8 EQ 0x2109DC, B 0x210D08
0x210A28 GT 0x210A2C, B 0x210D48
0x210AD8 EQ 0x210B2C, B 0x210ADC
0x210B64 EQ 0x210B70, B 0x210B68
0x210BD0 HI 0x210F04, B 0x210D84
0x210C14 CC 0x210C18, B 0x210C3C
0x210D44 GT 0x210A2C, B 0x210D48
0x210D80 HI 0x210F04, B 0x210D84
0x210E0C EQ 0x210EE4, B 0x210E10
0x210EA8 EQ 0x210B70, B 0x210B68

输出的块只是一部分,还有一些没有打印出来,比如 sub_210E10 函数

这个函数没有任何赋值常数的操作,证明赋值这个行为是在上个块进行的,看看它的上个块是什么

1
0x210E0C EQ 0x2137D4, B 0x210E10

0x210E0C 所在块

确实对 W20、W21 进行了赋值。

所以,我们可以遍历表中第一列,第一列的块一定是其它块的前驱块。从前驱块开始,找后继块的赋值并计算跳转值

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
import re

import ida_bytes
import ida_kernwin
import ida_lines
import idc


START_EA = 0x21092C
END_EA = 0x210F2C
TABLE_EA = 0x527780
PTR_SIZE = 8
TABLE_COUNT = 0x100
MAX_BLOCK_BYTES = 0x300


SEED_EDGES = [
(0x21098C, "EQ", 0x210EF8, 0x210990),
(0x2109D8, "EQ", 0x2109DC, 0x210D08),
(0x210A28, "GT", 0x210A2C, 0x210D48),
(0x210AD8, "EQ", 0x210B2C, 0x210ADC),
(0x210B64, "EQ", 0x210B70, 0x210B68),
(0x210BD0, "HI", 0x210F04, 0x210D84),
(0x210C14, "CC", 0x210C18, 0x210C3C),
(0x210D44, "GT", 0x210A2C, 0x210D48),
(0x210D80, "HI", 0x210F04, 0x210D84),
(0x210E0C, "EQ", 0x210EE4, 0x210E10),
(0x210EA8, "EQ", 0x210B70, 0x210B68),
]


MASK32 = 0xFFFFFFFF


def out(s):
try:
ida_kernwin.msg(s + "\n")
except Exception:
print(s)


def mnem(ea):
return (idc.print_insn_mnem(ea) or "").lower()


def op(ea, n):
return ida_lines.tag_remove(idc.print_operand(ea, n) or "").strip()


def norm_reg(reg):
reg = (reg or "").strip().lower().replace(",", "")
if reg.startswith("x") and reg[1:].isdigit():
return "w" + reg[1:]
if reg == "xzr":
return "wzr"
return reg


def reg64(reg):
reg = norm_reg(reg)
if reg.startswith("w") and reg[1:].isdigit():
return "x" + reg[1:]
if reg == "wzr":
return "xzr"
return reg


def is_reg(ea, n):
return idc.get_operand_type(ea, n) == idc.o_reg


def imm(ea, n):
if idc.get_operand_type(ea, n) in (idc.o_imm, idc.o_near, idc.o_far):
return idc.get_operand_value(ea, n) & MASK32
return None


def mov_shift(ea):
for n in range(1, 5):
text = op(ea, n)
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", text, re.I)
if m:
return int(m.group(1), 0)
line = ida_lines.tag_remove(idc.generate_disasm_line(ea, 0) or "")
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", line, re.I)
return int(m.group(1), 0) if m else 0


def next_ea(ea, end):
n = idc.next_head(ea, end)
return None if n == idc.BADADDR or n >= end else n


def calc(insn, a, b):
if a is None or b is None:
return None
if insn == "add":
return (a + b) & MASK32
if insn == "sub":
return (a - b) & MASK32
if insn == "orr":
return (a | b) & MASK32
if insn == "eor":
return (a ^ b) & MASK32
return None


def item(value, assigned=False, calculated=False):
if value is None:
return None
return (value & MASK32, assigned, calculated)


def item_ok(v):
return isinstance(v, tuple) and len(v) == 3 and isinstance(v[0], int)


def item_value(v):
return v[0] if item_ok(v) else None


def item_ready(v):
return item_ok(v) and v[1] and v[2]


def set_reg(regs, reg, value):
reg = norm_reg(reg)
if reg in ("wzr", "xzr"):
return
if value is None:
regs.pop(reg, None)
else:
regs[reg] = value


def mentions_reg(text, reg):
regs = set(re.findall(r"\b[wx](?:\d+|zr)\b", text.lower()))
return bool(regs & {norm_reg(reg), reg64(reg)})


def read_target(index):
if index is None or index < 0 or index >= TABLE_COUNT:
return None
ea = TABLE_EA + index * PTR_SIZE
return ida_bytes.get_qword(ea) if PTR_SIZE == 8 else ida_bytes.get_dword(ea)


def csel_info(ea):
dst = norm_reg(op(ea, 0))
src_t = norm_reg(op(ea, 1))
src_f = norm_reg(op(ea, 2))
cond = op(ea, 3).upper()
if not cond:
line = ida_lines.tag_remove(idc.generate_disasm_line(ea, 0) or "")
parts = [p.strip().upper() for p in line.split(",")]
cond = parts[-1] if len(parts) >= 4 else "?"
return dst, src_t, src_f, cond


def call_clobber(regs):
for i in range(19):
regs.pop("w%d" % i, None)


def copy_regs(regs):
return dict(regs or {})


def known_block_starts():
starts = {START_EA}
for _, _, true_target, false_target in SEED_EDGES:
if START_EA <= true_target < END_EA:
starts.add(true_target)
if START_EA <= false_target < END_EA:
starts.add(false_target)
ea = START_EA
while ea is not None and ea < END_EA:
if mnem(ea) == "br":
n = next_ea(ea, END_EA)
if n is not None:
starts.add(n)
ea = next_ea(ea, END_EA)
return sorted(starts)


def block_start_for_br(br_ea, starts):
prev = None
for start in starts:
if start <= br_ea:
prev = start
else:
break
return prev


def analyze_block(block_start, initial_regs=None):
"""Analyze one candidate block entry until the first BR/direct terminal."""
regs = copy_regs(initial_regs)
pending = {}
ea = block_start
end = min(END_EA, block_start + MAX_BLOCK_BYTES)

while ea is not None and START_EA <= ea < end:
insn = mnem(ea)
dst = norm_reg(op(ea, 0))

if insn in ("mov", "movz"):
v = imm(ea, 1)
if v is not None:
set_reg(regs, dst, item(v << mov_shift(ea), assigned=True))
elif is_reg(ea, 1):
src = regs.get(norm_reg(op(ea, 1)))
set_reg(regs, dst, src if item_ok(src) else None)
else:
set_reg(regs, dst, None)

elif insn == "movn":
v = imm(ea, 1)
set_reg(regs, dst, None if v is None else item(~(v << mov_shift(ea)), assigned=True))

elif insn == "movk":
v = imm(ea, 1)
old = item_value(regs.get(dst))
if old is None:
old = 0
if v is None:
set_reg(regs, dst, None)
else:
shift = mov_shift(ea)
mask = (0xFFFF << shift) & MASK32
set_reg(regs, dst, item((old & ~mask) | ((v & 0xFFFF) << shift), assigned=True))

elif insn in ("add", "sub", "orr", "eor"):
a_item = regs.get(norm_reg(op(ea, 1))) if is_reg(ea, 1) else item(imm(ea, 1))
b_item = regs.get(norm_reg(op(ea, 2))) if is_reg(ea, 2) else item(imm(ea, 2))
value = calc(insn, item_value(a_item), item_value(b_item))
assigned = (item_ok(a_item) and a_item[1]) or (item_ok(b_item) and b_item[1])
set_reg(regs, dst, None if value is None else item(value, assigned=assigned, calculated=True))

elif insn == "csel":
dst, src_t, src_f, cond = csel_info(ea)
t_item = regs.get(src_t)
f_item = regs.get(src_f)
if item_ready(t_item) and item_ready(f_item):
set_reg(regs, dst, ("choice", cond, item_value(t_item), item_value(f_item), ea))
else:
set_reg(regs, dst, None)

elif insn == "ldr":
mem = op(ea, 1)
choice = None
for reg, value in regs.items():
if isinstance(value, tuple) and value and value[0] == "choice" and mentions_reg(mem, reg):
choice = value
break
if choice is not None:
_, cond, idx_t, idx_f, csel_ea = choice
pending[reg64(dst)] = (cond, read_target(idx_t), read_target(idx_f), csel_ea, ea)
set_reg(regs, dst, None)

elif insn == "br":
regs_at_br = copy_regs(regs)
data = pending.get(reg64(op(ea, 0)))
if data is None:
return {"row": None, "regs": regs_at_br, "br": ea}
cond, target_t, target_f, csel_ea, ldr_ea = data
if target_t is None or target_f is None:
return {"row": None, "regs": regs_at_br, "br": ea}
row = (ea, cond, target_t, target_f, block_start, csel_ea, ldr_ea)
return {"row": row, "regs": regs_at_br, "br": ea}

elif insn == "b":
return {"row": None, "regs": copy_regs(regs), "br": None}

elif insn in ("bl", "blr"):
call_clobber(regs)

elif insn == "ret":
return {"row": None, "regs": copy_regs(regs), "br": None}

ea = next_ea(ea, end)

return {"row": None, "regs": copy_regs(regs), "br": None}


def main():
starts = known_block_starts()
queue = []
seed_by_br = {}
for br_ea, cond, true_target, false_target in SEED_EDGES:
block_start = block_start_for_br(br_ea, starts)
if block_start is None:
continue
seed_by_br[br_ea] = (cond, true_target, false_target)
queue.append((block_start, None, br_ea))

seen_jobs = set()
seen_brs = {br for br, _, _, _ in SEED_EDGES}
recovered = []

while queue:
start, initial_regs, expected_br = queue.pop(0)
if not (START_EA <= start < END_EA):
continue

# The same block may need to be revisited with a different predecessor state.
state_key = tuple(sorted((k, v) for k, v in (initial_regs or {}).items() if item_ok(v)))
job_key = (start, expected_br, state_key)
if job_key in seen_jobs:
continue
seen_jobs.add(job_key)

result = analyze_block(start, initial_regs)
if result is None:
continue
row = result["row"]
regs_at_br = result["regs"]
actual_br = result["br"]

if expected_br is not None and actual_br != expected_br:
continue

if row is None and expected_br in seed_by_br:
cond, target_t, target_f = seed_by_br[expected_br]
queue.append((target_t, regs_at_br, None))
queue.append((target_f, regs_at_br, None))
continue

if row is None:
continue

br_ea, cond, target_t, target_f, block_start, csel_ea, ldr_ea = row
if br_ea not in seen_brs:
recovered.append(row)
seen_brs.add(br_ea)
queue.append((target_t, regs_at_br, None))
queue.append((target_f, regs_at_br, None))

for br_ea, cond, target_t, target_f, block_start, csel_ea, ldr_ea in recovered:
out("0x%X %s 0x%X, B 0x%X" % (br_ea, cond, target_t, target_f))

out("recovered %d new table branches" % len(recovered))


if __name__ == "__main__":
main()

输出:

1
2
3
4
5
6
7
8
9
0x210B00 EQ 0x210BD4, B 0x210B04
0x210E34 EQ 0x210EAC, B 0x210E38
0x210B28 EQ 0x210B2C, B 0x210ADC
0x210E68 EQ 0x210EE4, B 0x210E10
0x210C60 EQ 0x210CB4, B 0x210C64
0x210CD8 EQ 0x210E6C, B 0x210CDC
0x210C88 EQ 0x210BD4, B 0x210C8C
0x210D04 EQ 0x210BD4, B 0x210CB4
0x210CB0 EQ 0x210CB4, B 0x210C64

这样剩下的跳转关系也齐了。

patch 完效果如下

其他间接跳转也可以使用脚本,改一下开头的起始地址,结束地址,表地址即可,也可以写脚本自动提取。

这是随便找了另一个间接跳转函数去除的效果,中间出现的一些值是间接跳转的遗留产物,不影响正常逻辑

觉得影响也可以 patch 掉

控制流平坦化

tss_sdk_encryptpacket 函数为例

观察 cfg 图,这四块明显是控制流平坦化中的分发块

最上面的控制流平坦化结构如下

用来比较的是 w21 和其他寄存器,但是这些寄存器在入口块就已经赋值过了,所以这块分发块是有固定的执行顺序的

1
2
3
4
0x1B9C84 -> 0x1B9C9C
0x1B9CA0 -> 0x1B9CCC,
0x1B9CD0 -> 0x1B9CD4,
0x1B9CD8 -> 0x1B9EE4,

这个地方的跳转关系可以轻松得到。

剩下三块分发块都是下面这个结构

以 w8 寄存器作为对比寄存器,基本块跳转地址为 loc_1B9F9C

这个地方将保存的 case 值赋给 w8,再跳转到分发块进行分发。

之前说到 fla 去除有三步:获取基本块 case 值、遍历分发块的 case 值得到跳转地址、patch

现在已知分发块结构为 cmp x8, reg + B.cond,而用来比较的 reg 一定在之前被赋值过,那么就可以写脚本得到分发块表。

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
import re

import ida_funcs
import idautils
import ida_lines
import idc


FUNC_EA = 0x1B9C24
MAX_BACKTRACK = 0x800
MASK32 = 0xFFFFFFFF
DEF_MNEMS = {"mov", "movz", "movn", "movk", "add", "sub", "orr", "eor", "csel"}


def msg(line):
try:
print(line)
except Exception:
pass


def clean(text):
return ida_lines.tag_remove(text or "").strip()


def mnem(ea):
return clean(idc.print_insn_mnem(ea)).lower()


def op(ea, n):
return clean(idc.print_operand(ea, n))


def norm_reg(reg):
reg = (reg or "").strip().lower().replace(",", "")
if reg.startswith("x") and reg[1:].isdigit():
return "w" + reg[1:]
if reg == "xzr":
return "wzr"
return reg


def is_reg(ea, n):
return idc.get_operand_type(ea, n) == idc.o_reg


def imm(ea, n):
if idc.get_operand_type(ea, n) in (idc.o_imm, idc.o_near, idc.o_far):
return idc.get_operand_value(ea, n) & MASK32
return None


def mov_shift(ea):
for n in range(1, 5):
text = op(ea, n)
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", text, re.I)
if m:
return int(m.group(1), 0)
line = clean(idc.generate_disasm_line(ea, 0))
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", line, re.I)
return int(m.group(1), 0) if m else 0


def prev_head(ea, start):
p = idc.prev_head(ea, start)
return None if p == idc.BADADDR or p < start else p


def next_head(ea, end):
n = idc.next_head(ea, end)
return None if n == idc.BADADDR or n >= end else n


def is_cond_branch(ea):
name = mnem(ea)
return name.startswith("b.") and name not in ("b.al", "b.nv")


def branch_cond(ea):
name = mnem(ea)
return name[2:].upper() if name.startswith("b.") else name.upper()


def branch_target_text(ea):
text = op(ea, 0)
value = idc.get_operand_value(ea, 0)
if value != idc.BADADDR:
return "0x%X" % value
return text


def apply_binop(name, a, b):
if a is None or b is None:
return None
if name == "add":
return (a + b) & MASK32
if name == "sub":
return (a - b) & MASK32
if name == "orr":
return (a | b) & MASK32
if name == "eor":
return (a ^ b) & MASK32
return None


def operand_value(ea, n, func_start, depth, seen):
if is_reg(ea, n):
reg = norm_reg(op(ea, n))
if reg in ("wzr", "xzr"):
return 0
return resolve_reg_value(reg, ea, func_start, depth + 1, seen)
return imm(ea, n)


def resolve_reg_value(reg, before_ea, func_start, depth=0, seen=None):
"""Resolve constant value of reg immediately before before_ea."""
reg = norm_reg(reg)
if reg in ("wzr", "xzr"):
return 0
if seen is None:
seen = set()
key = (reg, before_ea)
if key in seen or depth > 48:
return None
seen.add(key)

low = max(func_start, before_ea - MAX_BACKTRACK)
ea = prev_head(before_ea, func_start)
while ea is not None and ea >= low:
name = mnem(ea)
dst = norm_reg(op(ea, 0))

if dst == reg and name in DEF_MNEMS:
if name in ("mov", "movz"):
value = imm(ea, 1)
if value is not None:
return (value << mov_shift(ea)) & MASK32
if is_reg(ea, 1):
return resolve_reg_value(op(ea, 1), ea, func_start, depth + 1, seen)
return None

if name == "movn":
value = imm(ea, 1)
if value is None:
return None
return (~(value << mov_shift(ea))) & MASK32

if name == "movk":
value = imm(ea, 1)
if value is None:
return None
old = resolve_reg_value(reg, ea, func_start, depth + 1, seen)
if old is None:
old = 0
shift = mov_shift(ea)
mask = (0xFFFF << shift) & MASK32
return ((old & ~mask) | ((value & 0xFFFF) << shift)) & MASK32

if name in ("add", "sub", "orr", "eor"):
a = operand_value(ea, 1, func_start, depth, seen)
b = operand_value(ea, 2, func_start, depth, seen)
return apply_binop(name, a, b)

return None

ea = prev_head(ea, func_start)

return None


def get_cmp_case_value(cmp_ea, func_start):
if mnem(cmp_ea) != "cmp":
return None
if norm_reg(op(cmp_ea, 0)) != "w8":
return None
if not is_reg(cmp_ea, 1):
return None
return resolve_reg_value(norm_reg(op(cmp_ea, 1)), cmp_ea, func_start)


def find_flag_source_cmp_for_leaf(leaf_branch_ea, func_start):
for xref in idautils.XrefsTo(leaf_branch_ea):
pred = xref.frm
if not is_cond_branch(pred):
continue
cmp_ea = prev_head(pred, func_start)
if cmp_ea is None:
continue
if get_cmp_case_value(cmp_ea, func_start) is not None:
return cmp_ea
return None


def find_cases(func_ea=FUNC_EA):
func = ida_funcs.get_func(func_ea)
if not func:
raise RuntimeError("function not found at 0x%X" % func_ea)

rows = []
seen_addrs = set()
ea = func.start_ea
while ea is not None and ea < func.end_ea:
if mnem(ea) == "cmp":
br_ea = next_head(ea, func.end_ea)
if br_ea is None or not is_cond_branch(br_ea):
ea = next_head(ea, func.end_ea)
continue

case_val = get_cmp_case_value(ea, func.start_ea)
if case_val is None:
ea = next_head(ea, func.end_ea)
continue

other_jmp = next_head(br_ea, func.end_ea)
if other_jmp is None:
ea = next_head(ea, func.end_ea)
continue

if ea not in seen_addrs:
rows.append((ea, case_val, branch_cond(br_ea), branch_target_text(br_ea), other_jmp))
seen_addrs.add(ea)
ea = next_head(ea, func.end_ea)
continue

if not is_cond_branch(ea):
ea = next_head(ea, func.end_ea)
continue

prev = prev_head(ea, func.start_ea)
if prev is not None and mnem(prev) == "cmp":
ea = next_head(ea, func.end_ea)
continue

cmp_ea = find_flag_source_cmp_for_leaf(ea, func.start_ea)
if cmp_ea is None:
ea = next_head(ea, func.end_ea)
continue
case_val = get_cmp_case_value(cmp_ea, func.start_ea)
other_jmp = next_head(ea, func.end_ea)
if case_val is not None and other_jmp is not None and ea not in seen_addrs:
rows.append((ea, case_val, branch_cond(ea), branch_target_text(ea), other_jmp))
seen_addrs.add(ea)

ea = next_head(ea, func.end_ea)

return rows


def main():
for addr, case_val, cond, eq_jmp, other_jmp in find_cases(FUNC_EA):
msg(
"{'addr' : 0x%X, 'caseVal' : 0x%X, 'cond' : '%s', 'eq_jmp' : %s, 'other_jmp' : 0x%X},"
% (addr, case_val, cond, eq_jmp, other_jmp)
)


if __name__ == "__main__":
main()

输出

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
{'addr' : 0x1B9CE0, 'caseVal' : 0x1674F175, 'cond' : 'GE', 'eq_jmp' : 0x1B9CFC, 'other_jmp' : 0x1B9CE8},
{'addr' : 0x1B9CE8, 'caseVal' : 0xF740EF3C, 'cond' : 'GE', 'eq_jmp' : 0x1B9D18, 'other_jmp' : 0x1B9CF0},
{'addr' : 0x1B9CF0, 'caseVal' : 0x8CF6E865, 'cond' : 'NE', 'eq_jmp' : 0x1B9CE0, 'other_jmp' : 0x1B9CF8},
{'addr' : 0x1B9CFC, 'caseVal' : 0x4C0FA2CE, 'cond' : 'GE', 'eq_jmp' : 0x1B9D2C, 'other_jmp' : 0x1B9D04},
{'addr' : 0x1B9D04, 'caseVal' : 0x3C777BC5, 'cond' : 'GE', 'eq_jmp' : 0x1B9D40, 'other_jmp' : 0x1B9D0C},
{'addr' : 0x1B9D0C, 'caseVal' : 0x1674F175, 'cond' : 'NE', 'eq_jmp' : 0x1B9CE0, 'other_jmp' : 0x1B9D14},
{'addr' : 0x1B9D18, 'caseVal' : 0x1042B5BC, 'cond' : 'GE', 'eq_jmp' : 0x1B9D48, 'other_jmp' : 0x1B9D20},
{'addr' : 0x1B9D20, 'caseVal' : 0xF740EF3C, 'cond' : 'NE', 'eq_jmp' : 0x1B9CE0, 'other_jmp' : 0x1B9D28},
{'addr' : 0x1B9D2C, 'caseVal' : 0x6C23C62F, 'cond' : 'GE', 'eq_jmp' : 0x1B9D50, 'other_jmp' : 0x1B9D34},
{'addr' : 0x1B9D34, 'caseVal' : 0x4C0FA2CE, 'cond' : 'NE', 'eq_jmp' : 0x1B9CE0, 'other_jmp' : 0x1B9D3C},
{'addr' : 0x1B9D40, 'caseVal' : 0x3C777BC5, 'cond' : 'NE', 'eq_jmp' : 0x1B9CE0, 'other_jmp' : 0x1B9D44},
{'addr' : 0x1B9D48, 'caseVal' : 0x1042B5BC, 'cond' : 'NE', 'eq_jmp' : 0x1B9CE0, 'other_jmp' : 0x1B9D4C},
{'addr' : 0x1B9D50, 'caseVal' : 0x6C23C62F, 'cond' : 'NE', 'eq_jmp' : 0x1B9CE0, 'other_jmp' : 0x1B9D54},
{'addr' : 0x1B9DA4, 'caseVal' : 0xF740EF3C, 'cond' : 'GE', 'eq_jmp' : 0x1B9F1C, 'other_jmp' : 0x1B9DAC},
{'addr' : 0x1B9DAC, 'caseVal' : 0x8CF6E865, 'cond' : 'EQ', 'eq_jmp' : 0x1B9D80, 'other_jmp' : 0x1B9DB4},
{'addr' : 0x1B9DB8, 'caseVal' : 0x4C0FA2CE, 'cond' : 'GE', 'eq_jmp' : 0x1B9E80, 'other_jmp' : 0x1B9DC0},
{'addr' : 0x1B9DC0, 'caseVal' : 0x3C777BC5, 'cond' : 'GE', 'eq_jmp' : 0x1B9E40, 'other_jmp' : 0x1B9DC8},
{'addr' : 0x1B9DC8, 'caseVal' : 0x1674F175, 'cond' : 'NE', 'eq_jmp' : 0x1B9F9C, 'other_jmp' : 0x1B9DD0},
{'addr' : 0x1B9E40, 'caseVal' : 0x3C777BC5, 'cond' : 'NE', 'eq_jmp' : 0x1B9F9C, 'other_jmp' : 0x1B9E44},
{'addr' : 0x1B9E80, 'caseVal' : 0x6C23C62F, 'cond' : 'LT', 'eq_jmp' : 0x1B9EDC, 'other_jmp' : 0x1B9E88},
{'addr' : 0x1B9E88, 'caseVal' : 0x6C23C62F, 'cond' : 'NE', 'eq_jmp' : 0x1B9F9C, 'other_jmp' : 0x1B9E8C},
{'addr' : 0x1B9EDC, 'caseVal' : 0x4C0FA2CE, 'cond' : 'NE', 'eq_jmp' : 0x1B9F9C, 'other_jmp' : 0x1B9EE4},
{'addr' : 0x1B9F14, 'caseVal' : 0xF740EF3C, 'cond' : 'LT', 'eq_jmp' : 0x1B9F94, 'other_jmp' : 0x1B9F1C},
{'addr' : 0x1B9F1C, 'caseVal' : 0x1042B5BC, 'cond' : 'GE', 'eq_jmp' : 0x1B9F70, 'other_jmp' : 0x1B9F24},
{'addr' : 0x1B9F24, 'caseVal' : 0xF740EF3C, 'cond' : 'NE', 'eq_jmp' : 0x1B9F9C, 'other_jmp' : 0x1B9F2C},
{'addr' : 0x1B9F70, 'caseVal' : 0x1042B5BC, 'cond' : 'NE', 'eq_jmp' : 0x1B9F9C, 'other_jmp' : 0x1B9F74},
{'addr' : 0x1B9F94, 'caseVal' : 0x8CF6E865, 'cond' : 'EQ', 'eq_jmp' : 0x1B9D58, 'other_jmp' : 0x1B9F9C},

再看基本块

基本块尾部将 case 值赋值到内存中,然后跳转到分发块进行分发。case 值则是通过 mov + movk 赋值的

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
import re

import ida_funcs
import ida_gdl
import ida_lines
import idc


FUNC_EA = 0x1B9C24
MASK32 = 0xFFFFFFFF
MAX_BACKTRACK = 0x120
DEF_MNEMS = {"mov", "movz", "movn", "movk", "add", "sub", "orr", "eor", "csel"}


def out(line):
print(line)


def clean(text):
return ida_lines.tag_remove(text or "").strip()


def mnem(ea):
return clean(idc.print_insn_mnem(ea)).lower()


def op(ea, n):
return clean(idc.print_operand(ea, n))


def norm_reg(reg):
reg = (reg or "").strip().lower().replace(",", "")
if reg.startswith("x") and reg[1:].isdigit():
return "w" + reg[1:]
if reg == "xzr":
return "wzr"
return reg


def is_reg(ea, n):
return idc.get_operand_type(ea, n) == idc.o_reg


def imm(ea, n):
if idc.get_operand_type(ea, n) in (idc.o_imm, idc.o_near, idc.o_far):
return idc.get_operand_value(ea, n) & MASK32
return None


def mov_shift(ea):
for n in range(1, 5):
text = op(ea, n)
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", text, re.I)
if m:
return int(m.group(1), 0)
line = clean(idc.generate_disasm_line(ea, 0))
m = re.search(r"lsl\s*#?\s*(0x[0-9a-f]+|\d+)", line, re.I)
return int(m.group(1), 0) if m else 0


def prev_head(ea, start):
p = idc.prev_head(ea, start)
return None if p == idc.BADADDR or p < start else p


def next_head(ea, end):
n = idc.next_head(ea, end)
return None if n == idc.BADADDR or n >= end else n


def apply_binop(name, a, b):
if a is None or b is None:
return None
if name == "add":
return (a + b) & MASK32
if name == "sub":
return (a - b) & MASK32
if name == "orr":
return (a | b) & MASK32
if name == "eor":
return (a ^ b) & MASK32
return None


def operand_value(ea, n, low, depth, seen):
if is_reg(ea, n):
reg = norm_reg(op(ea, n))
if reg in ("wzr", "xzr"):
return 0
return resolve_reg_value(reg, ea, low, depth + 1, seen)
return imm(ea, n)


def resolve_reg_value(reg, before_ea, low, depth=0, seen=None):
reg = norm_reg(reg)
if reg in ("wzr", "xzr"):
return 0
if seen is None:
seen = set()
key = (reg, before_ea)
if key in seen or depth > 48:
return None
seen.add(key)

ea = prev_head(before_ea, low)
while ea is not None and ea >= low:
name = mnem(ea)
dst = norm_reg(op(ea, 0))
if dst != reg or name not in DEF_MNEMS:
ea = prev_head(ea, low)
continue

if name in ("mov", "movz"):
value = imm(ea, 1)
if value is not None:
return (value << mov_shift(ea)) & MASK32
if is_reg(ea, 1):
return resolve_reg_value(op(ea, 1), ea, low, depth + 1, seen)
return None

if name == "movn":
value = imm(ea, 1)
return None if value is None else (~(value << mov_shift(ea))) & MASK32

if name == "movk":
value = imm(ea, 1)
if value is None:
return None
old = resolve_reg_value(reg, ea, low, depth + 1, seen)
if old is None:
old = 0
shift = mov_shift(ea)
mask = (0xFFFF << shift) & MASK32
return ((old & ~mask) | ((value & 0xFFFF) << shift)) & MASK32

if name in ("add", "sub", "orr", "eor"):
a = operand_value(ea, 1, low, depth, seen)
b = operand_value(ea, 2, low, depth, seen)
return apply_binop(name, a, b)

if name == "csel":
# The caller handles W8=CSEL specially so it can emit both successors.
return None

return None

return None


def find_last_def(reg, before_ea, low):
reg = norm_reg(reg)
ea = prev_head(before_ea, low)
while ea is not None and ea >= low:
if mnem(ea) in DEF_MNEMS and norm_reg(op(ea, 0)) == reg:
return ea
ea = prev_head(ea, low)
return None


def is_next_case_store(ea):
if mnem(ea) != "stur":
return False
if norm_reg(op(ea, 0)) != "w8":
return False
mem = op(ea, 1).replace(" ", "").lower()
return "x29" in mem and ("var_1c" in mem or "#-0x1c" in mem or "#-28" in mem)


def find_block_for_ea(func, ea):
for block in ida_gdl.FlowChart(func):
if block.start_ea <= ea < block.end_ea:
return block
return None


def is_terminal(ea):
name = mnem(ea)
return name == "ret" or name == "br" or name == "b" or name.startswith("b.")


def linear_block_start(func, ea):
cur = ea
prev = prev_head(cur, func.start_ea)
while prev is not None and prev >= func.start_ea:
if is_terminal(prev):
return cur
cur = prev
prev = prev_head(cur, func.start_ea)
return func.start_ea


def find_block_jump_after_store(func, store_ea):
ea = next_head(store_ea, func.end_ea)
while ea is not None and ea < func.end_ea:
if is_terminal(ea):
return ea
ea = next_head(ea, func.end_ea)
return None


def analyze_store(func, store_ea):
block = find_block_for_ea(func, store_ea)
if block is None:
return None

block_start = linear_block_start(func, store_ea)
jump_ea = find_block_jump_after_store(func, store_ea)
if jump_ea is None:
return None
search_low = max(func.start_ea, store_ea - MAX_BACKTRACK)
def_ea = find_last_def("w8", store_ea, search_low)
if def_ea is None:
return None

if mnem(def_ea) == "csel":
src_eq = norm_reg(op(def_ea, 1))
src_other = norm_reg(op(def_ea, 2))
cond = op(def_ea, 3).upper()
eq_val = resolve_reg_value(src_eq, def_ea, search_low)
other_val = resolve_reg_value(src_other, def_ea, search_low)
if eq_val is None or other_val is None:
return None
return (
"{'basicBlockAddr' : 0x%X, 'eqCaseVal' : 0x%X, 'otherCaseVal' : 0x%X, 'cond' : '%s'},"
% (jump_ea, eq_val, other_val, cond)
)

value = resolve_reg_value("w8", store_ea, search_low)
if value is None:
return None
return "{'basicBlockAddr' : 0x%X, 'nextCaseVal' : 0x%X}," % (jump_ea, value)


def main():
func = ida_funcs.get_func(FUNC_EA)
if func is None:
raise RuntimeError("function not found at 0x%X" % FUNC_EA)

ea = func.start_ea
while ea is not None and ea < func.end_ea:
if is_next_case_store(ea):
line = analyze_store(func, ea)
if line:
out(line)
ea = next_head(ea, func.end_ea)


if __name__ == "__main__":
main()

输出 g

1
2
3
4
5
6
{'basicBlockAddr' : 0x1B9DA0, 'eqCaseVal' : 0xF740EF3C, 'otherCaseVal' : 0x6C23C62F, 'cond' : 'EQ'},
{'basicBlockAddr' : 0x1B9E38, 'nextCaseVal' : 0x1042B5BC},
{'basicBlockAddr' : 0x1B9E78, 'nextCaseVal' : 0x1042B5BC},
{'basicBlockAddr' : 0x1B9ED4, 'eqCaseVal' : 0xF740EF3C, 'otherCaseVal' : 0x3C777BC5, 'cond' : 'EQ'},
{'basicBlockAddr' : 0x1B9F10, 'nextCaseVal' : 0x8CF6E865},
{'basicBlockAddr' : 0x1B9F68, 'eqCaseVal' : 0x1042B5BC, 'otherCaseVal' : 0x1674F175, 'cond' : 'EQ'},

有两个表之后,就可以依据跳转关系用 python 处理得到跳转关系

1
2
3
4
5
6
7
8
9
10
11
0x1B9C84 -> 0x1B9C9C
0x1B9CA0 -> 0x1B9CCC,
0x1B9CD0 -> 0x1B9CD4,
0x1B9CD8 -> 0x1B9EE4,

0x1B9DA0 EQ 0x1B9D28, B 0x1B9D54,
0x1B9E38 -> 0x1B9D4C,
0x1B9E78 -> 0x1B9D4C,
0x1B9ED4 EQ 0x1B9D28, B 0x1B9D44,
0x1B9F10 -> 0x1B9CF8,
0x1B9F68 EQ 0x1B9D4C, B 0x1B9D14,

patch 后效果如下

一些垃圾值依旧是控制流平坦化赋值遗留下的产物,觉得不干净也可以将赋值 case 指令都 nop 掉。

其他 fla 也与此类似,按照步骤进行处理即可

# 小结

混淆重要的不是如何去除,而是理解混淆的原理。了解原理之后自然就有方法去除了。

作为一个安卓蒟蒻,如果文章中有任何错误,欢迎各位大佬指正 (≧﹏ ≦),也欢迎各位大神和我交流不同的看法 orz