前言

从这道题目学习到了关于二代壳的一些基础知识点以及脱壳基础流程思路,分析Native层中运用到了一个dump_fix so文件的工具lasting-yang/frida_dump源码分析尚未完成

题目描述

小明忘记了某个App的手势密码,但是好在只要获取一个 Hash值就可以解密App加密的文件。选手只需要拿到手势密码密钥的Hash值后提交即可,不要求爆破获取手势密码。注意:1.请上交题目解题报告,否则题目成绩可能被判定无效。2.提交答案时只需提交中的字符串。3请关注赛事公告,访问方法:左侧菜单栏“赛事大厅”>所报名赛事的“详情”>下拉页面“赛事公告”

分析过程

Java层分析

运行app是一个手势锁,用jadx/jeb打开文件后可以看到字符串被加密了,有控制流平坦化
可以发现StringFog包内的StringObf.decode方法是解锁字符串的,进行hook看一下输出

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook() {
let activaty = Java.use("StringFog.StringObf");
activaty.decode.implementation = function(str){
let result = this.decode(str);
console.log(`StringObf.decode(${str}) = ${result}`);
return result;
}
}
function main() {
Java.perform(hook);
}

setImmediate(main);

看到很多可疑的字符串,有个Check方法,还有个cmp,以及登入失败提示Try again
定位到MainActivaty的

hook PatternLockUtils.enc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook() {
let activaty = Java.use("com.andrognito.patternlockview.utils.PatternLockUtils");
activaty.enc.implementation = function(str,list){
let result = this.enc(str,list);
console.log(`PatternLockUtils.enc :$r3 = ${str},list = ${list}`)
console.log(`PatternLockUtils.enc result = ${result})`)
return result;
}
}

function main() {
Java.perform(hook);
}

setImmediate(main);

可以看到该函数用于计算手势对应哈希

接着看到cmp

先hook一下cmp的传入传出参数

1
2
3
4
5
6
7
8
9
Java.perform(function () {
var Utils = Java.use("com.crackme.happylock.Utils");
Utils.cmp.implementation = function (s) {
console.log("[Utils.cmp] input = " + s);
var ret = this.cmp(s);
console.log("[Utils.cmp] ret = " + ret);
return ret;
};
});


传入的是手势对应哈希,接着分析下面

这里是用反射动态调用某个类里的 cmp(String) 方法
getDeclaredMethod 是 Java 反射(Reflection)里的核心方法,专门用来获取类里的某个方法
语法格式如下

1
Method method = 类对象.getDeclaredMethod( "方法名", 参数1类型.class, 参数2类型.class, ... );

Hook一下这个方法名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hook() {
let a = Java.use("com.crackme.happylock.Utils");
a.cmp.implementation = function(s){
let result = this.cmp(s);
try{
console.log("cmp.clz = " + a.clz.value);//返回Java 类对应的java.lang.Class对象本身
console.log("cmp.clz name = " + a.clz.value.getName());//返回类的完整类名字符串
}catch(e){
console.log("cmp.clz error = " + e);
}
console.log(`Utils.cmp result = ${result}`);
return result;
}
}

function main() {
Java.perform(hook);
}

setImmediate(main);

就是一开始hook解密字符串的com.crackme.happylock.Check类,尝试在jadx/jeb中搜索,无法直接找到,如果说在assets并且未加密的话,jeb或者jadx也是同样可以识别到这个类的,这里并没有识别到,那么就有热加载dex操作(在 App 运行过程中不重启、不重装,动态加载一个新的 dex 文件到内存里,并让虚拟机执行其中的代码)

既然知道了是动态加载,那么肯定离不开dexloader,我们直接搜索dexClassLoader:参考文章DexClassLoader 动态加载机制Android ClassLoader详解

有加密就有解密,这里看到了一个decode函数,直接进行hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function hook() {
try{
let a = Java.use("com.crackme.happylock.Utils");
let decodeoverload = a.decode.overloads;//查询有多少decode重构方法
console.log("Number of decodeoverload = " + decodeoverload.length);
a.decode.overload('[B').implementation = function(data){//寻找参数为byte的decode方法
let encoded = Java.array('byte', data);
console.log(`decodeoverload([B]) encode = ${encoded}`);
let result = this.decode(data);
console.log(`decodeoverload([B]) decode data = ${result}`);
return result;
}
}catch(e){
console.log("hook decodeoverload error = " + e);
}
}

function main() {
Java.perform(hook);
}

setImmediate(main);

