xhs shield 参数分析
断断续续其实拖了挺长时间的,本意是想通过亲手实践来学一下逆 app 的参数是一个怎样的流程,顺便了解下实战中涉及的安卓逆向的知识
定位 so
抓包分析
用 Reqable 对 xhs 进行抓包,翻了下请求,在请求头里看到了
shield 参数,一共 134 个字符

多观察几个请求头的这个参数可以发现它前 112 个字符是一样的,后面 22 个字符是变化的
hook NewStringUTF
首先需要知道这个这一串参数是从什么地方生成的,一般思路是 hook
NewStringUTF
NewStringUTF是 JNI 中的一个函数,把 native 层的const char *字符串转成 Java 层的jstring,相当于 Java/Kotlin 和 C/C++ native 代码之间的桥梁
JNI 规范里的 NewStringUTF 原型是:
1 | |
它会根据传入的 bytes 构造一个新的
java.lang.String, 并且它在 JNIEnv
函数表里的索引是 167
由此可以写出 hook NewStringUTF 的 frida 脚本
1 | |
但是尝试之后直接 Process terminated 了
开始以为是 frida 检测,绕了半天也没用,后面发现不是检测的问题,而是 hook 时机太早了,延后一点再注入这个就能正常看到日志了
1 | |
由此可以定位到要分析的参数在 libxyass.so 中
样本的是从 Google Play 下载的,因为谷歌推行了
Android App Bundle(.abb),按设备下发 apk,lib 在 apks 解压后的split_config.arm64_v8a.apk中
分析 so
sub_46DB0
根据 call off 的地址在 libxyass.so 中定位

caller off 拿到的是 return address,也就是 NewStringUTF
调完之后,要返回到的下一条指令地址,想要拿到调用地址的偏移需要减去 4
字节指令长度,所以调用地址的偏移是 0x469f8
而往前看最近的函数 sub_46DB0
可能就是构造这个参数的函数了

先是判断一个参数(怀疑是版本) 是否小于 6,根据结果选择
sub_47338 或者 sub_46fe8 这两个分支
hook 下 sub_46DB0 函数的参数 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
64function hook_sub_46DB0(){
const targetName = "libxyass.so";
const dlopen_ptr = Module.getGlobalExportByName("android_dlopen_ext");
if(dlopen_ptr){
Interceptor.attach(dlopen_ptr,{
onEnter:function(args){
const path = args[0]?.readUtf8String();
if (path && path.indexOf(targetName) !== -1) {
this.isTarget = true;
}
},
onLeave:function(retval){
if (this.isTarget) {
const mod = Process.findModuleByName(targetName);
if(mod){
const modBase = mod.base;
const table = modBase.add(0x976d8);
const ptr_func = table.readPointer();
const real_addr = ptr_func.sub(0x3fb24a64); // 静态计算
console.log("[+] Calculate Func Addr: " + real_addr);
Interceptor.attach(modBase.add(0x46DB0),{
onEnter:function(this:any,args){
const a1 = args[0];
const a2 = args[1];
const a3_ptr = args[2];
// const a3_var = a3_ptr.readS32();
const x8 = this.context.x8;
const x2 = this.context.x2;
console.log("\n[+] Hook sub_46DB0 args");
console.log(`[*] a1: ${a1}`);
console.log(hexdump(a1, {
length: 64,
ansi: true
}))
console.log(`[*] a2: ${a2}`);
console.log(hexdump(a2, {
length: 64,
ansi: true
}))
console.log(`[*] a3_ptr: ${a3_ptr}`);
console.log(hexdump(a3_ptr,{
length: 64,
ansi:true
}))
console.log(`[*] x8: ${x8}`);
console.log(hexdump(x8, {
length: 64,
ansi: true
}))
console.log(`[*] x2: ${x2}`);
console.log(hexdump(x2, {
length: 64,
ansi: true
}))
}
})
}
}
}
})
}
}

从 arg3 指向的地址处看到第一个字节是 0x4,4 < 6, 所以走的是
sub_47338 分支
1 | |
sub_4b8c8(RC4 + Base64)
Base64
可以看到这个函数有很多个参数并且最后面有个明显的 base64
算输出长度

Base64 字符表

