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
jstring NewStringUTF(JNIEnv *env, const char *bytes);

它会根据传入的 bytes 构造一个新的 java.lang.String, 并且它在 JNIEnv 函数表里的索引是 167

由此可以写出 hook NewStringUTF 的 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
function hook_jni_functions() {
Java.perform(() => {
// 获取 JNIEnv 指针
const env = Java.vm.getEnv().handle;
// JNIEnv 指针指向的第一项是函数表地址
const jni_table = env.readPointer();

// NewStringUTF 在函数表中的索引是 167 (32位和64位通用)
const newStringUTF_addr = jni_table.add(167 * Process.pointerSize).readPointer();

console.log("[+] NewStringUTF address: " + newStringUTF_addr);

Interceptor.attach(newStringUTF_addr, {
onEnter: function(args) {
// args[0] = JNIEnv*, args[1] = char*
const content = args[1]?.readUtf8String();
// if (args[1]) {
// // console.log("[String]: " + args[1].readUtf8String());
// }
if(content && content.indexOf("XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9")!==-1){
console.log("[+] Target Str: "+ content);
const bt = Thread.backtrace(this.context,Backtracer.ACCURATE);
for(let i = 0;i < bt.length;i++){
const addr = bt[i];
if(addr){
const module = Process.findModuleByAddress(addr);
if(module){
if(!module.path.includes("/system/")&&
!module.path.includes("/apex")&&
!module.path.includes("libart.so")){
console.log("[+] Success find target so");
console.log("[*] name: " + module.name);
console.log("[*] base: " + module.base);
console.log("[*] offset: " + addr?.sub(module.base));
console.log("[*] path: " + module.path);

break;
}
}
}
}

}
}
});
});
}

但是尝试之后发现有 frida 检测,直接 Process terminated 了

开始以为是 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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
[+] Target Str hit #1
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOFkq6zm/xTkni2FplIyTZTn
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #2
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOEtZZu1XoTf9PS0gVq4KKAv
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #3
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOGxNco302Ku/Xc5nAvbC6SZ
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #4
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOFiavxD7u26SgoZo/k/le/z
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #5
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOEuDtXyZ9NvlXwptYNmwHDV
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #6
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOGmUsrCaoUedrnEa+TUhAhC
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #7
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOG9KYKh2Vq1JgAURXQQotdU
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #8
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOHb1YCiTRGYtE/i66qrHzAO
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #9
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOHb1YCiTRGYtE/i66qrHzAO
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #10
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOGn+kxsjOcY2iCZgIDuNctb
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #11
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOGn+kxsjOcY2iCZgIDuNctb
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #12
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOEhApRW3JkvcQFYaC+dFfKb
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

[+] Target Str hit #13
[+] content: XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOE8e3sQfWG9kLLln9Ysi4br
[+] returnAddress: 0x78aff949fc
[+] caller module: libxyass.so
[+] caller base : 0x78aff4e000
[+] caller off : 0x469fc
[+] caller path : /data/app/~~aFy_2OaJJ9y0paWz5G_j2Q==/com.xingin.xhs-iJaR9BcYh84YvK07PTu_dA==/lib/arm64/libxyass.so

由此可以定位到要分析的参数在 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
64
function 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
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
00046db0    size_t sub_46db0(int64_t arg1, int64_t arg2, int32_t* arg3, char* arg4 @ x8)

