某企业加固应用脱壳、ollvm去混淆、算法分析记录

应用

某咖啡

抓包

新买的pixel 6, 用公司网络, 设置代理后,charles 没有任务反应, 就好像代理每设置成功一样。 但是用自己的另外一个手机开热点,可以抓到请求.
问题已解决: 在连接wifi 的时候设置使用设备 MAC, 不要随机mac 就可以使用公司网络进行抓包了.

反编译.

打开 发现用了 某数字厂商的壳. 于是尝试用 frida 脱壳. 结果发现这个apk 有anti-frida. 然后 hluda-server, Strong-frida 等都过不了.

脱壳

https://github.com/LLeavesG/eBPFDexDumper
我是用的这个基于 ebpf 的 dexDumper. 对内核有一定的要求, 本人亲测 pixel 6, 官方 rom, android 14. 可正常使用并完成脱壳.

1
./eBPFDexDumper dump -n com.example.app -o /sdcard/dex_out

脱壳后拖入 jadx, 发现有部分代码已经可以正常显示了. 全局搜索 sign, 定位几处可疑的地方。想要使用 frida, 但是有双进程保护,注入的时候找不到进程.
spawn 模式注入也会崩溃. 本人使用的是内核模块过 frida 检测. 但spawn 模式注入 不能先 注入 Java.perform。 要先加载脚本,等稳定后再把注入代码粘贴到hook 脚本中。否则一样闪退.
本人猜测是 init_array, 或者 jni_onload 中有 crc 检测. 但线程不但轮询检测中没有 crc 检测. 当然只能猜测.

本人使用 frida 环境
frida-server https://github.com/ultrafunkamsterdam/undetected-frida undetected_f1783
frida==17.8.3
frida-tools==14.8.1

frida hook String.getBytes 方法

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

console.log("Script loaded");

function showStacks() {
// return
Java.perform(function() {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
});
}

Java.perform(function() {
var StringClass = Java.use("java.lang.String");

StringClass.getBytes.overload('java.nio.charset.Charset').implementation = function (charset) {
var result = this.getBytes(charset);
var str = this.toString();

console.log(" === getBytes(Charset) ===");
console.log("Charset:", charset.toString());
console.log("String:", str);

// if (str.indexOf("mobile") !== -1 || str.indexOf("latitude") !== -1) {
// printStack("getBytes(Charset)");
// }
showStacks()
return result;
};

StringClass.getBytes.overload('java.lang.String').implementation = function (charset) {
var result = this.getBytes(charset);
var str = this.toString();

console.log(" === getBytes(charset) ===");
console.log("Charset:", charset);
console.log("String:", str);

// if (str.indexOf("mobile") !== -1 || str.indexOf("latitude") !== -1) {
// printStack("getBytes(charset)");
// }
showStacks()
return result;
};


StringClass.getBytes.overload().implementation = function () {
var result = this.getBytes();
var str = this.toString();

console.log(" === getBytes() ===");
console.log("String:", str);
showStacks()
// 过滤你关心的字段(避免刷屏)
// if (str.indexOf("mobile") !== -1 || str.indexOf("latitude") !== -1) {
// printStack("getBytes()");
// }

return result;
};
})

根据堆栈,定位到 md5_crypt 函数. frida 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
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

console.log("Script loaded");