sub_1e3c8
sub_4b8c8 中多次调用的这个函数
1 | |
arg1:目标字符串对象,arg2:要追加的数据地址,arg3:要追加的数据长度
先判断 arg1 当前是短模式还是长模式,取出当前长度 size
和当前容量 capacity,判断剩余容量够不够追加 arg3
字节,够的话直接把 arg2 指向的数据复制到末尾,不够的话调用
sub_1e1d4 做扩容并追加,然后更新长度字段,在新末尾补
'\0',返回 arg1
就是 append string 的逻辑
sub_1e070
1 | |
相当于 C++ 里的 operator new(size_t) / allocator
的底层分配函数
sub_1e494
1 | |
从位置 arg2 开始删除最多 arg3
个字符,并维护结尾 \0
结合反编译先 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
82
83
84
85function hook_sub_4B8C8(){
const targetName = "libxyass.so";
const dlopen_ptr = Module.getGlobalExportByName("android_dlopen_ext");
if(dlopen_ptr){
Interceptor.attach(dlopen_ptr,{
onEnter:function(args){
const path = args[0]?.readUtf8String();
if (path && path.indexOf(targetName) !== -1) {
this.isTarget = true;
}
},
onLeave:function(retval){
if (this.isTarget) {
const mod = Process.findModuleByName(targetName);
if(mod){
let modBase = mod.base;
let funcAddr = modBase.add(0x4B8C8);
Interceptor.attach(funcAddr,{
onEnter:function(args){
console.log("\n");
console.log("[*] a1 = " + args[0]?.toInt32());
console.log(
"[*] a2 =\n" +
hexdump(args[1]!, {
length: 64,
ansi: true
})
);
console.log("[*] a3 = " + args[2]?.toInt32());
console.log("[*] a4 = " + args[3]?.toInt32());
console.log(
"[*] a5 =\n" +
hexdump(args[4]!, {
length: 64,
ansi: true
})
);
console.log(
"[*] a6 =\n" +
hexdump(args[5]!, {
length: args[6]!.toUInt32(),
ansi: true
})
);
console.log("[*] a7 = " + args[6]!.toInt32());
}
})
}
}
}
})
}
}
function getStdStringInfo(ptr: NativePointer) {
if (ptr.isNull() || ptr.toUInt32() < 0x1000) {
return { size: 0, data: ptr };
}
try {
const firstByte = ptr.readU8();
let size: number;
let dataPtr: NativePointer;
if ((firstByte & 1) === 0) {
// 短模式
size = firstByte >> 1;
dataPtr = ptr.add(1);
} else {
// 长模式
size = ptr.add(8).readU64().toNumber();
dataPtr = ptr.add(16).readPointer();
}
if (size > 100000) return { size: 0, data: ptr };
return { size: size, data: dataPtr };
} catch (e) {
return { size: 0, data: ptr };
}
}
部分日志如下 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[*] a1 = 1
[*] a2 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76ec8ba700 0e 39 31 39 33 38 30 33 00 00 00 00 00 00 00 00 .9193803........
76ec8ba710 50 1e eb ed 79 00 00 00 10 a7 8b ec 76 00 00 00 P...y.......v...
76ec8ba720 e0 a6 8b ec 76 00 00 00 d8 ff ff ff 80 ff ff ff ....v...........
76ec8ba730 20 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 .z.9].uw....dnc
[*] a3 = -319115519
[*] a4 = 4
[*] a5 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76ec8ba6e8 31 00 00 00 00 00 00 00 24 00 00 00 00 00 00 00 1.......$.......
76ec8ba6f8 50 90 ed ed 79 00 00 00 0e 39 31 39 33 38 30 33 P...y....9193803
76ec8ba708 00 00 00 00 00 00 00 00 50 1e eb ed 79 00 00 00 ........P...y...
76ec8ba718 10 a7 8b ec 76 00 00 00 e0 a6 8b ec 76 00 00 00 ....v.......v...
[*] a6 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76ec8ba731 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 5c .z.9].uw....dnc\
[*] a7 = 16
[*] a1 = 1
[*] a2 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76aa228550 0e 39 31 39 33 38 30 33 00 00 00 00 00 00 00 00 .9193803........
76aa228560 50 1e eb ed 79 00 00 00 60 85 22 aa 76 00 00 00 P...y...`.".v...
76aa228570 30 85 22 aa 76 00 00 00 d8 ff ff ff 80 ff ff ff 0.".v...........
76aa228580 20 f6 e1 5f 18 eb 36 e5 9c 36 13 3e e9 22 be eb .._..6..6.>."..
[*] a3 = -319115519
[*] a4 = 4
[*] a5 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76aa228538 31 00 00 00 00 00 00 00 24 00 00 00 00 00 00 00 1.......$.......
76aa228548 90 59 f4 ed 79 00 00 00 0e 39 31 39 33 38 30 33 .Y..y....9193803
76aa228558 00 00 00 00 00 00 00 00 50 1e eb ed 79 00 00 00 ........P...y...
76aa228568 60 85 22 aa 76 00 00 00 30 85 22 aa 76 00 00 00 `.".v...0.".v...
[*] a6 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76aa228581 f6 e1 5f 18 eb 36 e5 9c 36 13 3e e9 22 be eb 1d .._..6..6.>."...
[*] a7 = 16
[*] a1 = 1
[*] a2 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
77ac5305c0 0e 39 31 39 33 38 30 33 00 00 00 00 00 00 00 00 .9193803........
77ac5305d0 50 1e eb ed 79 00 00 00 d0 05 53 ac 77 00 00 00 P...y.....S.w...
77ac5305e0 a0 05 53 ac 77 00 00 00 d8 ff ff ff 80 ff ff ff ..S.w...........
77ac5305f0 20 17 ff 6a 49 ac 2c 90 dd 49 5e 5d 59 2b 8e 16 ..jI.,..I^]Y+..
[*] a3 = -319115519
[*] a4 = 4
[*] a5 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
77ac5305a8 31 00 00 00 00 00 00 00 24 00 00 00 00 00 00 00 1.......$.......
77ac5305b8 90 59 f4 ed 79 00 00 00 0e 39 31 39 33 38 30 33 .Y..y....9193803
77ac5305c8 00 00 00 00 00 00 00 00 50 1e eb ed 79 00 00 00 ........P...y...
77ac5305d8 d0 05 53 ac 77 00 00 00 a0 05 53 ac 77 00 00 00 ..S.w.....S.w...
[*] a6 =
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
77ac5305f1 17 ff 6a 49 ac 2c 90 dd 49 5e 5d 59 2b 8e 16 a6 ..jI.,..I^]Y+...
[*] a7 = 16
9193803 是版本号,a7 恒为 16,推测为 a6 的长度,a6 的 16
字节没什么规律,应该是经过某个加密后的数据
结合 ai 分析修缮了下 hook 脚本,把返回值顺便也 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
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
153function hook_sub_4B8C8_v2() {
const targetName = "libxyass.so";
const mod = Process.findModuleByName(targetName);
if (mod) {
attach_to_func(mod.base);
} else {
const dlopen_ptr = Module.getGlobalExportByName("android_dlopen_ext");
if (dlopen_ptr) {
Interceptor.attach(dlopen_ptr, {
onEnter: function (args) {
this.path = args[0]?.readUtf8String();
},
onLeave: function (retval) {
if (this.path && this.path.indexOf(targetName) !== -1) {
const m = Process.findModuleByName(targetName);
if (m) attach_to_func(m.base);
}
}
});
}
}
}
interface StdStringInfo {
size: number;
data: NativePointer;
content: string | null;
}
function attach_to_func(modBase: NativePointer) {
const funcAddr = modBase.add(0x4B8C8);
console.log("[+] Hooking sub_4B8C8 at: " + funcAddr);
Interceptor.attach(funcAddr, {
onEnter: function (args) {
const a1 = args[0].toInt32();
const a3 = args[2].toInt32();
const a4 = args[3].toInt32();
const a7 = args[6].toInt32();
// 解析 std::string
const str2 = getStdString(args[1]);
const str5 = getStdString(args[4]);
// this.arg8_ptr = this.context.x8;
this.arg8_ptr = (this.context as any).x8;
if (this.arg8_ptr.toUInt32() < 0x1000) {
this.arg8_ptr = args[7];
}
console.log("\n" + "=".repeat(40));
console.log("[*] Call sub_4B8C8 ");
console.log(`[+] arg1 (ID): ${a1} (Hex: 0x${a1.toString(16).padStart(8, '0')})`);
console.log(`[+] arg3 (Serial): ${a3}`);
console.log(`[+] arg4 (Type): ${a4} -> 转换为 Header 中的: ${a4 != 0 ? 2 : 1}`);
console.log(`[+] arg2 (String 1):`);
console.log(` - Len: ${str2.size}`);
console.log(` - Content: ${str2.content}`);
if (str2.size > 0) {
console.log(hexdump(str2.data, { length: str2.size, ansi: true }));
}
console.log(`[+] arg5 (String 2):`);
console.log(` - Len: ${str5.size}`);
console.log(` - Content: ${str5.content}`);
if (str5.size > 0) {
console.log(hexdump(str5.data, { length: str5.size, ansi: true }));
}
console.log(`[+] arg6 (Raw Data):`);
console.log(` - Len (arg7): ${a7}`);
if (a7 > 0 && !args[5].isNull()) {
console.log(hexdump(args[5], { length: a7, ansi: true }));
}
console.log("=".repeat(40));
},
onLeave: function (retval) {
const result = getStdString(this.arg8_ptr);
console.log("\n[!] sub_4B8C8 Return:");
if (result.size > 0) {
console.log(`[*] Result Content: ${result.content}`);
} else {
console.log("[*] Result is empty");
}
}
});
}
function getStdStringInfo(ptr: NativePointer) {
if (ptr.isNull() || ptr.toUInt32() < 0x1000) {
return { size: 0, data: ptr };
}
try {
const firstByte = ptr.readU8();
let size: number;
let dataPtr: NativePointer;
if ((firstByte & 1) === 0) {
// 短模式
size = firstByte >> 1;
dataPtr = ptr.add(1);
} else {
// 长模式
size = ptr.add(8).readU64().toNumber();
dataPtr = ptr.add(16).readPointer();
}
if (size > 100000) return { size: 0, data: ptr };
return { size: size, data: dataPtr };
} catch (e) {
return { size: 0, data: ptr };
}
}
function getStdString(ptr: NativePointer): StdStringInfo {
if (ptr.isNull() || ptr.toUInt32() < 0x1000) {
return { size: 0, data: ptr, content: null };
}
try {
const firstByte = ptr.readU8();
let size: number;
let dataPtr: NativePointer;
if ((firstByte & 1) === 0) {
// 短模式 (SSO)
size = firstByte >> 1;
dataPtr = ptr.add(1);
} else {
// 长模式
size = ptr.add(8).readU64().toNumber();
dataPtr = ptr.add(16).readPointer();
}
let content: string | null = null;
if (size > 0) {
try {
content = dataPtr.readUtf8String(size);
} catch (e) {
content = "[Binary/Non-UTF8 Data]";
}
} else {
content = "";
}
return { size, data: dataPtr, content: content };
} catch (e) {
return { size: 0, data: ptr, content: "[Error Reading]" };
}
}
1 | |
RC4
中间部分很像 RC4
初始化 256 字节状态表

之后是 KSA ,key 为 std::abort();

这个 key 通过 frida hook 也能验证
1 | |
hook 结果
1 | |
后面的循环则是 PRGA + XOR 输出,在循环里面更新索引,从状态表取值,swap,再取一个字节与明文异或