00046dc4 uint64_t x22 = _ReadMSR(SystemReg: tpidr_el0)
00046dc8 int64_t x9 = *(x22 + 0x28)
00046de0 int64_t x8
00046de0
00046de0 if (*arg3 s< 6)
00046df4 x8 = 0x3fb6bd9c // sub_47338
00046de0 else
00046de8 x8 = 0x3fb6ba4c // sub_46fe8
00046de8
00046e0c (x8 - 0x3fb24a64)(arg1, arg2, arg3)
00046e1c char var_80
00046e1c sub_1e310(&var_80, &data_9b510)
00046e24 int32_t x21 = data_9b630
00046e28 uint64_t x20_1 = zx.q(*arg3)
00046e38 char var_98
00046e38 sub_1e310(&var_98, &data_9b4f8)
00046e3c char var_50
00046e3c uint64_t x8_1 = zx.q(var_50)
00046e54 int32_t temp0 = x8_1.d & 1
00046e58 int64_t var_40
00046e58 int64_t x5
00046e58
00046e58 x5 = temp0 == 0 ? &var_50 | 1 : var_40
00046e58
00046e5c uint32_t x6
00046e5c uint32_t var_48
00046e5c
00046e5c x6 = temp0 == 0 ? (x8_1 u>> 1).d : var_48
00046e78 char var_68
00046e78 BN_CODE_start_0x4b8c8_size_0x38738(1, &var_80, x21, x20_1.d, &var_98, x5, x6,
00046e78 &var_68)
00046e80 int64_t var_88
00046e80
00046e80 if ((zx.d(var_98) & 1) != 0)
00046e88 sub_1e0f4(var_88)
00046e90 int64_t var_70
00046e90
00046e90 if ((zx.d(var_80) & 1) != 0)
00046e98 sub_1e0f4(var_70)
00046e98
00046eb8 if ((zx.d(data_9b600) & 1) == 0 && sub_1ee3c(&data_9b600, x20_1) != 0)
00046f30 int16_t* x0_8 = sub_1e0ec(3)
00046f44 x0_8[1].b = 0xd6
00046f48 *x0_8 = 0x33f7
00046f4c sub_446ac(x0_8, 3)
00046f64 data_9b608 = x0_8
00046f68 sub_1ef98(&data_9b600)
00046f68
00046ed4 size_t result = sub_1e724(data_9b608, &var_68, arg4)
00046edc int64_t var_58
00046edc
00046edc if ((zx.d(var_68) & 1) != 0)
00046ee4 result = sub_1e0f4(var_58)
00046ee4
00046eec if ((zx.d(var_50) & 1) != 0)
00046ef4 result = sub_1e0f4(var_40)
00046ef4
00046f04 if (*(x22 + 0x28) == x9)
00046f18 return result
00046f18
00046f70 __stack_chk_fail()
00046f70 noreturn

sub_4b8c8(RC4 + Base64)

Base64

可以看到这个函数有很多个参数并且最后面有个明显的 base64

算输出长度

Base64 字符表

sub_1e3c8

sub_4b8c8 中多次调用的这个函数

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
0001e3c8    char* sub_1e3c8(char* arg1, int64_t arg2, size_t arg3)

0001e3dc uint64_t x8 = zx.q(*arg1)
0001e3ec int64_t x1
0001e3ec uint64_t x21
0001e3ec
0001e3ec if ((x8.d & 1) != 0)
0001e3fc x21 = *(arg1 + 8)
0001e404 x1 = (*arg1 & 0xfffffffffffffffe) - 1
0001e3ec else
0001e3f0 x21 = x8 u>> 1
0001e3f4 x1 = 0x16
0001e3f4
0001e410 if (x1 - x21 u< arg3)
0001e430 sub_1e1d4(arg1, x1, x21 + arg3 - x1, x21, x21, 0, arg3, arg2)
0001e410 else if (arg3 != 0)
0001e43c char* x22_1
0001e43c
0001e43c if ((x8.d & 1) != 0)
0001e448 x22_1 = *(arg1 + 0x10)
0001e43c else
0001e440 x22_1 = &arg1[1]
0001e440
0001e458 memcpy(&x22_1[x21], arg2, arg3)
0001e460 int64_t x8_2 = x21 + arg3
0001e460
0001e464 if ((zx.d(*arg1) & 1) != 0)
0001e474 *(arg1 + 8) = x8_2
0001e464 else
0001e46c *arg1 = (x8_2.d << 1).b
0001e46c
0001e478 x22_1[x8_2] = 0
0001e478
0001e490 return arg1

arg1:目标字符串对象,arg2:要追加的数据地址,arg3:要追加的数据长度

先判断 arg1 当前是短模式还是长模式,取出当前长度 size 和当前容量 capacity,判断剩余容量够不够追加 arg3 字节,够的话直接把 arg2 指向的数据复制到末尾,不够的话调用 sub_1e1d4 做扩容并追加,然后更新长度字段,在新末尾补 '\0',返回 arg1

