样本名shansong
,某bang的壳
本文主要是分享在逆向中遇到的对抗以及处理办法
010editor打开libDexHelper.so
一眼假的section
操作系统加载并不关心section,先直接删除掉,后面修复dynamic后再重新生成一份
一眼假的dynamic
dynamic除了vaddr
,其他成员都是可以伪造欺骗的,因为操作系统也只关心dynamic的vaddr
通过分析PT_LOAD的信息,即RVA
和FA
的转换,对dynamic的file_offset进行简单修复
跑工具重新生成了一份section
后,ida已经可以正常的解析so了
简单看了一下init_arrary
和JNI_OnLoad
方法,都是被加密的
猜测解密方法在.init
方法中
暂时不去关心解密算法的实现,等.init
方法运行结束后,直接dump内存,可以看到数据已经解密完成
dump内存可以写ida脚本,也可以直接使用dd
来dump(注意先要使用ida把app挂起,壳对/proc/self/mem
的读写做了监控)
类似于这样
实际是ida对 switch-case的识别失败
解决办法就是帮助ida正确识别 switch-case,具体可以参考
正确修复后,ida也就能正常反编译了
代码的正常流程被switch-case
打乱了
简单分析了一下汇编的代码结构,可以发现直接跳转到分发块的case块,会直接写死下一个要执行的case块
于是可以通过计算获取到目标case块的地址,将case块跳回分发块,改成直接跳转到对应的case块的地址
对于跳回def块的case块,则会通过条件执行来决定下一个要执行的case块
同样可以计算出不同条件所要跳转的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
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
|
import
idautils
import
idc
import
idaapi
from
keystone
import
*
ks
=
keystone.Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN)
# 跳转表的信息,根据switch-case表修改
jump_table
=
0x1154B4
element_sz
=
2
element_base
=
0x3B348
element_shift
=
2
# def块 和 分发块,根据switch-case表修改
def_block
=
0x3B330
jump_block
=
0x3B338
def_init_block
=
-
1
# input reg switch,根据switch-case表修改
reg_base
=
129
reg_switch
=
reg_base
+
0
# 当前处理的函数,根据switch-case表修改
func_addr
=
0x38330
f_blocks
=
idaapi.FlowChart(idaapi.get_func(func_addr), flags
=
idaapi.FC_PREDS)
def
get_code_refs_to_list(ea):
result
=
list
(idautils.CodeRefsTo(ea,
True
))
return
result
def
get_block(addr, f_blocks):
for
block
in
f_blocks:
if
block.start_ea <
=
addr
and
addr <
=
block.end_ea
-
4
:
return
block
return
None
def
get_next_case(start_ea, end_ea):
next_case
=
-
1
ea
=
start_ea
while
ea < end_ea:
mnem
=
idc.ida_ua.ua_mnem(ea)
if
mnem
=
=
'MOV'
:
op1
=
idc.get_operand_value(ea,
0
)
op2
=
idc.get_operand_value(ea,
1
)
op2_type
=
idc.get_operand_type(ea,
1
)
if
op1
=
=
reg_switch
and
op2_type
=
=
idc.o_imm:
next_case
=
op2
ea
=
ea
+
4
return
next_case
def
get_cond(ea):
cond
=
None
disasm
=
idc.GetDisasm(ea)
if
disasm.find(
'LT'
) !
=
-
1
:
cond
=
'blt'
elif
disasm.find(
'EQ'
) !
=
-
1
:
cond
=
'beq'
elif
disasm.find(
'CC'
) !
=
-
1
:
cond
=
'bcc'
elif
disasm.find(
'GT'
) !
=
-
1
:
cond
=
'bgt'
elif
disasm.find(
'NE'
) !
=
-
1
:
cond
=
'bne'
elif
disasm.find(
'GE'
) !
=
-
1
:
cond
=
'bge'
elif
disasm.find(
'HI'
) !
=
-
1
:
cond
=
'bhi'
elif
disasm.find(
'LE'
) !
=
-
1
:
cond
=
'ble'
else
:
print
(
'unknow cond:0x%x'
%
ea)
return
cond
def
get_cond_next_case(start_ea, end_ea):
cond_case
=
-
1
uncond_case
=
-
1
cond_reg
=
idc.get_operand_value(end_ea
-
8
,
1
)
uncond_reg
=
idc.get_operand_value(end_ea
-
8
,
2
)
ea
=
start_ea
while
ea < end_ea:
mnem
=
idc.ida_ua.ua_mnem(ea)
if
mnem
=
=
'MOV'
:
op1
=
idc.get_operand_value(ea,
0
)
op2
=
idc.get_operand_value(ea,
1
)
op2_type
=
idc.get_operand_type(ea,
1
)
if
op1
=
=
cond_reg
and
op2_type
=
=
idc.o_imm:
cond_case
=
op2
if
op1
=
=
uncond_reg
and
op2_type
=
=
idc.o_imm:
uncond_case
=
op2
ea
=
ea
+
4
# 部分case值的初始化,在init块中,不在对应的case块
if
cond_case
=
=
-
1
or
uncond_case
=
=
-
1
:
#print('def_init_block 0x%x' % def_init_block)
block
=
get_block(def_init_block, f_blocks)
ea
=
block.start_ea
end_ea
=
block.end_ea
cond_flag
=
False
uncond_flag
=
False
if
cond_case
=
=
-
1
:
cond_flag
=
True
if
uncond_case
=
=
-
1
:
uncond_flag
=
True
while
ea < end_ea:
mnem
=
idc.ida_ua.ua_mnem(ea)
if
mnem
=
=
'MOV'
:
op1
=
idc.get_operand_value(ea,
0
)
op2
=
idc.get_operand_value(ea,
1
)
op2_type
=
idc.get_operand_type(ea,
1
)
if
cond_flag:
if
op1
=
=
cond_reg
and
op2_type
=
=
idc.o_imm:
cond_case
=
op2
if
uncond_flag:
if
op1
=
=
uncond_reg
and
op2_type
=
=
idc.o_imm:
uncond_case
=
op2
ea
=
ea
+
4
if
cond_reg
=
=
160
:
cond_case
=
0
elif
uncond_reg
=
=
160
:
uncond_case
=
0
return
cond_case, uncond_case
def
do_patch(ea, opcode, src, dst):
jump_offset
=
" ({:d})"
.
format
(dst
-
src)
repair_opcode
=
opcode
+
jump_offset
#print(repair_opcode)
encoding, count
=
ks.asm(repair_opcode)
idaapi.patch_byte(ea, encoding[
0
])
idaapi.patch_byte(ea
+
1
, encoding[
1
])
idaapi.patch_byte(ea
+
2
, encoding[
2
])
idaapi.patch_byte(ea
+
3
, encoding[
3
])
jump_block_list
=
get_code_refs_to_list(jump_block)
jump_def_list
=
get_code_refs_to_list(def_block)
def
hex_to_dec(hex_str):
#print(hex_str)
#print(hex_str[0])
# 把16进制字符串转成带符号10进制
if
hex_str[
0
]
in
'0123456789'
:
dec_data
=
int
(hex_str,
16
)
else
:
# 负数算法
width
=
32
# 16进制数所占位数
d
=
'FFFF'
+
hex_str
dec_data
=
int
(d,
16
)
if
dec_data >
2
*
*
(width
-
1
)
-
1
:
dec_data
=
2
*
*
width
-
dec_data
dec_data
=
0
-
dec_data
return
dec_data
def
do_B_block(addr, cond):
block
=
get_block(addr, f_blocks)
if
block
is
None
:
return
next_case
=
get_next_case(block.start_ea, block.end_ea)
if
next_case
=
=
-
1
:
return
if
element_sz
=
=
1
:
case_data
=
idc.get_wide_byte(jump_table
+
next_case)
if
case_data >
0x7f
:
case_data
=
hex_to_dec(
hex
(case_data)[
2
:])
jmp_off
=
case_data
*
(
2
*
element_shift)
jmp_addr
=
jmp_off
+
element_base
elif
element_sz
=
=
2
:
case_data
=
idc.get_wide_word(jump_table
+
next_case
*
2
)
if
case_data >
0x7fff
:
case_data
=
hex_to_dec(
hex
(case_data)[
2
:])
jmp_off
=
case_data
*
(
2
*
element_shift)
jmp_addr
=
jmp_off
+
element_base
print
(
'jump_block_list->addr: 0x%x, next_case: %d, jmp_addr: 0x%x'
%
(addr, next_case, jmp_addr))
if
cond
=
=
'cbnz'
:
reg_cmp
=
idc.get_operand_value(addr
-
4
,
0
)
cond
=
"cbnz x{:d}, "
.
format
(reg_cmp
-
reg_base)
elif
cond
=
=
'cbz'
:
reg_cmp
=
idc.get_operand_value(addr
-
4
,
0
)
cond
=
"cbz x{:d}, "
.
format
(reg_cmp
-
reg_base)
do_patch(addr, cond, addr, jmp_addr)
# 处理 jump_block
for
addr
in
jump_block_list:
mnem
=
idc.ida_ua.ua_mnem(addr)
if
mnem
=
=
'B'
:
do_B_block(addr,
'b'
)
elif
mnem
=
=
'TBZ'
:
do_B_block(addr,
'b'
)
elif
mnem
=
=
'CBNZ'
:
do_B_block(addr,
'cbnz'
)
elif
mnem
=
=
'CBZ'
:
do_B_block(addr,
'cbz'
)
else
:
print
(
'unknow jump_block:0x%x'
%
addr)
def
do_cond_block(addr, ins):
cond
=
get_cond(addr
-
4
)
if
cond
is
None
:
print
(
'unkown cond 0x%x'
%
addr)
return
block
=
get_block(addr, f_blocks)
if
block
is
None
:
return
cond_case, uncond_case
=
get_cond_next_case(block.start_ea, block.end_ea)
if
cond_case
=
=
-
1
or
uncond_case
=
=
-
1
:
return
if
mnem
=
=
'CSINC'
:
uncond_case
=
uncond_case
+
1
cond_jmp_addr
=
-
1
uncond_jmp_addr
=
-
1
if
element_sz
=
=
1
:
case_data
=
idc.get_wide_byte(jump_table
+
cond_case)
if
case_data >
0x7f
:
case_data
=
hex_to_dec(
hex
(case_data)[
2
:])
jmp_off
=
case_data
*
(
2
*
element_shift)
cond_jmp_addr
=
jmp_off
+
element_base
case_data
=
idc.get_wide_byte(jump_table
+
uncond_case)
if
case_data >
0x7f
:
case_data
=
hex_to_dec(
hex
(case_data)[
2
:])
jmp_off
=
case_data
*
(
2
*
element_shift)
uncond_jmp_addr
=
jmp_off
+
element_base
elif
element_sz
=
=
2
:
case_data
=
idc.get_wide_word(jump_table
+
cond_case
*
2
)
if
case_data >
0x7fff
:
case_data
=
hex_to_dec(
hex
(case_data)[
2
:])
jmp_off
=
case_data
*
(
2
*
element_shift)
cond_jmp_addr
=
jmp_off
+
element_base
case_data
=
idc.get_wide_word(jump_table
+
uncond_case
*
2
)
if
case_data >
0x7fff
:
case_data
=
hex_to_dec(
hex
(case_data)[
2
:])
jmp_off
=
case_data
*
(
2
*
element_shift)
uncond_jmp_addr
=
jmp_off
+
element_base
print
(
'jump_def_list->addr: 0x%x, cond_case: %d, cond_jmp_addr: 0x%x, uncond_case: %d, uncond_jmp_addr: 0x%x'
%
(addr, cond_case, cond_jmp_addr, uncond_case, uncond_jmp_addr))
do_patch(addr
-
4
, cond, addr
-
4
, cond_jmp_addr)
do_patch(addr,
'b'
, addr, uncond_jmp_addr)
# 处理 def_block
for
addr
in
jump_def_list:
#print('jump_def_list->addr: 0x%x' % addr)
if
addr
+
4
=
=
def_block:
def_init_block
=
addr
# print('def_init_block 0x%x' % def_init_block)
continue
mnem
=
idc.ida_ua.ua_mnem(addr)
if
mnem !
=
'B'
:
continue
mnem
=
idc.ida_ua.ua_mnem(addr
-
4
)
if
mnem
=
=
'CSEL'
:
do_cond_block(addr,
'CSEL'
)
elif
mnem
=
=
'CSINC'
:
do_cond_block(addr,
'CSINC'
)
|
拿JNI_OnLoad
做了简单的测试
修改前:
修改后:
准备工作做完了,开始上frida
意料之中,存在frida检测
简单测试了一下:
看来是对frida的基本特征做了检测
对openat进行hook
1
2
3
4
5
|
/**
* gum js 引擎的线程名字: Name: gum-js-loop
* vala 引擎的线程名字: Name: gmain
* dbus 线程名字: Name: gdbus
* */
|
定位到检测代码的位置:
用的去特征的frida-server
,不知道为啥gdbus
的特征仍然存在
对gdbus
字符串的比较在sub_9DE68
进行,简单的处理一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
function
pass_frida(){
var
module = Process.findModuleByName(
"libDexHelper.so"
)
var
base = module.base
var
sub_9DE68 = base.add(0x9DE68)
Interceptor.attach(sub_9DE68, {
onEnter:
function
(args){
},
onLeave:
function
(ret){
if
(ret != 0){
ret.replace(0)
}
}
})
}
|
除了通过hook svc调用来定位,还可以通过hook pthread_create
来获取创建的所有线程来定位检测
1
2
3
4
5
6
7
8
9
10
|
>>> hex(0x000000764F830C5C - 0x000000764F792000)
'0x9ec5c'
>>> hex(0x000000764F83C97C - 0x000000764F792000)
'0xaa97c'
>>> hex(0x000000764F82F73C - 0x000000764F792000)
'0x9d73c'
>>> hex(0x000000764F875FD0 - 0x000000764F792000)
'0xe3fd0'
>>> hex(0x000000764F82F030 - 0x000000764F792000)
'0x9d030'
|
此时frida已经可以使用了
猜测是存在代码的hash值校验,或比较了内存与文件中的代码是否一致
逆向分析时,肯定是少不了动态调试的,且遇到反调试肯定也是不可避免的
首先也是通过对openat进行hook来定位检测
定位到线程0xaa97c
也是很常规的检测方式
简单修改一下,JNI_OnLoad
就可以正常调试了
更多【某bang壳的简单分析】相关视频教程:www.yxfzedu.com