hook append 函数(sub_1e3c8)
这个 hook 确实是挺重要的一个 hook,很多推测和验证都是根据这个 hook 拿到的数据来分析的
1 | |
这是其中一轮的日志 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[+] === sub_1E3C8 ===
[*] SIZE before append: 24
[*] CONTENT before append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a9dec8a90 00 00 00 01 ec fa af 01 00 00 00 02 00 00 00 07 ................
7a9dec8aa0 00 00 00 24 00 00 00 10 ...$....
[*] appending length: 7
[*] Data to Append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76fbf88701 39 31 39 33 38 30 33 9193803
[*] SIZE after append: 31
[*] CONTENT after append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a9dec8a90 00 00 00 01 ec fa af 01 00 00 00 02 00 00 00 07 ................
7a9dec8aa0 00 00 00 24 00 00 00 10 39 31 39 33 38 30 33 ...$....9193803
[+] Data Flow into sub_1E3C8 (Call 2)
[*] Length: 36
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
79edecb610 38 64 65 33 65 63 37 62 2d 62 61 61 30 2d 33 37 8de3ec7b-baa0-37
79edecb620 35 66 2d 62 64 33 66 2d 30 30 36 64 64 36 62 36 5f-bd3f-006dd6b6
79edecb630 35 33 32 35 5325
[+] === sub_1E3C8 ===
[*] SIZE before append: 31
[*] CONTENT before append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a9dec8a90 00 00 00 01 ec fa af 01 00 00 00 02 00 00 00 07 ................
7a9dec8aa0 00 00 00 24 00 00 00 10 39 31 39 33 38 30 33 ...$....9193803
[*] appending length: 36
[*] Data to Append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
79edecb610 38 64 65 33 65 63 37 62 2d 62 61 61 30 2d 33 37 8de3ec7b-baa0-37
79edecb620 35 66 2d 62 64 33 66 2d 30 30 36 64 64 36 62 36 5f-bd3f-006dd6b6
79edecb630 35 33 32 35 5325
[*] SIZE after append: 67
[*] CONTENT after append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
790de3a430 00 00 00 01 ec fa af 01 00 00 00 02 00 00 00 07 ................
790de3a440 00 00 00 24 00 00 00 10 39 31 39 33 38 30 33 38 ...$....91938038
790de3a450 64 65 33 65 63 37 62 2d 62 61 61 30 2d 33 37 35 de3ec7b-baa0-375
790de3a460 66 2d 62 64 33 66 2d 30 30 36 64 64 36 62 36 35 f-bd3f-006dd6b65
790de3a470 33 32 35 325
[+] Data Flow into sub_1E3C8 (Call 3)
[*] Length: 16
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76fbf881e9 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 5c .z.9].uw....dnc\
[+] === sub_1E3C8 ===
[*] SIZE before append: 67
[*] CONTENT before append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
790de3a430 00 00 00 01 ec fa af 01 00 00 00 02 00 00 00 07 ................
790de3a440 00 00 00 24 00 00 00 10 39 31 39 33 38 30 33 38 ...$....91938038
790de3a450 64 65 33 65 63 37 62 2d 62 61 61 30 2d 33 37 35 de3ec7b-baa0-375
790de3a460 66 2d 62 64 33 66 2d 30 30 36 64 64 36 62 36 35 f-bd3f-006dd6b65
790de3a470 33 32 35 325
[*] appending length: 16
[*] Data to Append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76fbf881e9 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 5c .z.9].uw....dnc\
[*] SIZE after append: 83
[*] CONTENT after append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
796df98aa0 00 00 00 01 ec fa af 01 00 00 00 02 00 00 00 07 ................
796df98ab0 00 00 00 24 00 00 00 10 39 31 39 33 38 30 33 38 ...$....91938038
796df98ac0 64 65 33 65 63 37 62 2d 62 61 61 30 2d 33 37 35 de3ec7b-baa0-375
796df98ad0 66 2d 62 64 33 66 2d 30 30 36 64 64 36 62 36 35 f-bd3f-006dd6b65
796df98ae0 33 32 35 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 325.z.9].uw....d
796df98af0 6e 63 5c nc\
=== RC4 Start ===
[*] Length: 83
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76fbf88241 00 00 00 00 00 00 00 53 00 00 00 00 00 00 00 a0 .......S........
76fbf88251 8a f9 6d 79 00 00 00 80 00 00 00 33 98 27 cd 00 ..my.......3.'..
76fbf88261 00 00 00 00 00 00 00 7c 00 00 00 2e 00 00 00 0e .......|........
76fbf88271 00 00 00 3d 00 00 00 09 00 00 00 32 00 00 00 e3 ...=.......2....
[!!!] CRITICAL: Final Binary Packet before Base64
[*] Length: 83
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7abde540b0 35 16 11 ed 31 1b 52 1b 0f df dc fa a0 8b 3a 52 5...1.R.......:R
7abde540c0 86 99 3b 45 6b e9 46 e3 7f 0f 75 e2 5e 44 ae 4a ..;Ek.F...u.^D.J
7abde540d0 91 d1 b3 cb e5 60 1e 9d aa ae b1 5f cf c3 2d d9 .....`....._..-.
7abde540e0 9b 7e 83 f6 31 46 0c 4e 11 63 dc 30 bd a6 da 2e .~..1F.N.c.0....
7abde540f0 63 84 e1 64 ab ac e6 ff 14 e4 9e 2d 85 a6 52 32 c..d.......-..R2
7abde54100 4d 94 e7 M..
[+] Data Flow into sub_1E3C8 (Call 4)
[*] Length: 83
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7abde540b0 35 16 11 ed 31 1b 52 1b 0f df dc fa a0 8b 3a 52 5...1.R.......:R
7abde540c0 86 99 3b 45 6b e9 46 e3 7f 0f 75 e2 5e 44 ae 4a ..;Ek.F...u.^D.J
7abde540d0 91 d1 b3 cb e5 60 1e 9d aa ae b1 5f cf c3 2d d9 .....`....._..-.
7abde540e0 9b 7e 83 f6 31 46 0c 4e 11 63 dc 30 bd a6 da 2e .~..1F.N.c.0....
7abde540f0 63 84 e1 64 ab ac e6 ff 14 e4 9e 2d 85 a6 52 32 c..d.......-..R2
7abde54100 4d 94 e7 M..
[+] === sub_1E3C8 ===
[*] SIZE before append: 16
[*] CONTENT before append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
76fbf881e9 00 04 00 04 00 00 00 01 00 00 00 53 00 00 00 53 ...........S...S
[*] appending length: 83
[*] Data to Append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7abde540b0 35 16 11 ed 31 1b 52 1b 0f df dc fa a0 8b 3a 52 5...1.R.......:R
7abde540c0 86 99 3b 45 6b e9 46 e3 7f 0f 75 e2 5e 44 ae 4a ..;Ek.F...u.^D.J
7abde540d0 91 d1 b3 cb e5 60 1e 9d aa ae b1 5f cf c3 2d d9 .....`....._..-.
7abde540e0 9b 7e 83 f6 31 46 0c 4e 11 63 dc 30 bd a6 da 2e .~..1F.N.c.0....
7abde540f0 63 84 e1 64 ab ac e6 ff 14 e4 9e 2d 85 a6 52 32 c..d.......-..R2
7abde54100 4d 94 e7 M..
[*] SIZE after append: 99
[*] CONTENT after append:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a8df3d1e0 00 04 00 04 00 00 00 01 00 00 00 53 00 00 00 53 ...........S...S
7a8df3d1f0 35 16 11 ed 31 1b 52 1b 0f df dc fa a0 8b 3a 52 5...1.R.......:R
7a8df3d200 86 99 3b 45 6b e9 46 e3 7f 0f 75 e2 5e 44 ae 4a ..;Ek.F...u.^D.J
7a8df3d210 91 d1 b3 cb e5 60 1e 9d aa ae b1 5f cf c3 2d d9 .....`....._..-.
7a8df3d220 9b 7e 83 f6 31 46 0c 4e 11 63 dc 30 bd a6 da 2e .~..1F.N.c.0....
7a8df3d230 63 84 e1 64 ab ac e6 ff 14 e4 9e 2d 85 a6 52 32 c..d.......-..R2
7a8df3d240 4d 94 e7 M..
=== Base64 output ===
AAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOFkq6zm/xTkni2FplIyTZTn
根据这个结果可以看出,它先加载了 24 字节的数据,接着是版本号
9193803,然后是一个 UUID
8de3ec7b-baa0-375f-bd3f-006dd6b65325,最后是 16
字节的加密数据,到这里一共有 83 字节的数据,经过了 RC4
加密,加密后又追加了 16 字节的数据,最后对 99 字节的数据进行了 Base64
编码
并且可以验证这个 RC4 是标准的

观察多轮结果可以发现 RC4 结束后 append 的那 16 字节数据是固定的
00 04 00 04 00 00 00 01 00 00 00 53 00 00 00 53,变化的是进入
RC4 加密前的 16 字节
而对于最开始加载的 24 字节的 Header 其实是与参数相对应的
00 00 00 01 是 arg1 的大端表示
ec fa af 01 是 arg3 的大端表示

00 00 00 02


arg4 != 0 所以转换为 Header 中的 2
00 00 00 07 是 arg2 的长度(即版本号字符串 9193803
的长度)
00 00 00 24 是 arg5 的长度(即 UUID 字符串的长度)
00 00 00 10 是 arg6 的长度
sub_47338
分析这条链路的时候,有很多 br 间接跳转和混淆
把 Sections 中的 .data
由默认的可写改为只读,这样能解决一部分简单的间接跳转,bn
可以自动分析出来跳转地址
另外的间接跳转则要根据不同情况进行分析
1 | |
sub_850d8

虚函数调用,frida hook 或者 trace 的方式可以获取到 vtable 和函数偏移,得到的 0x85194 处调用的 function_offset 是 0x868d8,所以是 sub_868d8(x0, arg2, arg3)

同时这个函数也是一个小型的控制流平坦化,分发器通过 x9_2
的值来决定下一步执行的基本块
sub_868a8
0x868d8 跳转过去实际是在 sub_868a8 这个函数里面
1 | |
很像是一个 hash update 的函数 Update(ctx, data, len)
根据反编译函数签名可以分析为
sub_868a8(ctx_wrapper* arg1, const uint8_t* data, size_t len)
arg1 是 外层对象, *(arg1 + 0x10) 是 hash
context 结构体,arg2 是输入数据指针,arg3
是输入长度
sub_86e44
这个函数中由多个条件语句控制的间接跳转,修复好跳转后发现是嵌套了多层控制流平坦化的函数
详细分析见 BN IL 层解决多条件控制的双分支间接跳转
sub_89240

另外这个 so 里面还出现了不少这种结构的跳转,从 jump
往前分析可以看到,如果要计算最终的跳转地址,需要得到参数值,如果只是处理单个函数的这种结构的间接跳转的话,可以直接
frida hook
获取参数值获得地址,然后从地址中读值,就能计算出最后的跳转地址了,算出之后在
bn 的 mlil 层用 Set User Variable Value
的方式设置成 ConstantValue 填入计算出的固定值,bn
就能接着往下分析了

设置完之后发现又有新的跳转,并且跳转地址计算依赖于上次计算也同样涉及得到的
x24_3,那就直接给 x24_3 设置成算出的常量值

这样的话 bn 就能顺利分析下去了,并且下方出现了新的类似的跳转结构

在分析的过程中发现样本中有很多都是这样的结构,每次都改 frida 脚本和手动指定值很麻烦,于是考虑自动化实现这个过程
首先在 bn 中识别出这种跳转结构,并且收集计算跳转地址和进行 frida hook 所需要的所有数据,然后自动生成 frida 脚本,执行 hook 后,再用脚本批量赋值的方式给所有这种结构的跳转设置跳转地址
因为处理混淆的过程中先处理的这种跳转结构,再去处理的前面那种由多个条件语句控制的间接跳转,所以当时没有意识到这些结构就是被控制流平坦化打乱的一个个块
而在另一个函数中能明显的看出来
sub_886d0


