本文的思路是很久以前就有的,但是找不到一个合适的工具“拍摄快照”一直都没有成功实现,才成功实现,感谢该文作者seeflower。如果原作者认为本文章不合适,请联系我删除,谢谢啦~
每次在虚拟机中打开ida,逆向一堆JniOnLoad或者.iniarray段被混淆的代码时。我都想着有没有一种方法,免去这些初始化流程(当然这些初始化操作是厂家的重点防护对象)。
有一天,工作完之后,手头上的jnionload还没有分析完,将VMware中运行的机器设置暂停,明天再分析。突然我想到,既然VMware能通过快照这种方式保存虚拟机的运行时状态,即使关机也可以恢复。那我能不能通过对Android APP”拍摄“一份”快照“(SnapShot),正好停在我想逆向的函数前,修改入参,然后反复运行这份快照,绕过初始化过程、绕过各种检测呢?
思路有了,接下来就是寻找工具,怎么去实现了。
为了能让Unidbg成功载入快照,拍摄内容:
有了目标,现在开始实现。
第一步:装载lldb(相机)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#获取lldb android 服务端
$NDK_PATH
/
toolchains
/
llvm
/
prebuilt
/
linux
-
x86_64
/
lib64
/
clang
/
9.0
.
8
/
lib
/
linux
/
aarch64
/
lldb
-
server
#ADB导入设备、赋予权限
adb push lldb
-
server
/
data
/
local
/
tmp
/
adb shell
cd
/
data
/
local
/
tmp
chmod
755
lldb
-
server
#开启服务
.
/
lldb
-
server p
-
-
server
-
-
listen unix
-
abstract:
/
/
/
data
/
local
/
tmp
/
debug.sock
#pc端连接android
$ lldb
platform select remote
-
android
platform connect unix
-
abstract
-
connect:
/
/
/
data
/
local
/
tmp
/
debug.sock
|
第二步:编写dump脚本
内存段(Segment)、寄存器(Register)的lldb Dump脚本
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
|
from
asyncio.log
import
logger
from
datetime
import
datetime
import
hashlib
import
json
from
pathlib
import
Path
from
typing
import
TYPE_CHECKING,
Dict
,
List
import
sys
import
zlib
import
os
if
TYPE_CHECKING:
from
lldb
import
*
import
lldb
black_list
=
{
'startswith'
: [
'/dev'
,
'/system/fonts'
,
'/dmabuf'
],
'endswith'
: [
'(deleted)'
,
'.apk'
,
'.odex'
,
'.vdex'
,
'.dex'
,
'.jar'
,
'.art'
,
'.oat'
,
'.art]'
],
'includes'
: [],
}
dump_path
=
Path(
"./dump/"
)
def
__lldb_init_module(debugger:
'SBDebugger'
, internal_dict:
dict
):
debugger.HandleCommand(
'command script add -f lldb_dumper.dumpMem dumpmem'
)
def
dumpMem(debugger:
'SBDebugger'
, command:
str
, exe_ctx:
'SBExecutionContext'
, result:
'SBCommandReturnObject'
, internal_dict:
dict
):
target
=
exe_ctx.GetTarget()
# type: SBTarget
arch_long
=
dump_arch_info(target)
frame
=
exe_ctx.GetFrame()
# type: SBFrame
regs
=
dump_regs(frame)
target
=
exe_ctx.GetTarget()
# type: SBTarget
sections
=
dump_memory_info(target)
max_seg_size
=
64
*
1024
*
1024
# dump内存
process
=
exe_ctx.GetProcess()
# type: SBProcess
segments
=
dump_memory(process, dump_path, black_list, max_seg_size)
(dump_path
/
"regs.json"
).write_text(json.dumps(regs))
(dump_path
/
"sections.json"
).write_text(json.dumps(sections))
(dump_path
/
"segments.json"
).write_text(json.dumps(segments))
def
dump_arch_info(target:
'SBTarget'
):
triple
=
target.GetTriple()
print
(f
'[dump_arch_info] triple => {triple}'
)
# 'aarch64', 'unknown', 'linux', 'android'
arch, vendor, sys, abi
=
triple.split(
'-'
)
if
arch
=
=
'aarch64'
or
arch
=
=
'arm64'
:
return
'arm64le'
elif
arch
=
=
'aarch64_be'
:
return
'arm64be'
elif
arch
=
=
'armeb'
:
return
'armbe'
elif
arch
=
=
'arm'
:
return
'armle'
else
:
return
''
def
dump_regs(frame:
'SBFrame'
):
regs
=
{}
# type: Dict[str, int]
registers
=
None
# type: List[SBValue]
for
registers
in
frame.GetRegisters():
# - General Purpose Registers
# - Floating Point Registers
print
(f
'registers name => {registers.GetName()}'
)
for
register
in
registers:
register_name
=
register.GetName()
register.SetFormat(lldb.eFormatHex)
register_value
=
register.GetValue()
regs[register_name]
=
register_value
print
(f
'regs => {json.dumps(regs, ensure_ascii=False, indent=4)}'
)
return
regs
def
dump_memory_info(target:
'SBTarget'
):
logger.debug(
'start dump_memory_info'
)
sections
=
[]
# 先查找全部分段信息
for
module
in
target.module_iter():
module: SBModule
for
section
in
module.section_iter():
section: SBSection
module_name
=
module.
file
.GetFilename()
start, end, size, name
=
get_section_info(target, section)
section_info
=
{
'module'
: module_name,
'start'
: start,
'end'
: end,
'size'
: size,
'name'
: name,
}
# size 好像有负数的情况 不知道是什么情况
print
(f
'Appending: {name}'
)
sections.append(section_info)
return
sections
def
get_section_info(tar, sec):
name
=
sec.name
if
sec.name
is
not
None
else
""
if
sec.GetParent().name
is
not
None
:
name
=
sec.GetParent().name
+
"."
+
sec.name
module_name
=
sec.addr.module.
file
.GetFilename()
module_name
=
module_name
if
module_name
is
not
None
else
""
long_name
=
module_name
+
"."
+
name
return
sec.GetLoadAddress(tar), (sec.GetLoadAddress(tar)
+
sec.size), sec.size, long_name
def
dump_memory(process:
'SBProcess'
, dump_path: Path, black_list:
Dict
[
str
,
List
[
str
]], max_seg_size:
int
):
logger.debug(
'start dump memory'
)
memory_list
=
[]
mem_info
=
lldb.SBMemoryRegionInfo()
start_addr
=
-
1
next_region_addr
=
0
while
next_region_addr > start_addr:
# 从内存起始位置开始获取内存信息
err
=
process.GetMemoryRegionInfo(next_region_addr, mem_info)
# type: SBError
if
not
err.success:
logger.warning(f
'GetMemoryRegionInfo failed, {err}, break'
)
break
# 获取当前位置的结尾地址
next_region_addr
=
mem_info.GetRegionEnd()
# 如果超出上限 结束遍历
if
next_region_addr >
=
sys.maxsize:
logger.info(f
'next_region_addr:0x{next_region_addr:x} >= sys.maxsize, break'
)
break
# 获取当前这块内存的起始地址和结尾地址
start
=
mem_info.GetRegionBase()
end
=
mem_info.GetRegionEnd()
# 很多内存块没有名字 预设一个
region_name
=
'UNKNOWN'
# 记录分配了的内存
if
mem_info.IsMapped():
name
=
mem_info.GetName()
if
name
is
None
:
name
=
''
mem_info_obj
=
{
'start'
: start,
'end'
: end,
'name'
: name,
'permissions'
: {
'r'
: mem_info.IsReadable(),
'w'
: mem_info.IsWritable(),
'x'
: mem_info.IsExecutable(),
},
'content_file'
: '',
}
memory_list.append(mem_info_obj)
# 开始正式dump
for
seg_info
in
memory_list:
try
:
start_addr
=
seg_info[
'start'
]
# type: int
end_addr
=
seg_info[
'end'
]
# type: int
region_name
=
seg_info[
'name'
]
# type: str
permissions
=
seg_info[
'permissions'
]
# type: Dict[str, bool]
# 跳过不可读 之后考虑下是不是能修改权限再读
if
seg_info[
'permissions'
][
'r'
]
is
False
:
logger.warning(f
'Skip dump {region_name} permissions => {permissions}'
)
continue
# 超过预设大小的 跳过dump
predicted_size
=
end_addr
-
start_addr
if
predicted_size > max_seg_size:
logger.warning(f
'Skip dump {region_name} size:0x{predicted_size:x}'
)
continue
skip_dump
=
False
for
rule
in
black_list[
'startswith'
]:
if
region_name.startswith(rule):
skip_dump
=
True
logger.warning(f
'Skip dump {region_name} hit startswith rule:{rule}'
)
if
skip_dump:
continue
for
rule
in
black_list[
'endswith'
]:
if
region_name.endswith(rule):
skip_dump
=
True
logger.warning(f
'Skip dump {region_name} hit endswith rule:{rule}'
)
if
skip_dump:
continue
for
rule
in
black_list[
'includes'
]:
if
rule
in
region_name:
skip_dump
=
True
logger.warning(f
'Skip dump {region_name} hit includes rule:{rule}'
)
if
skip_dump:
continue
# 开始读取内存
ts
=
datetime.now()
err
=
lldb.SBError()
seg_content
=
process.ReadMemory(start_addr, predicted_size, err)
tm
=
(datetime.now()
-
ts).total_seconds()
# 读取成功的才写入本地文件 并计算md5
# 内存里面可能很多地方是0 所以压缩写入文件 减少占用
if
seg_content
is
None
:
logger.debug(f
'Segment empty: @0x{start_addr:016x} {region_name} => {err}'
)
else
:
logger.info(f
'Dumping @0x{start_addr:016x} {tm:.2f}s size:0x{len(seg_content):x}: {region_name} {permissions}'
)
compressed_seg_content
=
zlib.compress(seg_content)
md5_sum
=
hashlib.md5(compressed_seg_content).hexdigest()
+
'.bin'
seg_info[
'content_file'
]
=
md5_sum
(dump_path
/
md5_sum).write_bytes(compressed_seg_content)
except
Exception as e:
# 这里好像不会出现异常 因为前面有 SBError 处理了 不过还是保留
logger.error(f
'Exception reading segment {region_name}'
, exc_info
=
e)
return
memory_list
|
符号表(SymTab)的lldb 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
|
from
audioop
import
add
import
json
from
pathlib
import
Path
from
typing
import
TYPE_CHECKING
if
TYPE_CHECKING:
from
lldb
import
*
import
lldb
symbolList
=
[]
def
__lldb_init_module(debugger:
'SBDebugger'
, internal_dict:
dict
):
debugger.HandleCommand(
'command script add -f lldb_symb.dumpsym dumpsym'
)
def
dumpsym(debugger:
'SBDebugger'
, command:
str
, exe_ctx:
'SBExecutionContext'
, result:
'SBCommandReturnObject'
, internal_dict:
dict
):
target
=
exe_ctx.GetTarget()
m
=
target.modules
for
module
in
m:
module:SBModule
module_name
=
module.
file
.GetFilename()
if
command
in
module_name:
symbols_array
=
module.get_symbols_array()
for
symbol
in
symbols_array:
symbolList.append(symInfo(symbol,target))
print
(json.dumps(symbolList))
dump_path
=
Path(
"./"
)
f_name
=
command
+
"_sym.json"
(dump_path
/
f_name).write_text(json.dumps(symbolList))
def
symInfo(symbol,target):
return
{
"name"
:symbol.name,
"addr"
:symbol.addr.GetLoadAddress(target)}
|
第三步:按下快门
将脚本导入到lldb:
command script import lldb_dumper.py
command script import lldb_symb.py
附加到目标so:
1
2
3
4
|
#env: lldb
#附加so
attach ${pid}
br
set
-
s ${so_name}
-
n ${函数名}
# 或者使用 -a ${相对偏移}
|
触发断点以后,通过以下命令得到快照文件:
1
2
|
dumpsym ${so_name}
#dump符号表
dumpmem
#dump内存信息
|
生成的文件有:
符号表json文件:libxxxxxxx.so_sym.json
dump文件夹:./dump/*
Register json文件:./dump/regs.json
Segment json文件:./dump/segments.json
Section json文件:./dump/sections.json
Unidbg是基于Unicorn增加JNI支持的Unicorn Plus Pro Ultra(就是牛皮哦~),所以Unidbg也通过了一系列Api对寄存器和内存操作:
恢复寄存器状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
String js
=
FileUtil.readString(
"/Users/fangg3/WorkSpace/tools/androidReverse/unidbg-0.9.6/unidbg-android/src/test/java/com/lldb/dumper/dump/regs.json"
, StandardCharsets.UTF_8);
JSONObject json
=
new JSONObject(js);
Backend backend
=
emulator.getBackend();
Memory memory
=
emulator.getMemory();
backend.reg_write(Arm64Const.UC_ARM64_REG_X0,
Long
.decode(json.getStr(
"x0"
)));
backend.reg_write(Arm64Const.UC_ARM64_REG_X1,
Long
.decode(json.getStr(
"x1"
)));
backend.reg_write(Arm64Const.UC_ARM64_REG_X2,
Long
.decode(json.getStr(
"x2"
)));
...
backend.reg_write(Arm64Const.UC_ARM64_REG_X28,
Long
.decode(json.getStr(
"x28"
)));
backend.reg_write(Arm64Const.UC_ARM64_REG_LR,
Long
.decode(json.getStr(
"lr"
)));
backend.reg_write(Arm64Const.UC_ARM64_REG_FP,
Long
.decode(json.getStr(
"fp"
)));
backend.reg_write(Arm64Const.UC_ARM64_REG_PC,
Long
.decode(json.getStr(
"pc"
)));
backend.reg_write(Arm64Const.UC_ARM64_REG_SP,
Long
.decode(json.getStr(
"sp"
)));
backend.reg_write(ArmConst.UC_ARM_REG_CPSR,
Long
.decode(json.getStr(
"cpsr"
)));
backend.reg_write(Arm64Const.UC_ARM64_REG_CPACR_EL1,
0x300000L
);
/
/
backend.reg_write(Arm64Const.UC_ARM64_REG_TPIDR_EL0,
0x000000797d778588L
);
/
/
TPIDR_EL0 这个寄存器很特殊,存储了当前线程信息,需要lldb下断点获取
|
恢复内存状态:
因为执行快照时,并不需要一些内存段,这里过滤一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
Boolean isWhite(String name) {
String[]
list
=
{
"libxxxxxx.so"
,
/
/
目标so
"libc.so"
,
/
/
一般都要加载的
"[stack:14414]"
,
/
/
堆栈相关 这里
14414
不固定 可以看看sp指向哪里,在segments.json里找
"[anon:.bss]"
,
"[anon:bionic TLS]"
/
/
TPIDR_EL0 寄存器会指向这里
/
/
还有其他根据实际情况添加
};
for
(String a :
list
) {
if
(name.contains(a))
return
true;
}
return
false;
}
|
这里内存文件已经zlib压缩,需要解压后再写入内存中。
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
|
int
UNICORN_PAGE_SIZE
=
0x1000
;
private
long
align_page_down(
long
x) {
return
x & ~(UNICORN_PAGE_SIZE
-
1
);
}
private
long
align_page_up(
long
x) {
return
(x
+
UNICORN_PAGE_SIZE
-
1
) & ~(UNICORN_PAGE_SIZE
-
1
);
}
private void map_segment(
long
address,
long
size,
int
perms) {
long
mem_start
=
address;
long
mem_end
=
address
+
size;
long
mem_start_aligned
=
align_page_down(mem_start);
long
mem_end_aligned
=
align_page_up(mem_end);
if
(mem_start_aligned < mem_end_aligned) {
emulator.getBackend().mem_map(mem_start_aligned, mem_end_aligned
-
mem_start_aligned, perms);
}
}
void loadMemory() {
String js
=
FileUtil.readString(
"/Users/fangg3/WorkSpace/tools/androidReverse/unidbg-0.9.6/unidbg-android/src/test/java/com/lldb/dumper/dump/segments.json"
, StandardCharsets.UTF_8);
JSONArray json
=
new JSONArray(js);
Memory memory
=
emulator.getMemory();
System.out.println(json.size());
try
{
for
(
int
i
=
0
; i < json.size(); i
+
+
) {
JSONObject seg_json
=
new JSONObject(json.get(i));
String name
=
seg_json.getStr(
"name"
);
Long
start
=
seg_json.getLong(
"start"
);
Long
end
=
seg_json.getLong(
"end"
);
String filename
=
seg_json.getStr(
"content_file"
);
JSONObject permissions
=
seg_json.getJSONObject(
"permissions"
);
Boolean r
=
permissions.getBool(
"r"
);
Boolean w
=
permissions.getBool(
"w"
);
Boolean x
=
permissions.getBool(
"x"
);
if
(Objects.equals(filename, "") || filename
=
=
null) {
continue
;
}
if
(!isWhite(name)) {
continue
;
}
int
per
=
0
;
if
(r) {
per |
=
UnicornConst.UC_PROT_READ;
}
if
(w) {
per |
=
UnicornConst.UC_PROT_WRITE;
}
if
(x) {
per |
=
UnicornConst.UC_PROT_EXEC;
}
map_segment(start, end
-
start, per);
System.out.println(filename
+
" "
+
name);
byte[] content_file
=
FileUtil.readBytes(
"/Users/fangg3/WorkSpace/tools/androidReverse/unidbg-0.9.6/unidbg-android/src/test/java/com/lldb/dumper/dump/"
+
filename);
content_file
=
ZipUtil.unZlib(content_file);
emulator.getBackend().mem_write(start, content_file);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(
"load memory over"
);
}
|
运行快照:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
List
<
Object
>
list
=
new ArrayList<
Object
>();
list
.add(vm.getJNIEnv());
DvmClass NativeApiobj
=
vm.resolveClass(
"com/mihoyo/hyperion/net/aaaaa"
);
list
.add(NativeApiobj.hashCode());
list
.add(vm.addLocalObject(new
StringObject(vm,
"123123123"
)));
list
.add(vm.addLocalObject(new
StringObject(vm,
"123123123"
)));
Number result
=
Module.emulateFunction(emulator, ctx_addr,
list
.toArray());
/
/
获取返回结果
String sign_str
=
(String) vm.getObject(result.intValue()).getValue();
System.out.println(
"sign_str="
+
sign_str);
|
有时候,会出现Memory unmap的情况,这个时候,在Segments.json,找到对应地址,将name 添加到whileList里面即可。
用unidbg 的libc 替换内存段中的libc,便于调试:
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
|
/
/
载入libc.so
...
vm.loadLibrary(new
File
(
"/Users/fangg3/WorkSpace/tools/androidReverse/unidbg-0.9.6/unidbg-android/src/main/resources/android/sdk23/lib64/libc.so"
), false);
Module libc
=
emulator.getMemory().findModule(
"libc.so"
);
...
/
/
过滤符号
public static String getSymName(
long
addr) {
JSONArray jarr
=
(JSONArray) parse;
for
(
int
i
=
0
; i < jarr.size(); i
+
+
) {
JSONObject jsonObject
=
jarr.getJSONObject(i);
if
(jsonObject.getLong(
"addr"
)
=
=
addr) {
return
jsonObject.getStr(
"name"
);
}
}
return
"";
}
/
/
添加hook 筛选运行地址 pc寄存器指向下一条运行的地址
...
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend,
long
address,
int
size,
Object
user) {
long
pc_value
=
emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_PC).longValue();
if
(sym_json.contains(String.valueOf(pc_value))) {
String symName
=
getSymName(address);
Symbol symbolByName
=
libc.findSymbolByName(symName);
if
(symbolByName !
=
null) {
emulator.getBackend().reg_write(Arm64Const.UC_ARM64_REG_PC, symbolByName.getAddress());
System.out.println(
"call "
+
symName);
}
}
}
|
有大佬能帮忙看看这些问题是什么原因么:
更多【一种通过“快照”的方式绕过So初始化时的检测】相关视频教程:www.yxfzedu.com