前言
复现该题以学习师傅们的解题思路,Java层是个人思路,后续是复现部分 涉及frida java/native层 hook, dump dex/so, bindiff恢复符号, dex文件结构等知识 附件: attachments.zip 包括题目附件,和dump相关文件
Java层分析
可以发现StringFog包内的StringObf.decode方法,直接hook查看结果
1
2
3
4
5
6
7
let StringObf = Java.use(
"StringFog.StringObf"
);
StringObf[
"decode"
].implementation =
function
(str) {
let result =
this
[
"decode"
](str);
console.log(`StringObf.decode(${str})=${result}`);
return
result;
};
hook后滑动手势,打印部分信息如下
其中com.crackme.happylock.Check类比较可疑,但无法直接找到,可能有热加载dex操作
搜索字符串decode前的值,MainActivity中可以定位到PatternLockUtils.enc和Utils.cmp
hook PatternLockUtils.enc
1
2
3
4
5
6
7
let PatternLockUtils = Java.use(
"com.andrognito.patternlockview.utils.PatternLockUtils"
);
PatternLockUtils[
"enc"
].implementation =
function
(arg1,arg2) {
console.log(`PatternLockUtils.enc is called:
null
=${arg1},
null
=${arg2}`);
let result =
this
[
"enc"
](arg1,arg2);
console.log(`PatternLockUtils.enc result=${result}`);
return
result;
};
可以发现和预期结果一致,该函数用于计算手势对应哈希
Utils.cmp通过反射调用clz.cmp方法进行验证
继续hook clz,打印相关信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let Utils = Java.use(
"com.crackme.happylock.Utils"
);
let clz = Utils.clz.value;
printInstance(clz);
console.log(
"clz: "
,clz);
console.log(
"==========Methods=========="
)
var
methods=clz.getMethods();
for
(let i=0;i<methods.length;i++){
console.log(methods[i]);
}
console.log(
"==========Fields=========="
)
var
fields=clz.getDeclaredFields();
if
(fields.length===0)
console.log(
"No fields!"
)
else
{
for
(let i=0;i<fields.length;i++){
console.log(fields[i]);
}
}
其中prinInstance是自己封装的实例打印函数
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
function
printClassFields(Class){
var
fields=Class.class.getDeclaredFields();
console.log(
"\n==========fields=========="
);
for
(
var
i=0;i<fields.length;i++){
var
fieldName=fields[i].getName();
var
fieldType=fields[i].getType();
console.log(fieldType,fieldName);
}
}
function
printClassMethods(Class){
var
methods=Class.class.getDeclaredMethods();
console.log(
"\n==========methods=========="
);
for
(
var
i=0;i<methods.length;i++){
var
methodParams=methods[i].getParameterTypes()
var
methodReturnType=methods[i].getReturnType()
var
methodName=methods[i].getName();
console.log(methodReturnType+
" "
+methodName+
"("
+methodParams+
")"
);
}
}
function
printInstanceMethods(Instance){
printClassMethods(Instance)
}
function
printInstanceFields(Instance){
var
fields=Instance.class.getDeclaredFields();
console.log(
"\n==========fields=========="
);
for
(
var
i=0;i<fields.length;i++){
var
fieldName=fields[i].getName();
var
fieldType=fields[i].getType();
var
fieldValue=Instance[fieldName].value;
console.log(fieldType,fieldName,
"="
,fieldValue);
}
}
function
printInstance(Instance){
printInstanceFields(Instance)
printInstanceMethods(Instance)
}
function
printClass(Class){
printClassFields(Class)
printClassMethods(Class)
}
printInstance可以发现clz对应的ClassLoader,指明了dex文件路径
clz正是上面可疑的Check类,Utils.cmp实际是调用了Check.cmp
但无法直接使用frida hook到Check类,开始分析so层
so的init_array中可以发现多个函数进行字符串解密操作
JNI_OnLoad没找到关键逻辑,hook RegisterNatives也没有发现注册函数
接下来可以尝试dump内存中的dex和so,内存中的so已经解密了字符串,并且内存中的dex可能有Check类
Dump Dex
使用frida-dexdump得到的dex文件中,无法找到com.crackme.happylock.Check类
使用https://github.com/lasting-yang/frida_dump脚本得到的dex文件能找到Check类,但代码被抽取
maps
查看app的内存映射情况进行观察
首先使用adb shell ps或frida-ps搜索app的pid
再使用maps查看内存映射情况
其中classes.dex标注了deleted, 下面开始dump dex文件
GDA dump
使用GDA可以,连接设备后搜索包名,在dex栏中可以定位,之后输入dex的起始地址和大小即可dump
如果无法正确dump可以查看文末问题解决
dump文件输出在GDA.exe同级目录dump/内,拖入jeb可以看到Check代码
getflag
一段简单的异或,可以getflag
1
2
3
4
5
6
7
8
encrypted_data
=
[
0x76
,
17
,
2
,
80
,
9
,
0x7D
,
6
,
22
,
0x71
,
66
,
0
,
81
,
94
,
41
,
87
,
20
,
0x7A
,
65
,
88
,
5
,
94
,
41
,
7
,
19
,
0x76
,
22
,
3
,
2
,
90
,
41
,
87
,
71
,
0x75
,
68
,
4
,
7
,
0x5F
,
0x74
,
4
,
67
]
key
=
b
"CrackMe!CrackMe!"
decrypted_bytes
=
[]
for
i
in
range
(
len
(encrypted_data)):
decrypted_byte
=
encrypted_data[i] ^ key[i
%
len
(key)]
decrypted_bytes.append(decrypted_byte)
decrypted_text
=
bytes(decrypted_bytes).decode()
print
(decrypted_text)
下面分析Native层做了什么操作
Native层
Dump SO
可以使用GDA或https://github.com/lasting-yang/frida_dump的dump_so.py脚本
GDA dump操作同dump dex类似,但是需要手动使用https://github.com/F8LEFT/SoFixer修复so文件
dump_so.py脚本可以自动dump并修复
恢复符号
分析dump并修复的so,可以发现shadowhook相关字符串,这是字节跳动的一个开源inline hook框架https://github.com/bytedance/android-inline-hook
下载抖音apk,提取libshadowhook.so,使用bindiff恢复符号(注意所有文件路径不能有中文):
ida打开libshadowhook.so,保存得到idb文件
ida打开dump出的libhappylock.so文件
ctrl+6调用bindiff,选择Diff Database,选择libshadowhook.so的idb文件
在Matched Functions窗口中ctrl+a选中所有匹配符号
再右键或者按ctrl+6,选择Import Symbols/Comments,设置匹配度
恢复符号后,JNI_OnLoad可以发现shadowhook相关操作
逻辑分析
参考ShadowHook 手册 , shadowhook_hook_sym_name函数声明如下
1
2
3
#include "shadowhook.h"
void
*shadowhook_hook_sym_name(
const
char
*lib_name,
const
char
*sym_name,
void
*new_addr,
void
**orig_addr);
hook_libc_execve
shadowhook_hook_func_addr中调用了shadowhook_hook_sym_name
hook了libc的execve
1
2
3
4
__int64
shadowhook_hook_func_addr()
{
return
shadowhook_hook_sym_name(aLibcSo_1, aExecve, sub_121E4, &off_44758);
}
代理函数sub_121E4中判断系统调用是否为dex2oat,如果是则不执行,如果不是则执行
即执行除dex2oat外的系统调用
hook_libart_ClassLinker::LoadMethod
shadowhook_hook_sym_name_callback 中也调用了shadowhook_hook_sym_name
1
2
3
4
5
6
7
__int64
shadowhook_hook_sym_name_callback()
{
__int64
v0;
v0 = __android_log_print();
return
shadowhook_hook_sym_name(aLibartSo, v0, sub_12270, &qword_44770);
}
参考SWDD的[2025软件系统安全赛]HappyLock 直接hook shadowhook_hook_sym_name(0x12830)
注意要配合hook dlopen使用,保证加载so时立刻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
function
hook_0x12380(){
var
ModuleAddr= Module.findBaseAddress(
'libhappylock.so'
);
console.log(
"libhappylock.so: "
+ModuleAddr)
Interceptor.attach(ModuleAddr.add(0x12830), {
onEnter:
function
(args) {
console.log(
'lib_name:'
, args[0].readCString());
console.log(
'sym_name:'
, args[1].readCString());
console.log(`proxy_func: ${args[2]} (libhappylock.so+0x${(args[2]-ModuleAddr).toString(16)})`);
console.log(`orig_addr: ${args[3]} (libhappylock.so+0x${(args[3]-ModuleAddr).toString(16)})\n`);
},
onLeave:
function
(retval) {
}
});
}
function
hook_dlopen_and_myhook() {
var
isHooking=
false
;
var
dlopen=Module.findExportByName(
null
,
"dlopen"
);
var
android_dlopen_ext=Module.findExportByName(
null
,
"android_dlopen_ext"
);
console.log(`dlopen: ${dlopen}, android_dlopen_ext: ${android_dlopen_ext}`);
if
(android_dlopen_ext){
Interceptor.attach(android_dlopen_ext,{
onEnter:
function
(args){
var
libPath=args[0].readCString();
if
(libPath !== undefined && libPath !=
null
&&libPath.indexOf(
"libhappylock"
) !== -1) {
this
.isCanHook =
true
;
}
},onLeave:
function
(args) {
if
(
this
.isCanHook&&!isHooking){
console.log(
"android_dlopen_ext libhappylock.so"
);
isHooking=
true
;
hook_0x12380();
}
}
})
}
}
setImmediate(hook_dlopen_and_myhook)
使用c++filt恢复函数符号名后,可以得知libart.so被hook的函数是ClassLinker::LoadMethod
1
art::ClassLinker::LoadMethod(art::DexFile const&, art::ClassAccessor::Method const&, art::Handle<art::mirror::Class>, art::ArtMethod
*
)
代理函数sub_12270 检测dex文件大小,如果匹配到目标dex则执行回填操作
(此处a1应是this指针,pdex是DexFile的引用,先调用了0x44770处的原始LoadMethod加载方法后再回填代码)
其中+0x217处是key的起始地址,后续为字符串表保存的字符串
+0x178处是Check.cmp的代码
综上所述,JNI_OnLoad没有注册Native方法,而是hook了libc和libart
当加载到目标dex文件时,回填代码
GDA无法正确dump问题
如果发现GDA dump无法正常使用(看不到app的内存映射情况等)
首先参考GDA关于android脱壳的问题说明 ,查看GDump是否正确推送至手机
其次GDA自带的adb可能会与系统adb冲突,将GDA自带的adb目录设置到系统adb环境变量之上即可
1
默认在 C:\Users\<User>\AppData\Roaming\GDA\gadtmp 目录
为了简化操作,可编写bat脚本,启动GDA时自动将gda自带的adb目录设置到环境变量最上方
@echo off
set "DIR_TO_ADD=C:\Users\admin\AppData\Roaming\GDA\gdatmp"
set "Path=%DIR_TO_ADD%;%Path%"
start "" "E:\Tools\MobileTools\Decompilers\GDA\GDA4.11.exe"
但使用时依然要注意: 其他shell中运行adb命令仍然会kill GDA的adb server
References
感谢以下师傅的指导:Shangwendada PangBai P1umH0
上传的附件: