在工作中,多多少少会遇到花指令的情况,本系列文章我会慢慢更新,把我学习过程分享给大家,从简单的构造到如何结合手头的工具patch
洋文叫:Junk Code 脏指令 主要作用就是用来干扰,反汇编引擎(线形扫描&递归下降) 和 F5 增加逆向(PJ)难度的一种技术,能屏蔽大部分脚本小子
,这些指令CPU也不认识,执行到这种指令CPU直接会报错,如何不影响CPU正常执行又能干扰反汇编引擎的花指令呢
花指令的种类有很多,这里只讨论移动端或者说ARM平台,哪些团队在用花指令呢我的知识范围内发现的的商业级样本中,魔兽世界怀旧服macOS版
某泔水团
等等等等........ 大家可以后续补充。
这里为什么没有研究x86是第一因为我不会第二个原因是x86是非定长指令集,这会导致x86有那种双字节的花指令不适合线形扫描,要校验的太多了无法通过定长4字节的方式进行线性扫描。
ARM64指令集天生就是定长4字节,这里可能有人会杠ADRL x0, unk_1023d5501
8字节你怎么说,其实这是两条指令adrp x0, #0xc
add x0, x0, #0x501
行不更名坐不改姓:周樟寿
首先我要给梁总
道个歉我就是那个在群里口嗨,要举报你的沙雕虽然您删了,但是还是要道歉。我呢是一个失业在家找不到工作研究技术的老逼蹬。
example1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
_main:
0x0000000100003c84
FF8300D1 sub sp, sp,
#0x20
0x0000000100003c88
FD7B01A9 stp fp, lr, [sp,
#0x10]
0x0000000100003c8c
FD430091 add fp, sp,
#0x10
0x0000000100003c90
08008052
mov w8,
#0x0
0x0000000100003c94
FF0B00B9
str
wzr, [sp,
#0x8]
0x0000000100003c98
db
0xb1
;
'.'
0x0000000100003c99
db
0x7f
;
'.'
0x0000000100003c9a
db
0x39
;
'9'
0x0000000100003c9b
db
0x05
;
'.'
0x0000000100003c9c
db
0x4e
;
'N'
0x0000000100003c9d
db
0x61
;
'a'
0x0000000100003c9e
db
0xbc
;
'.'
0x0000000100003c9f
db
0x00
;
'.'
0x0000000100003ca0
29048052
mov w9,
#0x21
|
上边的样本是我自己构造的简单花指令样本,可以看到 0x100003c98-0x100003c9f 无法被反汇编引擎正常解析并标记为数据db,这个样本是在Hopper中解析的,在ida pro
呈现的是:DCB/DCW/DWD/DCQ 这里简单理解他们都是数据只是大小不同 B = byte,W= word (2bytes),D = dword(4bytes),Q = qword(8bytes)
QA:那问题来了数据不是有数据段么,怎么出现在代码段了?
DCB/DCW/DWD/DCQ 在ARM中表示定义数据
的指令,正常的代码经过编译器怎么能犯这种错误呢,问题就出在二进制工程师通过某种手段构造出来故意干扰反汇编引擎
和脚本小子
下边公布代码:
main.cpp
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
|
/
*
*
*
*
*
@author 周樟寿
*
/
#include <iostream>
/
*
*
*
*
*
__attribute__((always_inline)) 内联函数
*
@
return
*
/
static __attribute__((always_inline))
int
case1() {
#if __arm64__
__asm__(
"udf #0x5397FB1\n"
".long 87654321\n"
".long 12345678\n"
);
#endif
int
b
=
1
*
3
+
(
5
*
6
);
return
b;
}
int
main() {
int
c
=
case1();
std::cout << (c) << std::endl;
return
0
;
}
|
这个例子是无法正常运行执行的,因为cpu执行到了我们通过内联汇编构造的非法指令,这里使用了两种方式来构造非法指令
有了基础知识,我们先想办法修正这类这里使用ida python
进行修正
fix_example_junk_code.py
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
|
""
@author
周樟寿
""
import
ida_bytes
import
idautils
import
idc
arm64_nop
=
b
"\x1f\x20\x30\xd5"
if
__name__
=
=
'___main__'
:
print
(
"fix_example_junk_code.py begin....."
)
segments
=
idautils.Segments()
#获取所有段
for
set
in
segments:
#只处理 text 段 这个脚本支持 so 和 match-o
if
idc.get_segm_name(seg)
in
[
'__text'
,
'.text'
]
#开始
seg_start_ea
=
idc.get_segm_start(seg)
#结束
seg_end_ea
=
idc.get_segm_end(seg)
-
4
#临时变量
current_ea
=
seg_start_ea
while
current_ea <
=
seg_end_ea:
#指令大小对于非指令无效
item_size
=
idc.get_item_size(current_ea)
#是否是asm 指令判断
is_asm_code
=
ida_bytes.is_code(ida_bytes.get_flags(current_ea))
if
not
is_asm_code:
# patch nop
ida_bytes.patch_bytes(current_ea,arm64_nop)
current_ea
=
current_ea
+
4
else
:
current_ea
=
current_ea
+
item_size
print
(
"fix script end success ........"
)
|
第一章遗留下了一个问题就是,如何构造不影响cpu执行指令的花指令
这个小节需要解决的是让CPU正常执行并且还能干扰反汇编引擎
&F5
,先看例子讲解原理
case2
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
|
/
*
*
*
*
*
@author 周樟寿
*
/
#include <iostream>
/
*
*
*
*
*
__attribute__((always_inline)) 内联函数
*
@
return
*
/
static __attribute__((always_inline))
int
case2() {
#if __arm64__
__asm__(
"b #0x10\n"
"udf #0x5397FB1\n"
".long 87654321\n"
".long 12345678\n"
);
#endif
int
b
=
1
*
3
+
(
5
*
6
);
return
b;
}
int
main() {
int
c
=
case2();
std::cout << (c) << std::endl;
return
0
;
}
|
这个例子与第一个例子最大的不同就是,增加了一个B指令
来让CPU正常执行跳过我们精心构造的花指令,下面聊聊为什么用 B指令
和 为什么后边的立即数是 #0x10
B
指令 使用B指令是因为,B指令的寻址方式是相对寻址,相对于PC寄存器进行的,其次B是无条件跳转无需关心LR寄存器
立即数为什么是 0x10
这里先把转成10进制等于 16
这里要回顾一下PC寄存器的知识,PC
寄存器永远存储的是需要执行下一条指令的地址,这里就会衍生出来一个公式来计算具体跳多远,公式: 立即数=(填充指令数量+1) 4,套用上边例子最终结果 16=(3+1) 4
这种简单类型的修正思路就是,发现B指令,先计算出立即数,然后判断判断一条是否为指令集,然后轮训判断下去保存总指令大小,如果发现立即数=我们扫描的区域大小直接全nop
掉,
下一章我直接根据实际样本进行学习以及修正。
fix_case2.py
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
|
""
@author
周樟寿
""
import
ida_bytes
import
idautils
import
idc
arm64_nop
=
b
"\x1f\x20\x30\xd5"
def
get_range():
segments
=
idautils.Segments()
# 获取所有段
for
seg
in
segments:
# 只处理 text 段 这个脚本支持 so 和 match-o
if
idc.get_segm_name(seg)
in
[
'__text'
,
'.text'
]:
# 开始
seg_start_ea
=
idc.get_segm_start(seg)
# 结束
seg_end_ea
=
idc.get_segm_end(seg)
-
4
break
return
seg_start_ea, seg_end_ea
if
__name__
=
=
'___main__'
:
print
(
"fix_example_junk_code.py begin....."
)
# 开始
seg_start_ea, seg_end_ea
=
get_range()
# 临时变量
current_ea
=
seg_start_ea
while
current_ea <
=
seg_end_ea:
# 指令大小对于非指令无效
item_size
=
idc.get_item_size(current_ea)
# 是否是asm 指令判断
is_asm_code
=
ida_bytes.is_code(ida_bytes.get_flags(current_ea))
if
not
is_asm_code:
current_ea
=
current_ea
+
4
else
:
# 判断是否是指令B
if
idc.print_insn_mnem(current_ea)
in
[
'b'
,
'B'
]:
# 获取B 后边的目标地址 立即数=目标地址-当前地址
b_imm
=
idc.get_operand_value(current_ea,
0
)
-
current_ea
# 判断指令B下边是否是指令不是指令开始非指令区域大小
if
not
ida_bytes.is_code(ida_bytes.get_flags(current_ea
+
4
)):
data_size
=
0
begin_ea
=
current_ea
+
4
while
True
:
if
not
ida_bytes.is_code(ida_bytes.get_flags(begin_ea)):
data_size
=
data_size
+
4
begin_ea
=
begin_ea
+
4
else
:
break
if
data_size
=
=
b_imm:
# todo not
print
(
"nop fix"
)
current_ea
=
current_ea
+
item_size
print
(
"fix script end success ........"
)
|
看了这么多代码我相信老鸟都看烦了,不拖拉直接把基础剩余的花指令套路大概过一遍直接上真实样本分享我的修正思路,大家有更好的思路也可以交流。
花指令的类型还有以下几种:
还有很多精心构造的下边我给出具体构造的例子,不会再写具体修正脚本大家可以自己试着去修正一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
/
*
*
*
*
*
@author 周樟寿
*
/
int
bcf(
int
a) {
return
(a
+
a);
}
static __attribute__((always_inline))
int
case3() {
int
a
=
3
;
if
(bcf(a) !
=
0
) {
#if __arm64__
__asm__(
"b 0xc\n"
"udf #0x5397FB1\n"
".long 87654321\n"
".long 12345678\n"
);
#endif
}
int
b
=
1
*
3
+
(
5
*
6
);
return
b;
}
|
这只是简单的例子虚假控制流按我这个写法会被编译器优化掉实际情况远比这复杂的多,仅供参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static __attribute__((always_inline))
int
case4() {
int
a
=
3
;
if
(bcf(a) !
=
0
) {
#if __arm64__
__asm__(
"b 0xc\n"
"add sp,sp,#0x100\n"
"add sp,sp,#0x100\n"
);
#endif
}
int
b
=
1
*
3
+
(
5
*
6
);
return
b;
}
|
例子和构造样本就先简单到这里我们直接结合,真实样本样本学习其中的套路
看开水团系列的应用我是纯抱着学习态度去的,因为平时也用但是觉得卡卡的偶尔还非常热抱着好奇心的态度去学习,你别说有点东西下边开始分享我的学习过程,尽量分享的细致一点和我的思路,这个学习记录没有参考别人的修正方案按自己的理解进行修正,称不上完美但是可以f5了看起来清爽一点,如果哪里有脚本上的优化大家可以交流集思广益。
样本总结
包含的种类非常多我只修正了我关心的部分会讲解修正过程以及他们之间的关系,开水团的花指令是有关联的属于嵌套关系
而且还夹杂了DCQ这种数据,其中BR x8
这种动态跳转我没有仔细去看不在我的修正范围
搜索方向
安卓从ida pro
的export 窗口看 JNI_onLoad 开始看起就能发现花指令的存在了,一直跟着向下即可,同Group产品我也都看了双平台的花指令套路是一致的,大家不要去看iOS端修起来很费时间,如果学习尽量看安卓下边正式开始吧。
[x] 样本1之 利用 x30 (LR) 寄存器 + RET 强制 停止函数
在点进去 JNI_onLoad
就会看到如下
1
2
3
4
5
6
7
|
.text:
14580
E0
7B
3E
A9 STP X0, X30,[SP,
#var_20]
.text:
14584
01
00
00
94
BL sub_14588
.text:
14584
; End of function JNI_OnLoad
sub_14588
.text:
14588
60
00
00
10
ADR X0, loc_14594
.text:
1458C
FE
03
00
AA MOV X30, X0
.text:
14590
C0
03
5F
D6 RET
|
这个样本很有意思,第一利用了 ADR
地址无关性把目标地址放进了 X0
寄存器,第二利用 X30寄存器也就是LR寄存器直接跳了过去,RET其实等同于 mov pc,lr
那如何修正呢,先人工修正一下看看
修正.asm
1
2
3
4
5
6
7
|
.text:
14580
E0
7B
3E
A9 STP X0, X30,[SP,
#var_20]
.text:
14584
01
00
00
94
BL sub_14588
.text:
14584
; End of function JNI_OnLoad
sub_14588
.text:
14588
60
00
00
10
nop
.text:
1458C
FE
03
00
AA nop
.text:
14590
C0
03
5F
D6 B loc_14594
|
这里手工修正,修正了3条指令核心就在 RET
修正 B 目标地址
,这里其实有朋友会说为什么不在 BL sub_14588
这里修正 B loc_14594
因为实际情况远比想象的复杂得多,这个ret
中间的位置可能会出现 sp栈相关操作要不要保留?不清楚干嘛的都要保留,我直接放出修正脚本写的比较low大家别喷有些没找到函数,我就直接手工操作的。
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
|
/
*
*
*
*
*
@author 周樟寿
*
/
import
ida_bytes
import
idautils
import
idc
import
re
from
keystone
import
*
if
__name__
=
=
'__main__'
:
"""
初始化汇编 引擎 keystone
"""
ks
=
Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
arm64_nop
=
b
"\x1f\x20\x03\xd5"
segments
=
idautils.Segments()
for
seg
in
segments:
"""这里只处理 代码段"""
x_30_ret_total
=
0
if
idc.get_segm_name(seg)
in
[
"__text"
,
".text"
]:
"""获取段信息"""
seg_name
=
idc.get_segm_name(seg)
seg_start_addr
=
idc.get_segm_start(seg)
seg_end_addr
=
idc.get_segm_end(seg)
-
4
seg_size
=
seg_end_addr
-
seg_start_addr
current_ea
=
seg_start_addr
while
current_ea <
=
seg_end_addr:
item_size
=
idc.get_item_size(current_ea)
is_asm_code
=
ida_bytes.is_code(ida_bytes.get_flags(current_ea))
if
is_asm_code:
current_mnem
=
idc.print_insn_mnem(current_ea)
"""
优化修复逻辑 里边有很多地方都是 通过地址-4 这种计算方式来判断的,遇到了特殊情况就无法搜索
需要向上逐4 字节搜索
"""
new_fix
=
True
if
new_fix:
"""
这是原始模板,但是存在中间穿插 栈平衡相关得指令无法使用 精确 -4 判断 第一个操作数是 x30
和 -8 判断 mnem 是 adr
adr x0 ,#0xdc
mov x30 x0
ret
这里优化搜索规则 当前是RET 指令向上搜索, 如果向上搜索 4-8 字节 发现有操作 X30 (LR) 寄存器 就标
标记好了,x30 的 操作位置,继续向上搜索 查找ADR 指令 找到要跳转 得目标
"""
if
current_mnem
=
=
'RET'
:
if
((idc.print_operand(current_ea
-
4
,
0
)
=
=
'X30'
)
and
(
idc.print_insn_mnem(current_ea
-
4
)
=
=
'MOV'
)) \
or
\
(idc.print_operand(current_ea
-
8
,
0
)
=
=
'X30'
and
idc.print_insn_mnem(
current_ea
-
8
)
=
=
'MOV'
):
# (
# (idc.print_operand(current_ea - 4, 0) == 'X30') and (
# idc.print_insn_mnem(current_ea - 4) == 'MOV')
# ):
if
idc.print_insn_mnem(current_ea
-
4
)
in
[
'ADD'
,
'MOV'
]:
print
(
f
'current {hex(current_ea)} x30_ret sub_4={idc.print_insn_mnem(current_ea - 4)}'
)
"""统计"""
x_30_mov
=
0
x_30_adr
=
0
"""
nop ret
"""
# ida_bytes.patch_bytes(current_ea, arm64_nop)
location_addr
=
0
if
idc.print_insn_mnem(current_ea
-
4
)
=
=
"MOV"
:
"""
nop mov
"""
# ida_bytes.patch_bytes(current_ea, arm64_nop)
# ida_bytes.patch_bytes(current_ea - 4, arm64_nop)
"""
用于确定具体跳转目标得指令位置
"""
location_addr
=
current_ea
-
8
elif
idc.print_insn_mnem(current_ea
-
4
)
=
=
"ADD"
:
# ida_bytes.patch_bytes(current_ea - 8, arm64_nop)
location_addr
=
current_ea
-
12
else
:
print
(f
'未知:{hex(current_ea)}'
)
if
location_addr !
=
0
:
try
:
b_number
=
int
(
re.search(r
'[0-9A-F]+'
, idc.print_operand(location_addr,
1
)).group(),
16
)
path_asm
=
f
'b #{hex(b_number - current_ea)}'
print
(
f
' address:{hex(location_addr)} operand:{hex(b_number - current_ea)} asm:{path_asm}'
)
encoding, count
=
ks.asm(path_asm)
patch_byte
=
bytes([
int
(i)
for
i
in
encoding])
print
(f
"------------------->{path_asm} = ["
, end
=
'')
for
i
in
encoding:
print
(
"%02x "
%
i, end
=
'')
print
(
']'
)
"""
修正跳转
"""
ida_bytes.patch_bytes(location_addr, arm64_nop)
"""
修正ret
"""
ida_bytes.patch_bytes(current_ea, patch_byte)
if
idc.print_insn_mnem(current_ea
-
4
)
=
=
"MOV"
:
"""
nop mov
"""
# ida_bytes.patch_bytes(current_ea, arm64_nop)
ida_bytes.patch_bytes(current_ea
-
4
, arm64_nop)
elif
idc.print_insn_mnem(current_ea
-
4
)
=
=
"ADD"
:
ida_bytes.patch_bytes(current_ea
-
8
, arm64_nop)
except
Exception as e:
print
(f
'出错地址:{hex(location_addr)}'
)
#
# asm_number = b_number - location_addr
#
# path_asm = f'b #{b_number}'
# encoding, count = ks.asm(path_asm)
# patch_byte = bytes([int(i) for i in encoding])
# print(f"------------------->{path_asm} = [", end='')
# for i in encoding:
# print("%02x " % i, end='')
# print(']')
#
# ida_bytes.patch_bytes(location_addr, patch_byte)
x_30_ret_total
=
x_30_ret_total
+
1
if
not
new_fix:
"""如果之 这种花指令跳转规则 """
if
(current_mnem
=
=
'RET'
)
and
(idc.print_operand(current_ea
-
4
,
0
)
=
=
'X30'
)
and
(
idc.print_operand(current_ea
-
4
,
1
)
in
[
'X0'
,
'W0'
]):
"""当前指令-8"""
sub_8_mnem
=
idc.print_insn_mnem(current_ea
-
8
)
sub_8_imm
=
0
b_imm
=
0
if
sub_8_mnem
=
=
"ADR"
:
has_patch
=
False
sub_8_imm
=
idc.print_operand(current_ea
-
8
,
1
)
sub_8_imm
=
int
(re.search(r
'[0-9A-F]+'
, sub_8_imm).group(),
16
)
"""
计算地址
立即数 = 目标地址-当前地址
"""
b_imm
=
sub_8_imm
-
(current_ea
-
8
)
if
has_patch:
ida_bytes.patch_bytes(current_ea, arm64_nop)
ida_bytes.patch_bytes(current_ea
-
4
, arm64_nop)
path_asm
=
f
'bl #{b_imm}'
encoding, count
=
ks.asm(path_asm)
patch_byte
=
bytes([
int
(i)
for
i
in
encoding])
print
(f
"------------------->{path_asm} = ["
, end
=
'')
for
i
in
encoding:
print
(
"%02x "
%
i, end
=
'')
print
(
']'
)
if
has_patch:
ida_bytes.patch_bytes(current_ea
-
8
, patch_byte)
"""统计"""
x_30_ret_total
=
x_30_ret_total
+
1
print
(
f
'address:{hex(current_ea)} sub_8_mnem[{sub_8_mnem}] sub_8_imm={hex(sub_8_imm)} b_offset:{hex(b_imm)}'
)
"""skip"""
current_ea
=
current_ea
+
item_size
print
(f
"total all ret30 total:{x_30_ret_total}"
)
|
整体脚本实现相对来说比较啰嗦,我讲究的是能用就行剩下的交给GPT帮我优化,脚本包含我刚开始的修正代码,当时修正的是错误的大家看看就好了。
[x] 第二种花指令,是隐藏真实跳转地址,并交给中央转发器统一分发
以前没见过就是觉得非常厉害大概的跳转思路设计的很精妙,这也导致了我刚开始无脑NOP
陷入了痛苦深渊,直到写这篇文章的时候有开源项目实现过这种隐藏真实跳转的 ,写这篇文章的时候说实话没有看过他的核心思想就是根据样本学习和动态调试,一步一步扣出来的,下边结合样本和具体修正思路来看比较好,样本种类很多大家慢慢看别急、别急、
样本1
1
2
3
4
5
6
7
8
9
10
11
|
.text:
14594
E0
7B
7E
A9 LDP X0, X30, [SP,
#-0x20]
.text:
14598
E0
7B
3B
A9 STP X0, X30, [SP,
#-0x50]
.text:
1459C
20
00
80
D2 MOV X0,
#1
.text:
145A0
02
00
00
14
B loc_145A8
.text:
145A4
06
00
00
00
DCD
6
.text:
145A8
15
02
00
94
BL sub_14DFC
.text:
145AC
14
00
00
00
DCD
0x14
.text:
145B0
30
00
00
00
DCD
0x30
.text:
145B4
8C
00
00
00
DCD
0x8C
.text:
145B8
B8
00
00
00
DCD
0xB8
.text:
145BC
1F
20
03
D5 DCD
0xD503201F
|
1
2
3
4
5
|
.text:
14DFC
E0
07
BF A9 STP X0, X1, [SP,
#var_10]!
.text:
14E00
C0
5B
60
B8 LDR W0, [X30,W0,UXTW
#2]
.text:
14E04
DE
43
20
8B
ADD X30, X30, W0,UXTW
.text:
14E08
E0
07
C1 A8 LDP X0, X1, [SP
+
0x10
+
var_10],
#0x10
.text:
14E0C
C0
03
5F
D6 RET
|
这要结合中央转发器来看,先说中央转发器都干了什么里边通篇就只做了一个运算操作,最终跳出到目标地址依然利用了 RET X30 的特性这里不过多介绍,核心突出的就是计算和跳转不要关心STP
和LDP
公式是这样,读懂需要看上下文刚开始我也很懵,最后调试几遍就发现了规律,首先看x30
与x0
的值是这么来的计算一遍看看。
先看X30(LR)已知条件是LR寄存器永远指向下一条要执行的指令地址拿样本1中来看,当前X30=0x145AC
,为什么因为调用中央转发器用的是BL
有返回的跳转
再看x0
的值,向上溯源调用中央转发器前就有一条MOV X0, #1
这个#1是什么,这里可以理解为跳转编号,大家可以找到中央转发器看一下XREF
引用每个调用前都会有 X0或者W0的立即数赋值操作。
大家要注意具有迷惑性的 0x145A4 DCD 0x6
实际样本中还有一种 x0 赋值方式就是LDR w0,0xc
这并不是直接把0xc赋值给 w0,LDR是基于 PC寄存器的一种地址无关性的读取操作,说人话的意思就是 加载 pc寄存器地址+0xc 存放的内容放到 w0寄存器中
具体会在脚本中体现因为要考虑到
1
2
3
4
5
6
7
8
9
|
X30
=
0x145AC
+
Load(
0x145AC
+
(
1
<<
2
))
#这一步只是地址计算
0x145AC
+
0x4
=
0x145B0
#加载这个地址的值
Load(
0x145B0
)
=
0x30
#实际得出的地址
0x145AC
+
0x30
=
0x145DC
|
核心思想与总结
[x] 利用中央转发起进行x30 条转,配合花指令存储偏移传递不同编号进行跳转,干扰编译器
脚本小子
无脑NOP,让花指令数据变得有意义实际修正脚本也是写了非常多,还有很多情况需要考虑还好我都写了中文注释
find_中央转发器.py
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
|
/
*
*
*
*
*
@author 周樟寿
*
/
import
ida_bytes
import
idautils
import
idc
if
__name__
=
=
'__main__'
:
segments
=
idautils.Segments()
for
seg
in
segments:
"""这里只处理 代码段"""
if
idc.get_segm_name(seg)
in
[
"__text"
,
".text"
]:
seg_name
=
idc.get_segm_name(seg)
seg_start_addr
=
idc.get_segm_start(seg)
seg_end_addr
=
idc.get_segm_end(seg)
-
4
seg_size
=
seg_end_addr
-
seg_start_addr
print
(
f
's_name:{seg_name} s_start:{hex(seg_start_addr)} s_end:{hex(seg_end_addr)} s_size:{seg_size} has_align:{seg_size % 4 == 0}'
)
current_ea
=
seg_start_addr
while
current_ea <
=
seg_end_addr:
"""判断是不是 ASM"""
is_asm_code
=
ida_bytes.is_code(ida_bytes.get_flags(current_ea))
""" 获取长度 """
item_size
=
idc.get_item_size(current_ea)
if
not
is_asm_code:
create_result
=
ida_bytes.create_dword(current_ea,
4
,
True
)
current_ea
=
current_ea
+
4
else
:
c_asm
=
idc.GetDisasm(current_ea)
if
c_asm
=
=
"ADD X30, X30, W0,UXTW"
:
print
(f
'全局跳转.....address:{hex(current_ea)} asm:{c_asm}'
)
current_ea
=
current_ea
+
item_size
print
(
'format script success ........'
)
|
修正中央跳转.py
这个脚本的修正思路,通过中央转发器的 xref 引用进行向上修复,前提是通过脚本找到,我贴心的为大家打印了日志和修正开关,还有对跳转位置写了备注
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
|
import
struct
import
ida_bytes
import
idautils
import
idc
import
re
from
keystone
import
*
def
op_convert(x):
if
x.startswith(
"0x"
):
return
int
(x,
16
)
else
:
return
int
(x)
"""
此脚本应用于 特殊花指令跳转........
"""
if
__name__
=
=
'__main__'
:
arm64_nop
=
b
"\x1f\x20\x03\xd5"
"""
初始化汇编 引擎 keystone
"""
ks
=
Ks(KS_ARCH_ARM64, KS_MODE_LITTLE_ENDIAN)
"""
这里是经过分析 找到得全局分发器入口地址
"""
# redirects = [0x1065FEBD4, 0x106D959F0]
redirects
=
[
0x14DFC
]
for
redirect
in
redirects:
for
xref
in
idautils.XrefsTo(redirect,
0
):
print
(
'============================================================================'
)
print
(xref.
type
, idautils.XrefTypeName(xref.
type
),
'from'
,
hex
(xref.frm),
'to'
,
hex
(xref.to))
redirect_base
=
xref.frm
+
4
print
(f
'redirect base:{hex(redirect_base)}'
)
for
a_xref
in
idautils.XrefsTo(xref.frm,
0
):
"""获得 x0 后边得立即数"""
op1
=
idc.print_operand(a_xref.frm
-
4
,
1
).replace(
"="
, "
").replace("
#", "")
"""转换 数字"""
xx_number
=
op_convert(op1)
"""计算偏移"""
offset
=
xx_number <<
2
f_bytes
=
ida_bytes.get_bytes(redirect_base
+
offset,
4
)
n_tuple
=
struct.unpack(
'<I'
, f_bytes)
hex_data
=
[
hex
(num)
for
num
in
n_tuple][
0
]
index_arr
=
redirect_base
+
int
(hex_data,
16
)
"""添加备注 """
ida_bytes.set_cmt(a_xref.frm, f
"current redirect:{hex(index_arr)}"
,
False
)
print
(
f
"--[{idc.print_insn_mnem(a_xref.frm - 4)}-{idc.print_insn_mnem(a_xref.frm - 8)}]---------[{xx_number}-{offset}]-----to:[{hex(index_arr)}]-------imm[{hex(index_arr - a_xref.frm)}]---->"
,
a_xref.
type
, idautils.XrefTypeName(a_xref.
type
),
'from'
,
hex
(a_xref.frm),
'to'
,
hex
(a_xref.to))
"""
开始修正这些指令
计算方式 = 实际目标地址-当前地址 = 立即数
b 所得立即数
"""
has_patch
=
True
if
has_patch:
patch_imm
=
hex
(index_arr
-
a_xref.frm)
path_asm
=
f
'b #{patch_imm}'
encoding, count
=
ks.asm(path_asm)
patch_byte
=
bytes([
int
(i)
for
i
in
encoding])
print
(f
"------------------->{path_asm} = ["
, end
=
'')
for
i
in
encoding:
print
(
"%02x "
%
i, end
=
'')
print
(
']'
)
ida_bytes.patch_bytes(a_xref.frm, patch_byte)
"""
patch 完 这些指令 要把 操作 x0 和 w0 这种指令也 nop掉 向上 搜索
"""
if
has_patch:
sub_4
=
a_xref.frm
-
4
sub_8
=
a_xref.frm
-
8
if
(idc.print_insn_mnem(sub_4)
in
[
"LDR"
,
"MOV"
])
and
idc.print_operand(sub_4,
0
)
in
[
"X0"
,
"W0"
]:
ida_bytes.patch_bytes(sub_4, arm64_nop)
if
(idc.print_insn_mnem(sub_8)
in
[
"LDR"
,
"MOV"
])
and
idc.print_operand(sub_8,
0
)
in
[
"X0"
,
"W0"
]:
ida_bytes.patch_bytes(sub_8, arm64_nop)
print
(
'============================================================================\n'
)
print
(
'xref fix script sucess.......'
)
|
我只是分享出来我的学习过程,顺便吐槽一下 开水团 Group
的APP很卡,还有就是我在不同意隐私条款的情况下依然在抽样我的设备信息,具体条款我也看了简直太流氓了。其次就是有那么点不单纯窥探,这些脚本并不能够完全修正但F5但是可以不致于抓瞎操作,当前这个脚本适用于 安卓和iOS端开水团group
的产品,后边我会把修正字符串加密的脚本也放出,兄弟团大概率会把他们的编译器升级,把具体跳转地址也加密了,大家有一起学习iOS&Android逆向的朋友可以一起交流
todo 因为本次教程只是抛砖引玉,并不是完美修复与完美f5,还要修正是样本中间接二次跳转还是会干扰反汇编引擎,中间有很多栈相关的操作其实在修正后都是无用的,我还在慢慢手动Patch去二级跳转带来的反汇编引擎JUMPOUT,如果要让IDA完美解析需要把中间二级跳转与无用栈指令全都干掉
更多【看开水团 与 ARM 花指令构造与静态Patch】相关视频教程:www.yxfzedu.com