就是 append string 的逻辑

sub_1e070

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0001e070    int64_t sub_1e070(size_t arg1)

0001e084 size_t bytes
0001e084
0001e084 bytes = arg1 != 0 ? arg1 : 1
0001e084
0001e08c while (true)
0001e08c int64_t result = malloc(bytes)
0001e08c
0001e090 if (result != 0)
0001e0b0 return result
0001e0b0
0001e094 int64_t x0_1 = sub_1f2ac()
0001e094
0001e098 if (x0_1 == 0)
0001e098 break
0001e098
0001e09c x0_1()
0001e09c
0001e0b8 struct std::exception::std::bad_alloc::VTable** x0_2 = sub_1e9e0(8)
0001e0c0 sub_34dc8(x0_2)
0001e0d8 sub_1ea58(x0_2, &_typeinfo_for_std::bad_alloc, sub_34da8)
0001e0d8 noreturn

相当于 C++ 里的 operator new(size_t) / allocator 的底层分配函数

sub_1e494

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
0001e494    char* sub_1e494(char* arg1, int64_t arg2, int64_t arg3)

0001e4a8 uint64_t x8 = zx.q(*arg1)
0001e4b0 uint64_t x9
0001e4b0
0001e4b0 if ((x8.d & 1) != 0)
0001e4bc x9 = *(arg1 + 8)
0001e4b0 else
0001e4b4 x9 = x8 u>> 1
0001e4b4
0001e4c4 if (x9 u< arg2)
0001e56c sub_1e168()
0001e56c noreturn
0001e56c
0001e4c8 if (arg3 != 0)
0001e4d0 if (arg3 != -1)
0001e4ec char* x20_1
0001e4ec uint64_t x21_1
0001e4ec
0001e4ec if ((x8.d & 1) != 0)
0001e50c x21_1 = *(arg1 + 8)
0001e50c x20_1 = *(arg1 + 0x10)
0001e4ec else
0001e4f0 x21_1 = x8 u>> 1
0001e4f4 x20_1 = &arg1[1]
0001e4f4
0001e510 int64_t x9_2 = x21_1 - arg2
0001e518 int64_t x22_1
0001e518
0001e518 x22_1 = x9_2 u< arg3 ? x9_2 : arg3
0001e518
0001e520 if (x9_2 != x22_1)
0001e524 void* x0 = &x20_1[arg2]
0001e52c memmove(x0, x0 + x22_1, x9_2 - x22_1)
0001e530 x8 = zx.q(*arg1)
0001e530
0001e534 int64_t x9_3 = x21_1 - x22_1
0001e534
0001e538 if ((x8.d & 1) != 0)
0001e548 *(arg1 + 8) = x9_3
0001e538 else
0001e540 *arg1 = (x9_3.d << 1).b
0001e540
0001e54c x20_1[x9_3] = 0
0001e4d0 else if ((x8.d & 1) != 0)
0001e500 (*(arg1 + 0x10))[arg2] = 0
0001e504 *(arg1 + 8) = arg2
0001e4d4 else
0001e4e0 arg1[arg2 + 1] = 0
0001e4e4 *arg1 = (arg2.d << 1).b
0001e4e4
0001e564 return arg1

从位置 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
85
function 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
153
function 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[*] Call sub_4B8C8 
[+] arg1 (ID): 1 (Hex: 0x00000001)
[+] arg3 (Serial): -319115519
[+] arg4 (Type): 4
[+] arg2 (String 1):
- Len: 7
- Content: 9193803
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
6ef29976c1 39 31 39 33 38 30 33 9193803
[+] arg5 (String 2):
- Len: 36
- Content: 8de3ec7b-baa0-375f-bd3f-006dd6b65325
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
71ae32d090 38 64 65 33 65 63 37 62 2d 62 61 61 30 2d 33 37 8de3ec7b-baa0-37
71ae32d0a0 35 66 2d 62 64 33 66 2d 30 30 36 64 64 36 62 36 5f-bd3f-006dd6b6
71ae32d0b0 35 33 32 35 5325
[+] arg6 (Raw Data):
- Len (arg7): 16
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
6ef29976f1 f1 7a d0 39 5d af 75 77 b9 fe 9d 1a 64 6e 63 5c .z.9].uw....dnc\
========================================