Java.perform(function() {
let CryptoHelper = Java.use("com.xxxxx.safeboxlib.CryptoHelper");
let StringCls = Java.use("java.lang.String");

function bytesToString(bytes) {
try {
return StringCls.$new(bytes);
} catch (e) {
return "[decode error]";
}
}
function bytesToHex(bytes) {
let result = "";
for (let i = 0; i < bytes.length; i++) {
let b = (bytes[i] & 0xff).toString(16);
if (b.length == 1) b = "0" + b;
result += b;
}
return result;
}

// CryptoHelper["localAESWork4Api"].implementation = function (bArr, i2) {
// console.log(`CryptoHelper.localAESWork4Api is called: bArr=${bArr}, i2=${i2}`);
// console.log("localAESWork4Api input(str):", bytesToString(bArr)); console.log("localAESWork4Api input(hex):", bytesToHex(bArr));
// let result = this["localAESWork4Api"](bArr, i2);
// console.log(`CryptoHelper.localAESWork4Api result=${result}`);
// console.log("localAESWork4Api output(str):", bytesToString(result)); console.log("localAESWork4Api output(hex):", bytesToHex(result));
// return result;
// };

// let CryptoHelper = Java.use("com.xxxxx.safeboxlib.CryptoHelper");
CryptoHelper["localAESWork"].implementation = function (bArr, i2, bArr2) {
// console.log(`CryptoHelper.localAESWork is called: bArr=${bArr}, i2=${i2}, bArr2=${bArr2}`);
console.log(`CryptoHelper.localAESWork is called: i2=${i2}, `);
console.log("localAESWork bytesToString(bArr) :", bytesToString(bArr)); //console.log("localAESWork input(hex):", bytesToHex(bArr));
console.log("localAESWork bytesToHex(bArr) :", bytesToHex(bArr)); //console.log("localAESWork input(hex):", bytesToHex(bArr));
console.log("localAESWork bytesToString(bArr2) :", bytesToString(bArr2)); //console.log("localAESWork input(hex):", bytesToHex(bArr));
console.log("localAESWork bytesToHex(bArr2) :", bytesToHex(bArr2)); //console.log("localAESWork input(hex):", bytesToHex(bArr));

let result = this["localAESWork"](bArr, i2, bArr2);
// console.log(`CryptoHelper.localAESWork result=${result}`);
console.log("localAESWork output(str):", bytesToString(result));
console.log("localAESWork output(hex):", bytesToHex(result));
return result;
};


// let CryptoHelper = Java.use("com.xxxxx.safeboxlib.CryptoHelper");
// CryptoHelper["c"].implementation = function (str) {
// console.log(`CryptoHelper.c is called: str=${str}`);
// let result = this["c"](str);
// console.log(`CryptoHelper.c result=${result}`);
// return result;
// };
//
// let StubApp = Java.use("com.stub.StubApp");
// var res = StubApp.getString2("21888")
// console.log("StubApp res is ", res);


// let CryptoHelper = Java.use("com.xxxxx.safeboxlib.CryptoHelper");
CryptoHelper["md5_crypt"].implementation = function (bArr, i2, bArr2) {
console.log(`CryptoHelper.md5_crypt is called: i2=${i2}, `);
console.log("md5_crypt bytesToString(bArr) :", bytesToString(bArr)); //console.log("localAESWork input(hex):", bytesToHex(bArr));
console.log("md5_crypt bytesToHex(bArr) :", bytesToHex(bArr)); //console.log("localAESWork input(hex):", bytesToHex(bArr));
console.log("md5_crypt bytesToString(bArr2) :", bytesToString(bArr2)); //console.log("localAESWork input(hex):", bytesToHex(bArr));
console.log("md5_crypt bytesToHex(bArr2) :", bytesToHex(bArr2)); //console.log("localAESWork input(hex):", bytesToHex(bArr));
let result = this["md5_crypt"](bArr, i2, bArr2);
console.log("md5_crypt output(str):", bytesToString(result));
console.log("md5_crypt output(hex):", bytesToHex(result));
return result;
};

})