这个函数和上面 sub_86e44
的混淆模式很像,虽然有三个条件,但是依旧在本质上是相同的
cond,而且最后的两个未解析出来的 jump
正是上面那些间接跳转块的结构
不同的是,这个函数有关地址的计算用到了传入的参数,这就对静态分析造成了很大的影响(其实最开始
bn 连 0x88760
处跳转的两个地址都没有算出来,图上能分析出来是手动指定过的),用解决
sub_86e44
方式就处理不了了,因为能解析出跳转地址的前提是能收集到所有计算需要的数据,而这个函数多处计算缺乏
arg 值,所以只能结合动态 hook 的结果来分析
一共进行三个步骤:bn 中识别结构准备 hook 所需数据 -> frida hook 收集 patch 数据 -> bn 中批量设置跳转地址
自动化收集数据
以 0x873f4 处的跳转为例,要求 x24_1
的值,需要 x24 的值和后面的常量,x24
的值依赖于 arg1

arg1 对应寄存器
x13,后续也直接收集对应的寄存器名字
所以第一个 bn
自动化脚本要做的事就是扫描所有函数,识别出该种结构的跳转,获取 jump
地址,jump 值 x24_1 定义处地址,收集常量
0x3abc9d8、算 x24 用到的寄存器
x13,偏移量为
0x80,同时关注从地址中读取的数据大小以及这个数据与常量的运算方式(因为可能在
mlil 层看到的是减去一个常量但实际是 add
操作加上一个负数,所以对这两种模式都作了匹配)
对 so 中所有函数进行上述操作,获得这些信息之后直接生成相应的 frida 脚本,就不用每次去手动修改 hook 代码了
1 | |


可以看到 bn 成功识别出了结构匹配的跳转
frida hook
使用上一步生成的 frida 脚本进行 hook 操作,这里写了一个 python
脚本来执行
frida,因为直接在控制台打印日志太多太频繁了不方便查看,所以改成通过
send 的方式把数据发送回 python 脚本,由 python
来处理日志输出,同时可以生成两份格式的文件,一份便于自己阅读查看,一份便于后续写
bn python 脚本时读取内容来批量设置跳转地址
上面例子得到的 frida 脚本 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'use strict';
const soName = 'libxyass.so';
const patterns = [
{
"func_name": "sub_886d0",
"func_start": "0x886d0",
"func_off": "0x886d0",
"jump_addr": "0x873f4",
"jump_off": "0x873f4",
"jump_expr": "x24_1#2",
"arith_op": "add",
"delta": "-0x3abc9d8",
"delta_int": -61589976,
"arg_name": "arg1",
"base_reg": "x13",
"load_off": 128,
"load_off_hex": "0x80",
"load_size": 8,
"pattern": "load_plus_const",
"load_expr": "[arg1#0 + 0x80].q @ mem#2",
"addr_expr": "arg1#0 + 0x80",
"final_expr": "x24#1 - 0x3abc9d8"
},
{
"func_name": "sub_89240",
"func_start": "0x89240",
"func_off": "0x89240",
"jump_addr": "0x88630",
"jump_off": "0x88630",
"jump_expr": "x3_50#69",
"arith_op": "add",
"delta": "-0x3df3530",
"delta_int": -64959792,
"arg_name": "arg11",
"base_reg": "x14",
"load_off": 144,
"load_off_hex": "0x90",
"load_size": 8,
"pattern": "load_plus_const",
"load_expr": "[arg11#0 + 0x90].q @ mem#38",
"addr_expr": "arg11#0 + 0x90",
"final_expr": "x26_15#21 - 0x3df3530"
}
];
let installed = false;
const installedSet = new Set();
function toInt(v) {
if (typeof v === 'number') return v;
if (v === null || v === undefined) return 0;
const s = String(v).trim().toLowerCase();
if (s.startsWith('-0x')) return -parseInt(s.slice(3), 16);
if (s.startsWith('0x')) return parseInt(s, 16);
return parseInt(s, 10);
}
function hexSigned(v) {
if (v === null || v === undefined) return '0x0';
if (v < 0) return '-0x' + (-v).toString(16);
return '0x' + v.toString(16);
}
function logLocal(s) {
console.log(s);
try {
send({ type: 'log', data: s });
} catch (_) {
}
}
function sendJumpResult(obj) {
try {
send({ type: 'jump', data: obj });
} catch (_) {
}
}
function readLoadedValue(basePtr, off, size) {
const p = basePtr.add(off);
if (size === 1) return ptr(p.readU8());
if (size === 2) return ptr(p.readU16());
if (size === 4) return ptr(p.readU32());
return p.readPointer();
}
function calcTarget(enc, op, delta) {
if (op === 'sub') {
if (delta >= 0) return enc.sub(delta);
return enc.add(-delta);
}
if (op === 'add') {
if (delta >= 0) return enc.add(delta);
return enc.sub(-delta);
}
return enc;
}
function normalizePattern(rec) {
const jumpOff = toInt(rec.jump_off);
const loadOff = rec.load_off !== undefined ? toInt(rec.load_off) : toInt(rec.load_off_hex);
const delta = rec.delta_int !== undefined ? rec.delta_int : toInt(rec.delta);
return {
func_name: rec.func_name || 'unknown',
jump_addr: rec.jump_addr || null,
jump_off: jumpOff,
jump_off_hex: rec.jump_off || hexSigned(jumpOff),
base_reg: rec.base_reg,
arg_name: rec.arg_name || null,
load_off: loadOff,
load_off_hex: rec.load_off_hex || hexSigned(loadOff),
load_size: rec.load_size || 8,
arith_op: rec.arith_op || 'none',
delta: delta,
delta_hex: rec.delta || hexSigned(delta),
jump_expr: rec.jump_expr || '',
load_expr: rec.load_expr || '',
addr_expr: rec.addr_expr || '',
final_expr: rec.final_expr || '',
pattern: rec.pattern || ''
};
}
function installHooks() {
const m = Process.findModuleByName(soName);
if (!m) return false;
if (installed) return true;
installed = true;
const normalized = patterns.map(normalizePattern);
logLocal(`[+] installing ${normalized.length} hooks for ${soName}, base=${m.base}`);
for (const rec of normalized) {
try {
if (!rec.base_reg) {
logLocal(`[!] skip ${rec.jump_off_hex}: no base_reg`);
continue;
}
const key = `${rec.jump_off_hex}:${rec.base_reg}:${rec.load_off_hex}:${rec.delta_hex}:${rec.arith_op}`;
if (installedSet.has(key)) continue;
installedSet.add(key);
const addr = m.base.add(rec.jump_off);
Interceptor.attach(addr, {
onEnter() {
try {
const basePtr = this.context[rec.base_reg];
if (basePtr === undefined || basePtr === null) {
logLocal(`[!] missing ${rec.base_reg} @ ${rec.jump_off_hex}`);
return;
}
const enc = readLoadedValue(basePtr, rec.load_off, rec.load_size);
const target = calcTarget(enc, rec.arith_op, rec.delta);
const targetOff = target.sub(m.base);
logLocal('');
logLocal('==== indirect jump hit ====');
logLocal(`[+] func = ${rec.func_name}`);
logLocal(`[+] jump_off = ${rec.jump_off_hex}`);
logLocal(`[+] jump_addr = ${addr}`);
logLocal(`[+] base_reg = ${rec.base_reg}`);
logLocal(`[+] base_ptr = ${basePtr}`);
logLocal(`[+] load_off = ${rec.load_off_hex}`);
logLocal(`[+] enc = ${enc}`);
logLocal(`[+] op = ${rec.arith_op}`);
logLocal(`[+] delta = ${rec.delta_hex}`);
logLocal(`[+] target = ${target}`);
logLocal(`[+] target_off = ${targetOff}`);
logLocal('===========================');
logLocal('');
sendJumpResult({
func_name: rec.func_name,
jump_off: rec.jump_off_hex,
jump_addr: String(addr),
base_reg: rec.base_reg,
base_ptr: String(basePtr),
arg_name: rec.arg_name,
load_off: rec.load_off_hex,
load_size: rec.load_size,
enc: String(enc),
arith_op: rec.arith_op,
delta: rec.delta_hex,
target: String(target),
target_off: String(targetOff),
pattern: rec.pattern,
jump_expr: rec.jump_expr,
load_expr: rec.load_expr,
addr_expr: rec.addr_expr,
final_expr: rec.final_expr
});
} catch (e) {
logLocal(`[!] hook failed @ ${rec.jump_off_hex}: ${e}`);
logLocal(` reg=${rec.base_reg} load_off=${rec.load_off_hex} delta=${rec.delta_hex} op=${rec.arith_op}`);
}
}
});
} catch (e) {
logLocal(`[!] attach failed @ ${rec.jump_off_hex}: ${e}`);
}
}
return true;
}
if (!installHooks()) {
const android_dlopen_ext = Module.findGlobalExportByName('android_dlopen_ext');
const dlopen = Module.findGlobalExportByName('dlopen');
function watch(sym) {
if (!sym) return;
Interceptor.attach(sym, {
onEnter(args) {
this.path = args[0].isNull() ? '' : args[0].readCString();
},
onLeave() {
if (this.path.indexOf(soName) !== -1) {
logLocal(`[+] detected load: ${this.path}`);
installHooks();
}
}
});
}
watch(android_dlopen_ext);
watch(dlopen);
}
python 交互
1 | |
hook 结果
1 | |
批量设置跳转地址
hook 日志中有大量重复的内容,为了防止 patch 时重复设置值,读取
.jsonl 文件的记录后先按 (jump_off, target_off)
去重
核心 patch 用到了
set_user_var_value(var, definition_site, value) 这个
api,在 mlil 层批量设置指定的跳转地址
1 | |