[!] sub_4B8C8 Return:
[*] Result Content: AAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438PdeJeRK5KkdGzy+VgHp2qrrFfz8Mt2Zt+g/YxRgxOEWPcML2m2i5jhOFkq6zm/xTkni2FplIyTZTn

RC4

中间部分很像 RC4

初始化 256 字节状态表

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

这个 key 通过 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
function verify_rc4_key() {
const targetSo = "libxyass.so";
const mod = Process.findModuleByName(targetSo);
if (!mod) {
console.log("[-] Module not found");
return;
}

const modBase = mod.base;

const keyOffset = 0x1442E; // 0xFFFFFFFFA0B51A0E = -0x5F4AE5F2,-0x5F4AE5F2 + 0x5F4C2A20 = 0x1442E
const targetAddr = modBase.add(keyOffset);

console.log(`\n[+] Module Base: ${modBase}`);
console.log(`[+] Target Key Address: ${targetAddr}`);

try {
Memory.protect(targetAddr, 64, 'rwx');

const keyContent = targetAddr.readUtf8String();
const keyHex = targetAddr.readByteArray(13); // 密钥长度为 13

console.log("[*] Confirmed Key String: " + keyContent);
console.log("[*] Confirmed Key Hex: ");
console.log(hexdump(keyHex as ArrayBuffer, { ansi: true }));

} catch (e) {
console.log("[-] Error reading key memory: " + e);
try {
console.log(hexdump(targetAddr.sub(16), { length: 64, ansi: true }));
} catch(e2) {}
}
}

hook 结果

1
2
3
4
5
6
[+] Module Base: 0x778ead6000
[+] Target Key Address: 0x778eaea42e
[*] Confirmed Key String: std::abort();
[*] Confirmed Key Hex:
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 73 74 64 3a 3a 61 62 6f 72 74 28 29 3b std::abort();

后面的循环则是 PRGA + XOR 输出,在循环里面更新索引,从状态表取值,swap,再取一个字节与明文异或

hook append 函数(sub_1e3c8)

这个 hook 确实是挺重要的一个 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
function hook_1E3C8(){
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(0x1E3C8);
Interceptor.attach(funcAddr, {
onEnter(args) {
// console.log("\n[+] === sub_1E3C8 ===");
this.strPtr = args[0];
this.src = args[1];
this.len = args[2]?.toInt32();

let before = getStdStringInfo(this.strPtr);
console.log("[*] SIZE before append: " + before.size);
if(before.size>0){
console.log("[*] CONTENT before append:\n" + hexdump(before.data,{
length:before.size,
}))
}
console.log("[*] appending length: " + this.len);
if (this.len > 0 && !this.src.isNull()) {
console.log("[*] Data to Append:\n" + hexdump(this.src, {
length: this.len }));
}
},
onLeave(retval) {
let after = getStdStringInfo(this.strPtr);
console.log("[*] SIZE after append: "+ after.size);
if(after.size>0){
console.log("[*] CONTENT after append:\n" + hexdump(after.data,{
length:after.size,
}
))
}

}

});
}

}
}
})
}

}

这是其中一轮的日志

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
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
int sub_47338(ctx_obj, arg2, arg3, ..., out_buf) {
save_stack_cookie();

session = sub_3f738(ctx_obj, arg2, data_9b610, ...);
workbuf = ctx_obj->alloc_block(0x1000);
state = sub_84228();

ok = sub_843b4_init(state, arg3[0], &arg3[1], 0x40);
zero(var_7c, 0x14);

if (!ok) {
cleanup_state(state);
format_default_output(out_buf, *arg3, var_78);
out_buf[0x11] = 0;
return;
}

mode, tag = sub_3f7d4(ctx_obj, session, data_9b618, ...);

while (mode != -1) {
block = ctx_obj->get_next_block(workbuf, 0);
hexbuf = alloc(0x2010);
bytes_to_hex_string(block, 0x1000, hexbuf, tag);

sub_850d8(state, block, mode); // 原始块处理
result = ctx_obj->process_block(workbuf, block, 0); // 虚表 +0x600

if (tag & 1)
result = sub_1e0f4(hexbuf); // hex 文本处理

mode, tag = sub_3f7d4(ctx_obj, session, data_9b618, result...);
}

ctx_obj->release_block(workbuf);
sub_85200(state, &var_78, &var_7c); // 导出最终 16 字节结果
cleanup_state(state);

out_buf[0] = 0x20;
memcpy(out_buf + 1, &var_78, 16);
out_buf[0x11] = 0;
}