hook 结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
localAESWork bytesToString(bArr2) : ��o�_��ug�� ��ŖI    �P�Wbr�l|a�4;
localAESWork bytesToHex(bArr2) : fdc66fb15f838677abfc20c8d5c5964909fa50da57621072a76c7c61ae343b
localAESWork output(str): {"lsopName":"xxxxxxx","mobile":"","deptId":"","appversion":"5395","packageName":"com.lucky.luckyclient","version":"5.3.95","deviceId":"xxxxxxx-b7a5-4a7f-xxxx-xxxxxxxxx","userId":"","platform":"android"}
localAESWork output(hex): 7b226c736f704e616d652235636b79636c69656e74616e64726f6964222c226d6f62696c65223a22222c22646570744964223a22222c2261707076657273696f6e223a2235333935222c227061636b6167654e616d65223a22636f6d2e6c75636b792e6c75636b79636c69656e74222c2276657273696f6e223a22352e332e3935222c226465766963654964223a2264623565346262352d623761352d346137662d626361302d316630383130663566346462222c22757365724964223a22222c22706c6174666f726d223a22616e64726f6964227d
CryptoHelper.localAESWork is called: i2=2,
localAESWork bytesToString(bArr) : {"deptId":"","positionCode":"F101","appversion":"5395","cityId":"0"}
localAESWork bytesToHex(bArr) : 7b22646570744964223a222f736974696f6e436f6465223a2246313031222c2261707076657273696f6e223a2235333935222c22636974794964223a2230227d
localAESWork bytesToString(bArr2) : ��o�_��ug�� ��ŖI �P�Wbr�l|a�4;
localAESWork bytesToHex(bArr2) : fdc66fb15f83867567abfc20c8d5c5964909fa50da57621072a76c7c61ae343b
localAESWork output(str): >�uA8�wq�f6�c�OU��p���X8���B�Y0�}1��4�8��`��o���ߴ:ݠ�<* Zˠ�j�]+:z���$�� MOv
localAESWork output(hex): 3ec8754138b17771a866369d63e04f55b1dc70bc9c9b5838ba8ad242e05930ca7d31d8e0349438ff916015f69f6ffec8c7dfb43adda0f2b83c172a205acba0e46a965d2b3a7af9b9cf24a4e1204d4f76
CryptoHelper.md5_crypt is called: i2=1,
md5_crypt bytesToString(bArr) : cid=210101;q=Psh1QTixd3GoZjadY-
md5_crypt bytesToHex(bArr) : 6369643d3231303130313b713d5073683151546978476f5a6a6164592d425056624863634c79636d316734756f72535175425a4d4d70394d646a674e4a51345f35466746666166625f374978392d304f7432673872673846796f6757737567354771575853733665766d357a79536b3453424e5433593d3b743d313737373337303232333938323b7569643d32396464363634352d626230362d346133382d396665302d32653433326264313066396231373737333536363235343439
md5_crypt bytesToString(bArr2) : ��o�_��ug�� ��ŖI �P�Wbr�l|a�4;
md5_crypt bytesToHex(bArr2) : fdc66fb15f83867567abfc20c5964909fa50da57621072a76c7c61ae343b
md5_crypt output(str): 921726219197044498301375182129635621
md5_crypt output(hex): 393231373236323139313937303434343936333031333735313832313239363335363231

可以看到 先 localAESWork 再 md5_crypt, localAESWork 的结果会参数到 md5_crypt 的计算.
CryptoHelper.md5_crypt is called: i2=1 可以看到 i2 = 1 这点跟 localAESWork 的 2 是不一样的.

ida 打开. 进入 jni_onload 函数。
img_11

进入 off_4BCE0, 可以看到是个结构体, 里面存放的是 java函数名, 方法签名, native层绑定的 jni 函数.

img_11

进入 android_native_md5 函数看看,可以看到, 有大面积的 ollvm .

img_11

将整个函数丢给 chatgpt 和 gemini,这两个我喜欢混着用。 但是最近我发现 claude code 真的非常强大. 例如分析某个函数时, chatgpt 和 gemini 还在用非常含糊的语言表达时.
claude code 会用非常肯定语气告诉你. 并且十分靠谱, 这一点是我觉得 cc 远比 gpt 和 gemini 好用的地方

放个 cc 的回答.
img_11

AI 告诉我们