可以看到解密后的dex头,将其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
Java.perform(function () {
let a = Java.use("com.crackme.happylock.Utils");
//java.io.FileOutputStream是Java里的一个文件输出流类,用于将数据写到文件里
let FileOutputStream = Java.use("java.io.FileOutputStream");
let dumped = false;
a.decode.overload("[B").implementation = function(data){
console.log("[+] Utils.decode(byte[]) called");
let result = this.decode(data);
console.log("[+] decode result length = " + result.length);
if(!dumped){
dumped = true;
let path = "/data/data/com.crackme.happylock/cache/happylock_dump.dex";
try{
//创建一个指向path的文件输出流对象 fos
let fos = FileOutputStream.$new(path);
fos.write(result);
fos.close();
console.log("[+] dex dumped to: " + path);
}catch(e){
console.log("[-] dump failed: " + e);
}
}
return result;
}
});

dump成功后用指令进行拉取

1
2
adb shell su -c "cp /data/data/com.crackme.happylock/cache/happylock_dump.dex /sdcard/happylock_dump.dex"
adb pull /sdcard/happylock_dump.dex C:\Users\LENOVO\Desktop\happylock_dump.dex

这样会保存到当前目录

然后可以使用指令将手机里dump和cp的文件删了,以免占内存

1
2
adb shell rm /sdcard/happylock_dump.dex
adb shell su -c "rm /data/data/com.crackme.happylock/cache/happylock_dump.dex"

将Dump下来的dex文件反编译看到

里面只有一个native方法,其余代码被抽取,接下来分析native层

Native层分析

这里直接用IDA打开libhappylock.so文件分析JNI_Onload函数发现字符串也有加密

使用工具lasting-yang/frida_dump对其进行dump so文件
配置好环境后使用指令

1
py -3 dump_so.py libhappylock.so

得到dump并且修复了的so文件libhappylock.so_0x7644078000_286720_fix.so,再次用IDA进行分析

先分析sub_126A8

这里可以看到一个字符串,那么猜测这个函数就是shadowhook的初始化操作,接着分析sub_12300函数

个函数就是将,aExecve hook 替换为sub_121E4

记录谁在调用 execve,如果要执行的是 dex2oat,就拦截掉,否则调用原始 execve。
dex2oat(参考文章:Android ART知多少?
dex2oat = DEX to OAT 编译器,是 Android 系统自带的工具。
转换过程:

1
2
3
4
5
6
7
8
9
Java/Kotlin 代码

.dex 文件
(Dalvik字节码)

dex2oat 编译

.oat / .odex 文件
(本地机器码,直接运行)

App 加固壳常会在运行时解密并加载真实 dex。逆向时通常在解密完成、真实 dex 被 ClassLoader/ART 接管时,用 Frida 或 ART hook 抓取内存中的 dex

接着看sub_12324函数

看一下sub_12270函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall sub_12270(__int64 a1, __int64 a2)
{
__int64 v3; // x19
__int64 base; // x8
_OWORD *v5; // x20

v3 = off_44770();
if ( *(a2 + 16) == 940 )
{
byte_44760 = 1;
mprotect(*(a2 + 8), 0x3ACu, 2);
base = *(a2 + 8);
v5 = (base + 0x217);
qword_44768 = base + 0x217;
memcpy((base + 0x178), &unk_42D90, 0x98u);
*v5 = xmmword_430A0;
}
return v3;
}

这里就是一个hook操作if ( *(a2 + 16) == 940 )这里是比较dex文件的长度,我们用frida hook出的dex文件长度就是940,这里将0x178位置的字节hook为unk_42D90的数据,将0x217出的字节替换为xmmword_430A0也就是CrackMe!CrackMe!

这里可以直接用010手动替换一下,进行修复

也可以直接动调查看classes.dex段内解密的dex

直接编写idapython脚本进行dump

1
2
3
4
5
6
7
start = 0x7910A15000
size = 0x03AC
path = "so.dex"
data = ida_bytes.get_bytes(start,size)
with open(path,"wb") as f:
f.write(data)
print("SUCCESS!!!")

得到完整的dex文件,用jeb文件打开即可看到反汇编代码,用jadx无法打开(具体原因可以参考SWDD的文章

这里就一个异或,编写解密脚本即可得到flag

EXP

1
2
3
4
5
6
7
8
9
10
11
12
endata = [  
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!'
flag = []
for i in range(len(endata)):
flag.append(endata[i] ^ key[i%len(key)])
print(''.join(chr(x) for x in flag))

#5cc3b0c720a25d25939f5db25dba1d2f66ed49ab

参考文章:
[2025软件系统安全赛]HappyLock – Sw’Blog
全国高校大学生软件创新大赛-软件系统安全赛 HappyLock复现
[原创]全国大学生软件安全攻防赛初赛wp