sub_850d8

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

同时这个函数也是一个小型的控制流平坦化,分发器通过 x9_2 的值来决定下一步执行的基本块

sub_868a8

0x868d8 跳转过去实际是在 sub_868a8 这个函数里面

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
000868a8    int64_t sub_868a8(void* arg1, int64_t arg2, size_t arg3)

000868bc if (arg3 != 0)
000868c0 void* x21_1 = *(arg1 + 0x10)
000868c4 size_t x19_1 = arg3
000868cc int64_t x20_1 = arg2
000868d0 int32_t x9_1 = *(x21_1 + 0x10)
000868d0 int32_t x10_1 = *(x21_1 + 0x14)
000868d4 uint64_t x8_1 = zx.q(*(x21_1 + 0x58))
000868dc int32_t x10_2
000868dc
000868dc x10_2 = x9_1 + (x19_1.d << 3) u< x9_1 ? x10_1 + 1 : x10_1
000868dc
000868e4 *(x21_1 + 0x10) = x9_1 + (x19_1.d << 3) // 字节数转 bit 数
000868e4 *(x21_1 + 0x14) = x10_2 + (arg3 u>> 0x1d).d // 更新总输入长度
000868e4 // 经典摘要算法里维护 message length 的方式

000868e8 if (x8_1.d == 0) // buf_used == 0,说明当前缓冲区是空的,直接去后面按整块处理
000868e8 goto label_8695c
000868e8
// 如果缓冲区里已有数据,先尝试把它补满到 64 字节
000868f0 void* x22_1 = x21_1 + 0x18 // x22_1 = buffer 起始地址
000868fc void* x0 = x22_1 + x8_1 // x0 = buffer 当前写入位置
000868fc
00086900 if (((x8_1 + x19_1) | x19_1) u>= 0x40)
00086930 memcpy(x0, x20_1, 0x40 - x8_1)
00086940 int64_t x19_2
00086940 int64_t x20_2
00086940 int64_t x23_2
00086940 x19_2, x20_2, x21_1, x23_2 = sub_86e44(x21_1, x22_1, 1)
00086944 x20_1 = x20_2 + x23_2
00086948 x19_1 = x19_2 - x23_2
00086958 __builtin_memset(dest: x22_1, ch: 0, count: 0x44)
0008695c label_8695c:
0008695c uint64_t x2_2 = x19_1 u>> 6 // blocks = len / 64
0008695c
00086960 if (x2_2 != 0)
0008696c int64_t x19_3
0008696c int64_t x20_3
0008696c x19_3, x20_3, x21_1 = sub_86e44(x21_1, x20_1, x2_2)
00086974 x20_1 = x20_3 + (x19_3 & 0xffffffffffffffc0)
00086978 x19_1 = x19_3 & 0x3f // remain = len % 64
00086978
0008697c if (x19_1 != 0)
0008698c *(x21_1 + 0x58) = x19_1.d
00086990 memcpy(x21_1 + 0x18, x20_1, x19_1)
00086900 else
0008690c memcpy(x0, x20_1, x19_1)
00086918 *(x21_1 + 0x58) += x19_1.d
00086918
000869a8 return 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

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 后,再用脚本批量赋值的方式给所有这种结构的跳转设置跳转地址

识别跳转结构

从上面的分析不难看出跟最后跳转地址相关大概就三行指令

要算出最后 x24_6 的值,需要知道 x24_5 的值,并收集后面的常量值,x24_5 依赖于 x12 寄存器和


xhs shield 参数分析
http://example.com/2026/03/31/xhs_shield/
作者
Eleven
发布于
2026年3月31日
许可协议