成功设置 patch
需要注意的是,每次修好一个跳转后,bn 可能会分析出新的跳转,所以上面的操作需要多次执行,直到没有新的跳转结构被识别出来为止
后续遇到不同的跳转结构就修改识别规则,重复上面的流程
sub_89240
这个函数就是按照上面的步骤能够完全将间接跳转修好的,修好之后能看出它是被控制流平坦化了的

而且也是有多个分发器相互嵌套的


HMAC-MD5
把这条链路的间接跳转大致全部修好之后,通过反编译的内容结合 hook 分析出这是一个 HMAC-MD5 实现
1 | |
贴一小段 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[MD5 update] ctx=0x7d8aacde90 len=64
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 19 da 55 e5 b3 33 ce 4d 85 ea c4 ed 14 39 aa 05 ..U..3.M.....9..
00000010 12 19 94 b4 0f 5c 88 d9 25 90 19 bf 14 a9 b7 73 .....\..%......s
00000020 11 eb ee 0f 5a f7 57 dc f4 fa 44 f2 bf 3a 24 50 ....Z.W...D..:$P
00000030 ce ab b4 d9 73 d6 9c c8 75 5c 58 38 11 27 bc 4a ....s...u\X8.'.J
[MD5 update] ctx=0x7d8aacdb00 len=64
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 73 b0 3f 8f d9 59 a4 27 ef 80 ae 87 7e 53 c0 6f s.?..Y.'....~S.o
00000010 78 73 fe de 65 36 e2 b3 4f fa 73 d5 7e c3 dd 19 xs..e6..O.s.~...
00000020 7b 81 84 65 30 9d 3d b6 9e 90 2e 98 d5 50 4e 3a {..e0.=......PN:
00000030 a4 c1 de b3 19 bc f6 a2 1f 36 32 52 7b 4d d6 20 .........62R{M.
[MD5 update] ctx=0x7d8aace130 len=97
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 2f 61 70 69 2f 68 74 74 70 64 6e 73 2f 70 72 65 /api/httpdns/pre
00000010 66 65 74 63 68 70 6c 61 74 66 6f 72 6d 3d 61 6e fetchplatform=an
00000020 64 72 6f 69 64 26 62 75 69 6c 64 3d 39 31 39 33 droid&build=9193
00000030 38 30 33 26 64 65 76 69 63 65 49 64 3d 38 64 65 803&deviceId=8de
00000040 33 65 63 37 62 2d 62 61 61 30 2d 33 37 35 66 2d 3ec7b-baa0-375f-
00000050 62 64 33 66 2d 30 30 36 64 64 36 62 36 35 33 32 bd3f-006dd6b6532
00000060 35 5
[MD5 update] ctx=0x7d8aace130 len=16
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 77 df 88 7f 83 7d cf 67 6b 25 a3 b6 1b 59 ca 27 w....}.gk%...Y.'
[MD5 final] ctx=0x7ceaa634d0
digest:
00000000 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 5c .z.9].uw....dnc\
比对后可知 hook 得到的最后的 digest 就是在最前面的 RC4 中未知的那 16 字节
观察这个输出的前两个块,它们逐字节异或得到的全是 0x6a,而
HMAC 中的两个固定常量 ipad(0x36) 和 opad(0x5c)
相互异或的结果正好是 0x6a,加之这两个块在 hook
日志中重复出现,基本可以确定这两个块是
A = K ⊕ ipad,B = K ⊕ opad,但是问题又来了,要是按照一般的
HMAC-MD5 实现的话,也就是
1 | |
但是经过验证,这样得到的结果和 hook 到的 digest 是不一致的,另外,在 final digest 前还有 16 字节的输出还没弄清来源
将中间状态也 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
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549const soName = 'libxyass.so';
let installed = false;
let watchedLoader = false;
const OFF_REAL_UPDATE = 0x868a8;
const OFF_FINAL_FUNC = 0x85200;
const OFF_RESTORE_CTX = 0x86438;
const OFF_TRANSFORM = 0x86e44;
const pendingByImpl = Object.create(null);
const finalThreadState = Object.create(null);
const hookedVirtualFns = new Set();
const recentUpdates = [];
function log(s) {
console.log(s);
}
function nowMs() {
return Date.now();
}
function ptrSafe(v) {
try {
return ptr(v);
} catch (_) {
return null;
}
}
function safeReadPointer(p) {
try {
return p.readPointer();
} catch (_) {
return null;
}
}
function safeReadU32(p) {
try {
return p.readU32();
} catch (_) {
return null;
}
}
function safeReadS32(p) {
try {
return p.readS32();
} catch (_) {
return null;
}
}
function safeReadByteArray(p, len) {
try {
return p.readByteArray(len);
} catch (_) {
return null;
}
}
function hexOfByteArray(ab) {
const u8 = new Uint8Array(ab);
let out = '';
for (let i = 0; i < u8.length; i++) {
const x = u8[i].toString(16);
out += (x.length === 1 ? '0' : '') + x;
}
return out;
}
function hexdumpShort(ptrOrBuf, len) {
try {
return hexdump(ptrOrBuf, {
offset: 0,
length: len,
header: true,
ansi: false
});
} catch (_) {
return '<hexdump failed>';
}
}
function ensureArray(map, key) {
if (!map[key]) map[key] = [];
return map[key];
}
function recordUpdate(impl, len, hex) {
ensureArray(pendingByImpl, impl).push({
impl,
len,
hex,
ts: nowMs()
});
recentUpdates.push({
impl,
len,
hex,
ts: nowMs()
});
if (recentUpdates.length > 500) {
recentUpdates.shift();
}
}
function clearImplPending(impl) {
if (impl && pendingByImpl[impl]) {
delete pendingByImpl[impl];
}
}
function getModuleBase() {
const m = Process.findModuleByName(soName);
return m ? m.base : null;
}
function resolveImplFromWrapper(wrapperPtr) {
const wrapper = ptrSafe(wrapperPtr);
if (!wrapper || wrapper.isNull()) return null;
const holderPtr = safeReadPointer(wrapper.add(0x48));
if (!holderPtr || holderPtr.isNull()) return null;
const implPtr = safeReadPointer(holderPtr.add(Process.pointerSize));
if (!implPtr || implPtr.isNull()) return null;
return implPtr.toString();
}
function resolveRawCtxFromImpl(implPtrOrStr) {
const impl = typeof implPtrOrStr === 'string' ? ptr(implPtrOrStr) : ptrSafe(implPtrOrStr);
if (!impl || impl.isNull()) return null;
const rawCtx = safeReadPointer(impl.add(0x10));
if (!rawCtx || rawCtx.isNull()) return null;
return rawCtx;
}
function readRawCtxState(rawCtxPtr) {
const p = ptrSafe(rawCtxPtr);
if (!p || p.isNull()) return null;
const s0 = safeReadU32(p.add(0x00));
const s1 = safeReadU32(p.add(0x04));
const s2 = safeReadU32(p.add(0x08));
const s3 = safeReadU32(p.add(0x0c));
const bitLo = safeReadU32(p.add(0x10));
const bitHi = safeReadU32(p.add(0x14));
const buffered = safeReadU32(p.add(0x58)); // ctx[0x16]
return {
rawCtx: p.toString(),
s0, s1, s2, s3,
bitLo, bitHi,
buffered
};
}
function fmtU32(x) {
if (x === null || x === undefined) return 'null';
return '0x' + (x >>> 0).toString(16).padStart(8, '0');
}
function logRawCtxState(prefix, rawCtxPtr) {
const st = readRawCtxState(rawCtxPtr);
if (!st) {
log(`${prefix} rawCtx=<null>`);
return;
}
log(
`${prefix} rawCtx=${st.rawCtx}` +
` A=${fmtU32(st.s0)}` +
` B=${fmtU32(st.s1)}` +
` C=${fmtU32(st.s2)}` +
` D=${fmtU32(st.s3)}` +
` bitLo=${fmtU32(st.bitLo)}` +
` bitHi=${fmtU32(st.bitHi)}` +
` buffered=${st.buffered}`
);
}
function sendCase(payload) {
try {
send(payload);
} catch (e) {
log('[send failed] ' + e);
}
}
function resolveVirtualPlus18(objPtrOrStr) {
const obj = typeof objPtrOrStr === 'string' ? ptr(objPtrOrStr) : ptrSafe(objPtrOrStr);
if (!obj || obj.isNull()) return null;
const vtable = safeReadPointer(obj);
if (!vtable || vtable.isNull()) return null;
const fn = safeReadPointer(vtable.add(0x18));
if (!fn || fn.isNull()) return null;
return fn;
}
function installVirtualMethodHookIfNeeded(fnPtr) {
const key = fnPtr.toString();
if (hookedVirtualFns.has(key)) return;
hookedVirtualFns.add(key);
log(`[*] hook virtual +0x18 method @ ${key}`);
Interceptor.attach(fnPtr, {
onEnter(args) {
const tid = Process.getCurrentThreadId();
const st = finalThreadState[tid];
this.tid = tid;
this.obj = ptrSafe(args[0]);
this.arg1 = ptrSafe(args[1]);
this.arg2 = args[2];
this.captureMode = null;
this.inTrackedFinal = false;
if (!st) return;
if (!this.obj || this.obj.isNull()) return;
if (st.impl !== this.obj.toString()) return;
this.inTrackedFinal = true;
let arg2Num = null;
try {
arg2Num = this.arg2.toUInt32();
} catch (_) {
arg2Num = null;
}
if (this.arg1 && !this.arg1.isNull() && arg2Num !== 16) {
this.captureMode = 'maybe_final_to_tmp';
}
if (this.arg1 && !this.arg1.isNull() && arg2Num === 16) {
this.captureMode = 'maybe_update_16';
}
if (this.inTrackedFinal) {
const rawCtx = resolveRawCtxFromImpl(this.obj);
logRawCtxState(`[VFUNC_ENTER ${this.captureMode}] impl=${this.obj}`, rawCtx);
}
},
onLeave(retval) {
if (!this.inTrackedFinal || !this.captureMode) return;
const tid = this.tid;
const st = finalThreadState[tid];
if (!st) return;
let ok = false;
try {
ok = retval.toInt32() !== 0;
} catch (_) {
ok = true;
}
if (!ok) return;
const rawCtx = resolveRawCtxFromImpl(this.obj);
logRawCtxState(`[VFUNC_LEAVE ${this.captureMode}] impl=${this.obj}`, rawCtx);
if (this.captureMode === 'maybe_final_to_tmp') {
const ab = safeReadByteArray(this.arg1, 16);
if (!ab) return;
const hex = hexOfByteArray(ab);
st.suspectedInnerBuf = this.arg1.toString();
st.suspectedInnerHex = hex;
log(`[INNER_CANDIDATE] tid=${tid} impl=${st.impl} buf=${this.arg1} hex=${hex}`);
log(hexdumpShort(this.arg1, 16));
}
if (this.captureMode === 'maybe_update_16') {
const ab = safeReadByteArray(this.arg1, 16);
if (!ab) return;
const hex = hexOfByteArray(ab);
log(`[UPDATE16_IN_FINAL] tid=${tid} impl=${st.impl} src=${this.arg1} hex=${hex}`);
log(hexdumpShort(this.arg1, 16));
}
}
});
}
function installHooks() {
if (installed) return true;
const base = getModuleBase();
if (!base) return false;
const realUpdate = base.add(OFF_REAL_UPDATE);
const finalFunc = base.add(OFF_FINAL_FUNC);
const restoreCtx = base.add(OFF_RESTORE_CTX);
const transform = base.add(OFF_TRANSFORM);
log(`[*] hooks installed @ ${base}`);
Interceptor.attach(transform, {
onEnter(args) {
this.rawCtx = ptrSafe(args[0]);
this.blockPtr = ptrSafe(args[1]);
logRawCtxState('[TRANSFORM_ENTER]', this.rawCtx);
if (this.blockPtr && !this.blockPtr.isNull()) {
log('[TRANSFORM_BLOCK]');
log(hexdumpShort(this.blockPtr, 64));
}
},
onLeave(_) {
logRawCtxState('[TRANSFORM_LEAVE]', this.rawCtx);
}
});
Interceptor.attach(realUpdate, {
onEnter(args) {
const implPtr = ptrSafe(args[0]);
const dataPtr = ptrSafe(args[1]);
const lenVal = args[2];
if (!implPtr || implPtr.isNull() || !dataPtr || dataPtr.isNull()) return;
let len = 0;
try {
len = lenVal.toInt32();
} catch (_) {
return;
}
if (len <= 0) return;
const ab = safeReadByteArray(dataPtr, len);
if (!ab) return;
const impl = implPtr.toString();
const hex = hexOfByteArray(ab);
recordUpdate(impl, len, hex);
const rawCtx = resolveRawCtxFromImpl(implPtr);
log(`[UPDATE] impl=${impl} len=${len} pendingCount=${pendingByImpl[impl].length}`);
logRawCtxState('[UPDATE_CTX_BEFORE]', rawCtx);
if (len <= 128) {
log(hexdumpShort(dataPtr, len));
}
this.impl = impl;
this.rawCtx = rawCtx;
this.len = len;
sendCase({
type: 'md5_update',
impl,
len,
hex
});
},
onLeave(_) {
if (this.rawCtx) {
logRawCtxState('[UPDATE_CTX_AFTER]', this.rawCtx);
}
}
});
Interceptor.attach(restoreCtx, {
onEnter(args) {
this.dst = ptrSafe(args[0]);
this.src = ptrSafe(args[1]);
const dstStr = this.dst ? this.dst.toString() : 'null';
const srcStr = this.src ? this.src.toString() : 'null';
log(`[RESTORE_CTX] dst=${dstStr} src=${srcStr}`);
logRawCtxState('[RESTORE_DST_BEFORE]', resolveRawCtxFromImpl(this.dst));
logRawCtxState('[RESTORE_SRC]', resolveRawCtxFromImpl(this.src));
},
onLeave(retval) {
let ok = false;
try {
ok = retval.toInt32() !== 0;
} catch (_) {
ok = true;
}
const dstStr = this.dst ? this.dst.toString() : 'null';
const srcStr = this.src ? this.src.toString() : 'null';
log(`[RESTORE_CTX_RET] ok=${ok} dst=${dstStr} src=${srcStr}`);
logRawCtxState('[RESTORE_DST_AFTER]', resolveRawCtxFromImpl(this.dst));
}
});
Interceptor.attach(finalFunc, {
onEnter(args) {
const tid = Process.getCurrentThreadId();
this.wrapperPtr = ptrSafe(args[0]);
this.outPtr = ptrSafe(args[1]);
this.outLenPtr = ptrSafe(args[2]);
this.wrapper = this.wrapperPtr ? this.wrapperPtr.toString() : 'null';
this.impl = resolveImplFromWrapper(this.wrapperPtr);
this.enterTs = nowMs();
const pendingAtEnter = this.impl && pendingByImpl[this.impl] ? pendingByImpl[this.impl].length : 0;
finalThreadState[tid] = {
wrapper: this.wrapper,
impl: this.impl,
enterTs: this.enterTs,
suspectedInnerBuf: null,
suspectedInnerHex: null
};
if (this.impl) {
const fn = resolveVirtualPlus18(this.impl);
if (fn) installVirtualMethodHookIfNeeded(fn);
}
log(`[FINAL_ENTER] tid=${tid} wrapper=${this.wrapper} impl=${this.impl} pendingAtEnter=${pendingAtEnter}`);
logRawCtxState('[FINAL_ENTER_CTX]', resolveRawCtxFromImpl(this.impl));
},
onLeave(retval) {
const tid = Process.getCurrentThreadId();
const st = finalThreadState[tid];
let ok = false;
try {
ok = retval.toInt32() !== 0;
} catch (_) {
ok = true;
}
if (!ok) {
log(`[FINAL_LEAVE] tid=${tid} wrapper=${this.wrapper} impl=${this.impl} ret=0`);
delete finalThreadState[tid];
return;
}
let digestHex = null;
if (this.outPtr && !this.outPtr.isNull()) {
const digestAb = safeReadByteArray(this.outPtr, 16);
if (digestAb) {
digestHex = hexOfByteArray(digestAb);
log(`[FINAL_DIGEST] tid=${tid} wrapper=${this.wrapper} impl=${this.impl} digest=${digestHex}`);
log(hexdumpShort(this.outPtr, 16));
}
}
logRawCtxState('[FINAL_LEAVE_CTX]', resolveRawCtxFromImpl(this.impl));
const items = (this.impl && pendingByImpl[this.impl]) ? pendingByImpl[this.impl].slice() : [];
const fullHex = items.map(x => x.hex).join('');
log(`[FINAL_LEAVE] tid=${tid} wrapper=${this.wrapper} impl=${this.impl} itemCount=${items.length}`);
log(`[FINAL_ITEMS] ${JSON.stringify(items.map(x => ({ impl: x.impl, len: x.len })))}`);
if (fullHex.length > 0) {
log(`[FULL_INPUT_LEN] ${fullHex.length / 2}`);
log(`[FULL_INPUT_HEX] ${fullHex}`);
} else {
log('[FULL_INPUT_LEN] 0');
}
sendCase({
type: 'md5_case',
wrapper: this.wrapper,
impl: this.impl,
digest: digestHex,
fullHex: fullHex,
items: items,
innerCandidateHex: st ? st.suspectedInnerHex : null,
innerCandidateBuf: st ? st.suspectedInnerBuf : null
});
if (this.impl) clearImplPending(this.impl);
delete finalThreadState[tid];
}
});
installed = true;
return true;
}
function watchDlopen() {
if (watchedLoader) return;
watchedLoader = true;
const android_dlopen_ext = Module.findGlobalExportByName('android_dlopen_ext');
const dlopen = Module.findGlobalExportByName('dlopen');
function watch(sym) {
if (!sym) return;
Interceptor.attach(sym, {
onEnter(args) {
this.path = '';
try {
this.path = args[0].isNull() ? '' : args[0].readCString();
} catch (_) {}
},
onLeave() {
if (!this.path) return;
if (this.path.indexOf(soName) === -1) return;
log(`[*] observed load of ${soName}: ${this.path}`);
try {
installHooks();
} catch (e) {
log('[!] installHooks after dlopen failed: ' + e);
}
}
});
}
watch(android_dlopen_ext);
watch(dlopen);
}
(function main() {
try {
if (!installHooks()) {
log(`[*] ${soName} not loaded yet, watching dlopen...`);
watchDlopen();
}
} catch (e) {
log('[!] main failed: ' + e);
}
})();
贴一段输出 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[UPDATE] impl=0x7d8aad4f40 len=64 pendingCount=1
[UPDATE_CTX_BEFORE] rawCtx=0x7dbaa91c90 A=0x10325476 B=0x98badcfe C=0xefcdab89 D=0x67452301 bitLo=0x00000000 bitHi=0x00000000 buffered=0
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a0e23c4f0 19 da 55 e5 b3 33 ce 4d 85 ea c4 ed 14 39 aa 05 ..U..3.M.....9..
7a0e23c500 12 19 94 b4 0f 5c 88 d9 25 90 19 bf 14 a9 b7 73 .....\..%......s
7a0e23c510 11 eb ee 0f 5a f7 57 dc f4 fa 44 f2 bf 3a 24 50 ....Z.W...D..:$P
7a0e23c520 ce ab b4 d9 73 d6 9c c8 75 5c 58 38 11 27 bc 4a ....s...u\X8.'.J
message: {'type': 'send', 'payload': {'type': 'md5_update', 'impl': '0x7d8aad4f40', 'len': 64, 'hex': '19da55e5b333ce4d85eac4ed1439aa05121994b40f5c88d9259019bf14a9b77311ebee0f5af757dcf4fa44f2bf3a2450ceabb4d973d69cc8755c58381127bc4a'}} data: None
[TRANSFORM_ENTER] rawCtx=0x7dbaa91c90 A=0x10325476 B=0x98badcfe C=0xefcdab89 D=0x67452301 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[TRANSFORM_BLOCK]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a0e23c4f0 19 da 55 e5 b3 33 ce 4d 85 ea c4 ed 14 39 aa 05 ..U..3.M.....9..
7a0e23c500 12 19 94 b4 0f 5c 88 d9 25 90 19 bf 14 a9 b7 73 .....\..%......s
7a0e23c510 11 eb ee 0f 5a f7 57 dc f4 fa 44 f2 bf 3a 24 50 ....Z.W...D..:$P
7a0e23c520 ce ab b4 d9 73 d6 9c c8 75 5c 58 38 11 27 bc 4a ....s...u\X8.'.J
[TRANSFORM_LEAVE] rawCtx=0x7dbaa91c90 A=0x941470d7 B=0x13fc57a3 C=0xcb2d64f4 D=0x2ca29856 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[UPDATE_CTX_AFTER] rawCtx=0x7dbaa91c90 A=0x941470d7 B=0x13fc57a3 C=0xcb2d64f4 D=0x2ca29856 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[UPDATE] impl=0x7d8aad50f0 len=64 pendingCount=1
[UPDATE_CTX_BEFORE] rawCtx=0x7dbaa8e9d0 A=0x10325476 B=0x98badcfe C=0xefcdab89 D=0x67452301 bitLo=0x00000000 bitHi=0x00000000 buffered=0
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a0e23c4f0 73 b0 3f 8f d9 59 a4 27 ef 80 ae 87 7e 53 c0 6f s.?..Y.'....~S.o
7a0e23c500 78 73 fe de 65 36 e2 b3 4f fa 73 d5 7e c3 dd 19 xs..e6..O.s.~...
7a0e23c510 7b 81 84 65 30 9d 3d b6 9e 90 2e 98 d5 50 4e 3a {..e0.=......PN:
7a0e23c520 a4 c1 de b3 19 bc f6 a2 1f 36 32 52 7b 4d d6 20 .........62R{M.
message:[TRANSFORM_ENTER] rawCtx=0x7dbaa8e9d0 A=0x10325476 B=0x98badcfe C=0xefcdab89 D=0x67452301 bitLo=0x00000200 bitHi=0x00000000 buffered=0
{'type': 'send', 'payload': {'type': 'md5_update', 'impl': '0x7d8aad50f0', 'len': 64, 'hex': '73b03f8fd959a427ef80ae877e53c06f7873fede6536e2b34ffa73d57ec3dd197b818465309d3db69e902e98d5504e3aa4c1deb319bcf6a21f3632527b4dd620'}} data: None
[TRANSFORM_BLOCK]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a0e23c4f0 73 b0 3f 8f d9 59 a4 27 ef 80 ae 87 7e 53 c0 6f s.?..Y.'....~S.o
7a0e23c500 78 73 fe de 65 36 e2 b3 4f fa 73 d5 7e c3 dd 19 xs..e6..O.s.~...
7a0e23c510 7b 81 84 65 30 9d 3d b6 9e 90 2e 98 d5 50 4e 3a {..e0.=......PN:
7a0e23c520 a4 c1 de b3 19 bc f6 a2 1f 36 32 52 7b 4d d6 20 .........62R{M.
[TRANSFORM_LEAVE] rawCtx=0x7dbaa8e9d0 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[UPDATE_CTX_AFTER] rawCtx=0x7dbaa8e9d0 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[RESTORE_CTX] dst=0x7d8aad4dc0 src=0x7d8aad4f40
[RESTORE_DST_BEFORE] rawCtx=<null>
[RESTORE_SRC] rawCtx=0x7dbaa91c90 A=0x941470d7 B=0x13fc57a3 C=0xcb2d64f4 D=0x2ca29856 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[RESTORE_CTX_RET] ok=true dst=0x7d8aad4dc0 src=0x7d8aad4f40
[RESTORE_DST_AFTER] rawCtx=0x7dbaa90090 A=0x941470d7 B=0x13fc57a3 C=0xcb2d64f4 D=0x2ca29856 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[UPDATE] impl=0x7d8aad4dc0 len=97 pendingCount=1
[UPDATE_CTX_BEFORE] rawCtx=0x7dbaa90090 A=0x941470d7 B=0x13fc57a3 C=0xcb2d64f4 D=0x2ca29856 bitLo=0x00000200 bitHi=0x00000000 buffered=0
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7d3aa7f6b0 2f 61 70 69 2f 68 74 74 70 64 6e 73 2f 70 72 65 /api/httpdns/pre
7d3aa7f6c0 66 65 74 63 68 70 6c 61 74 66 6f 72 6d 3d 61 6e fetchplatform=an
7d3aa7f6d0 64 72 6f 69 64 26 62 75 69 6c 64 3d 39 31 39 33 droid&build=9193
7d3aa7f6e0 38 30 33 26 64 65 76 69 63 65 49 64 3d 38 64 65 803&deviceId=8de
7d3aa7f6f0 33 65 63 37 62 2d 62 61 61 30 2d 33 37 35 66 2d 3ec7b-baa0-375f-
7d3aa7f700 62 64 33 66 2d 30 30 36 64 64 36 62 36 35 33 32 bd3f-006dd6b6532
7d3aa7f710 35 5
message: {'type': 'send', 'payload': {'type': 'md5_update', 'impl': '0x7d8aad4dc0', 'len': 97, 'hex': '2f6170692f68747470646e732f7072656665746368706c6174666f726d3d616e64726f6964266275696c643d393139333830332664657669636549643d38646533656337622d626161302d333735662d626433662d303036646436623635333235'}} data: None
[TRANSFORM_ENTER] rawCtx=0x7dbaa90090 A=0x941470d7 B=0x13fc57a3 C=0xcb2d64f4 D=0x2ca29856 bitLo=0x00000508 bitHi=0x00000000 buffered=0
[TRANSFORM_BLOCK]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7d3aa7f6b0 2f 61 70 69 2f 68 74 74 70 64 6e 73 2f 70 72 65 /api/httpdns/pre
7d3aa7f6c0 66 65 74 63 68 70 6c 61 74 66 6f 72 6d 3d 61 6e fetchplatform=an
7d3aa7f6d0 64 72 6f 69 64 26 62 75 69 6c 64 3d 39 31 39 33 droid&build=9193
7d3aa7f6e0 38 30 33 26 64 65 76 69 63 65 49 64 3d 38 64 65 803&deviceId=8de
[TRANSFORM_LEAVE] rawCtx=0x7dbaa90090 A=0x09449ef3 B=0xbdec25f4 C=0x5d2106a8 D=0x2171e099 bitLo=0x00000508 bitHi=0x00000000 buffered=0
[UPDATE_CTX_AFTER] rawCtx=0x7dbaa90090 A=0x09449ef3 B=0xbdec25f4 C=0x5d2106a8 D=0x2171e099 bitLo=0x00000508 bitHi=0x00000000 buffered=33
[*] hook virtual +0x18 method @ 0x7aa04959ac
[FINAL_ENTER] tid=21605 wrapper=0x7ceaa94070 impl=0x7d8aad4dc0 pendingAtEnter=1
[FINAL_ENTER_CTX] rawCtx=0x7dbaa90090 A=0x09449ef3 B=0xbdec25f4 C=0x5d2106a8 D=0x2171e099 bitLo=0x00000508 bitHi=0x00000000 buffered=33
[TRANSFORM_ENTER] rawCtx=0x7dbaa90090 A=0x09449ef3 B=0xbdec25f4 C=0x5d2106a8 D=0x2171e099 bitLo=0x00000508 bitHi=0x00000000 buffered=33
[TRANSFORM_BLOCK]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7dbaa900a8 33 65 63 37 62 2d 62 61 61 30 2d 33 37 35 66 2d 3ec7b-baa0-375f-
7dbaa900b8 62 64 33 66 2d 30 30 36 64 64 36 62 36 35 33 32 bd3f-006dd6b6532
7dbaa900c8 35 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 5...............
7dbaa900d8 00 00 00 00 00 00 00 00 08 05 00 00 00 00 00 00 ................
[TRANSFORM_LEAVE] rawCtx=0x7dbaa90090 A=0x7f88df77 B=0x67cf7d83 C=0xb6a3256b D=0x27ca591b bitLo=0x00000508 bitHi=0x00000000 buffered=33
[RESTORE_CTX] dst=0x7d8aad4dc0 src=0x7d8aad50f0
[RESTORE_DST_BEFORE] rawCtx=0x7dbaa90090 A=0x00000000 B=0x00000000 C=0x00000000 D=0x00000000 bitLo=0x00000000 bitHi=0x00000000 buffered=0
[RESTORE_SRC] rawCtx=0x7dbaa8e9d0 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[RESTORE_CTX_RET] ok=true dst=0x7d8aad4dc0 src=0x7d8aad50f0
[RESTORE_DST_AFTER] rawCtx=0x7dbaa90090 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000200 bitHi=0x00000000 buffered=0
[UPDATE] impl=0x7d8aad4dc0 len=16 pendingCount=2
[UPDATE_CTX_BEFORE] rawCtx=0x7dbaa90090 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000200 bitHi=0x00000000 buffered=0
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a0e23c548 77 df 88 7f 83 7d cf 67 6b 25 a3 b6 1b 59 ca 27 w....}.gk%...Y.'
message: [UPDATE_CTX_AFTER] rawCtx=0x7dbaa90090 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000280 bitHi=0x00000000 buffered=16
{'type': 'send', 'payload': {'type': 'md5_update', 'impl': '0x7d8aad4dc0', 'len': 16, 'hex': '77df887f837dcf676b25a3b61b59ca27'}} [VFUNC_ENTER maybe_update_16] impl=0x7d8aad4dc0 rawCtx=0x7dbaa90090 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000280 bitHi=0x00000000 buffered=16
data: None
[TRANSFORM_ENTER] rawCtx=0x7dbaa90090 A=0xd17e9742 B=0xae04d549 C=0xe6394cc0 D=0x453288e6 bitLo=0x00000280 bitHi=0x00000000 buffered=16
[TRANSFORM_BLOCK]
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7dbaa900a8 77 df 88 7f 83 7d cf 67 6b 25 a3 b6 1b 59 ca 27 w....}.gk%...Y.'
7dbaa900b8 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7dbaa900c8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7dbaa900d8 00 00 00 00 00 00 00 00 80 02 00 00 00 00 00 00 ................
[TRANSFORM_LEAVE] rawCtx=0x7dbaa90090 A=0x39d07af1 B=0x7775af5d C=0x1a9dfeb9 D=0x5c636e64 bitLo=0x00000280 bitHi=0x00000000 buffered=16
[VFUNC_LEAVE maybe_update_16] impl=0x7d8aad4dc0 rawCtx=0x7dbaa90090 A=0x39d07af1 B=0x7775af5d C=0x1a9dfeb9 D=0x5c636e64 bitLo=0x00000280 bitHi=0x00000000 buffered=0
[UPDATE16_IN_FINAL] tid=21605 impl=0x7d8aad4dc0 src=0x7a0e23c628 hex=f17ad0395daf7577b9fe9d1a646e635c
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a0e23c628 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 5c .z.9].uw....dnc\
[FINAL_DIGEST] tid=21605 wrapper=0x7ceaa94070 impl=0x7d8aad4dc0 digest=f17ad0395daf7577b9fe9d1a646e635c
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7a0e23c628 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 5c .z.9].uw....dnc\
[FINAL_LEAVE_CTX] rawCtx=0x7dbaa90090 A=0x00000000 B=0x00000000 C=0x00000000 D=0x00000000 bitLo=0x00000000 bitHi=0x00000000 buffered=0
[FINAL_LEAVE] tid=21605 wrapper=0x7ceaa94070 impl=0x7d8aad4dc0 itemCount=2
[FINAL_ITEMS] [{"impl":"0x7d8aad4dc0","len":97},{"impl":"0x7d8aad4dc0","len":16}]
[FULL_INPUT_LEN] 113
[FULL_INPUT_HEX] 2f6170692f68747470646e732f7072656665746368706c6174666f726d3d616e64726f6964266275696c643d393139333830332664657669636549643d38646533656337622d626161302d333735662d626433662d30303664643662363533323577df887f837dcf676b25a3b61b59ca27
message: {'type': 'send', 'payload': {'type': 'md5_case', 'wrapper': '0x7ceaa94070', 'impl': '0x7d8aad4dc0', 'digest': 'f17ad0395daf7577b9fe9d1a646e635c', 'fullHex': '2f6170692f68747470646e732f7072656665746368706c6174666f726d3d616e64726f6964266275696c643d393139333830332664657669636549643d38646533656337622d626161302d333735662d626433662d30303664643662363533323577df887f837dcf676b25a3b61b59ca27', 'items': [{'impl': '0x7d8aad4dc0', 'len': 97, 'hex': '2f6170692f68747470646e732f7072656665746368706c6174666f726d3d616e64726f6964266275696c643d393139333830332664657669636549643d38646533656337622d626161302d333735662d626433662d303036646436623635333235', 'ts': 1775451374304}, {'impl': '0x7d8aad4dc0', 'len': 16, 'hex': '77df887f837dcf676b25a3b61b59ca27', 'ts': 1775451374312}], 'innerCandidateHex': None, 'innerCandidateBuf': None}} data: None
看到了 MD5 IV,以逆序存放
A=0x10325476 B=0x98badcfe C=0xefcdab89 D=0x67452301
同时也看到了 inner = md5(A + message)
1 | |
这里的 A B C D 拼起来
inner = 77df887f837dcf676b25a3b61b59ca27 正是最终摘要前的
16 字节
之后计算 md5(B + inner)
1 | |
这个输出就是 final degist
不过按照标准 md5 还原的话与 hook 的结果是不一致的,应该做了魔改处理,这里就不展开了,总的链路已经梳理的差不多了,剩下的就是纯粹的算法逆向了
之后分析出来的结果这个 md5 除了上面的 IV
逆序外,旋转方向和位数也变了,第一轮右移的参数是
26,19,15,11,25,20,15,12,25,20,16,10,25,19,15,10,等价于左移
6,13,17,21,7,12,17,20,7,12,16,22,7,13,17,22,而标准的 md5
第一轮是左移 7,12,17,22 重复
另外它的 md5 k 常量表也与标准的不一样
1 | |
魔改 AES
除此之外,上面 HMAC 中的 A B 块的数据异或前也是经过了加密的,因为 A B 块本身是通过一个 key 分别异或 0x36 和 0x5c 得到的,而这个 key 就是通过一个魔改 AES 得到的,其实就是白盒 AES
先用 deviceId 派生 AES-128 的 key,步骤如下:
1 | |
然后进行密钥拓展,用了 TBox_1 ~ TBox_4 来模拟 AES 的字节替换、循环左移和轮密钥加的操作
1 | |
本质上就是在做
1 | |
最后把加密轮密钥变成解密轮密钥,先反转轮密钥顺序,再用 TBox_2 + TBox_5~8 预处理成解密轮密钥
在 .rodata 可以找到 8 个 T 盒,每个都是 1024 字节
0x18198 处 TBox_1:
1 | |
0x18598 处 TBox_2:
1 | |
0x18998 处 TBox_3:
1 | |
0x18d98 处 TBox_4:
1 | |
0x17098 处 TBox_5:
1 | |
0x17498 处 TBox_6:
1 | |
0x17898 处 TBox_7:
1 | |
0x17c98 处 TBox_8:
1 | |
之后的对提取出来的 hmac_string 进行解密,先将其进行 Base64
解码,得到原始密文再转成 hex,一共 96 字节,在进行 aes 解密,每轮用
TBox_5 ~ TBox_8 查表,完成 AES
的字节替换、行移位、列混淆、轮密钥加的步骤,并且是 CBC
模式,有块异或的操作,初始 IV 是固定值
31 01 32 34 04 02 08 61 66 7A 66 66 07 17 66 39,最后一轮用逆
S 盒完成字节替换,行移位,轮密钥加的步骤,所有 6 个块解完后去掉前 16
字节和后 16 字节,只保留中间的 64 字节,再后续的 HMAC-MD5 中去异或 0x36
和 0x5c 分别得到 ipad 和 opad
总流程
原始数据:build_id 即 APP 版本号 、device_id 即设备 uuid 、hmac_string (从 data/data/com.xingin.xhs/shared_prefs/s.xml 中提取)
对 build_id 进行密钥拓展得到 AES 解密轮密钥, hmac_string 经过魔改
AES 解密得到 64 字节和 message 一起传入 HMAC-MD5 得到 16
字节摘要,最后是一个 RC4 + Base64,最后的生成结果前加上 XY
就是 shield 的最终参数