某瓣字段_sig的分析

豆瓣

抓包

先了解如何抓包,这次尝试分析某个参数的加解密过程。

工具是:Charles 4.6.3。

抓包的配置方式如下:

1.将pc和op放在同一局域网下,pc上打开Charles,设置Proxy->Proxy Settings,填写端口并勾选,如下图所示。

image-20250310103644735

2.之后在op上,设置网络的代理,将手机的流量转发到手机的9999端口。

image-20250310103929125

3.为pc和手机装Charles的root证书,用于CA自签名。

image-20250310104014250

由于手机端的证书对文件名有要求,这个文件名需要通过计算获得,计算可以通过openssl x509 -subject_hash_old -in <证书>获得,下图是网图,它最后得到的文件名就是5cba21f7,然后后缀固定为.0,所以就是5cba21f7.0

image-20250310104324383

4.在安卓7以上的版本,app只接受系统证书,不接受用户证书,所以需要将证书移至/system/etc/security/cacerts,之后重启手机。

5.之后如图所示,设置ssl代理。

image-20250310104618901

之后就可以抓到包了。——上面只考虑了单向认证,如果是双向认证,有些app的包还是会显示unknown,那就要将app的证书和密钥填写至Charles中了,不过豆瓣是单向认证。

分析

根据访问不同的域名,进行区别。

image-20250310150823254

其中,参数_sig是请求签名,用于验证请求的完整性和合法性,通常由参数和密钥计算得出。

image-20250310150909829

这次要分析的就是如何得到这个_sig。——抓几次包,会发现每次发包的header中,就_sig的值一直在变化,所以如果想要写脚本来发包,伪造客户端,就要解决_sig构造的问题。

通过搜索"_sig",似乎找到了为_sig赋值的地方,将pair0.first经过一些规则(HTTP的规范)后,作为值赋给_sig。

image-20250310151822183

在setQueryParameter中会检查s是否为空,如果已存在,还会把它移除,重新进行签名。

image-20250310155727702

之后,我想hook上面的函数,结果遇到了frida检测。

这次frida检测,我学到了以下几点:

1.frida用旧一些的版本,最新的可能不稳定;

2.把握好hook的时机,Interceptor.attach和Interceptor.replace的执行方式不同。attach是设置监控,监控目标地址,当执行到目标地址,才会启动对应的脚本内容;而replace是对目标地址立马进行替换,如果目标地址所在的模块尚未加载,会执行失败。

以下是grok的回答。

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
什么是“Hook 陷阱”?

“Hook 陷阱”并不是一个官方术语,而是形象地描述了 Frida 在特定时机设置 Hook 的工作方式。简单来说,它就像在目标函数的调用路径上“埋下一个陷阱”,等待程序运行到那个位置时触发,而不是立即执行 Hook 逻辑。

在 Frida 中,Hook 的实现主要通过 Interceptor 模块完成:

Interceptor.attach:附加到目标函数的入口和出口,分别在函数调用前(onEnter)和调用后(onLeave)执行自定义逻辑。
Interceptor.replace:直接替换目标函数的实现,用你定义的函数逻辑覆盖原始函数。

“陷阱”的关键在于时机:

Hook 的设置(比如调用 Interceptor.attach 或 Interceptor.replace)是在你的 Frida JS 脚本运行时完成的。
Hook 的触发(比如执行 onEnter 或替换后的函数逻辑)则是在目标函数被实际调用时发生的。

举个例子:
javascript
Interceptor.attach(Module.getExportByName('libc.so', 'open'), {
onEnter: function(args) {
console.log('open 被调用了!');
}
});

脚本运行时,Interceptor.attach 会“设置陷阱”,告诉 Frida 监控 open 函数。
但 onEnter 里的 console.log 并不会立即执行,而是等到程序调用 open 时才打印日志。

这种“等待触发”的机制就是“Hook 陷阱”的核心。
替换函数的时机:脚本运行后会第一时间替换吗?

你提到:“将脚本跑起来后,它难道不是第一时间将 Native 层函数替换掉吗?”答案是不完全是。让我们详细分析一下。
Frida 替换函数的工作原理

使用 Interceptor.replace 可以替换 Native 层函数的实现。例如:
javascript
Interceptor.replace(ptr('0x12345678'), new NativeCallback(function() {
console.log('函数被替换了!');
return 0;
}, 'int', []));

当脚本执行到 Interceptor.replace 时,Frida 会尝试将目标函数的地址替换为你的自定义函数。
但这有一个前提:目标函数所在的模块必须已经加载到内存中。

时机的影响因素

模块是否已加载
如果你在脚本运行时要替换的函数属于某个动态加载的 .so 文件(比如 libexample.so),而这个文件在脚本启动时尚未加载,Frida 无法立即替换。
因为 Frida 需要知道模块的基址和函数的偏移量。如果 Process.findModuleByName('libexample.so') 返回 null,你就无法计算目标函数的地址,替换自然无法生效。
动态加载的场景
在 Android 中,很多 .so 文件是通过 dlopen(或 android_dlopen_ext)动态加载的。
如果你的目标函数在某个动态加载的模块中,你需要在模块加载后再执行替换操作。

实际例子:你的代码可能的样子

假设你想替换 libmsaoaidsec.so 中的某个函数 sub_1b924,代码可能是这样的:
javascript
function hook_sub_1b924() {
let module = Process.findModuleByName('libmsaoaidsec.so');
if (module) {
let sub_1b924 = module.base.add(0x1b924);
Interceptor.replace(sub_1b924, new NativeCallback(function() {
console.log('sub_1b924 被替换了!');
return 0;
}, 'int', []));
} else {
console.log('模块尚未加载!');
}
}

如果脚本运行时 libmsaoaidsec.so 还没加载,module 会是 null,替换不会生效。
你需要等待模块加载完成后再调用 hook_sub_1b924()。

为什么不直接在脚本运行时替换?

你可能会疑惑:为什么不直接在脚本启动时就完成所有替换?原因有以下几点:

模块未加载的问题
如上所述,如果目标模块没加载,Frida 无法找到函数地址,替换会失败。
解决办法是监控模块加载过程(比如 Hook android_dlopen_ext),在模块加载后再执行替换。
初始化逻辑的依赖
有些 .so 文件在加载后会执行初始化逻辑(比如通过 soinfo::call_constructors() 调用 .init 或 .init_array)。
如果你在初始化前替换函数,可能会干扰模块的正常初始化(比如反调试、校验逻辑),导致程序崩溃或行为异常。
通过在初始化开始前(比如 call_constructors 的 onEnter)替换函数,你可以更安全地介入。

例如,一个常见的策略是:
javascript
Interceptor.attach(Module.getExportByName('libdl.so', 'android_dlopen_ext'), {
onEnter: function(args) {
this.onLeave = function(retval) {
hook_sub_1b924(); // 在模块加载后替换函数
};
}
});

这里在 android_dlopen_ext 调用后(模块加载完成时)再执行替换,确保时机正确。

总结:如何理解“Hook 陷阱”和替换时机

“Hook 陷阱”的含义
它是指在脚本运行时设置 Hook,但实际逻辑在目标函数调用时触发,就像“埋下陷阱”等待猎物。
比如通过 Interceptor.attach 设置的 onEnter 和 onLeave,都是在目标函数执行时才生效。
替换函数的时机
Interceptor.replace 在脚本运行时设置替换,但实际生效取决于模块是否加载。
如果目标函数所在的 .so 文件是动态加载的,你需要在模块加载后(比如通过 Hook android_dlopen_ext 或 soinfo::call_constructors())再执行替换。
你的疑问解答
脚本运行后不会“第一时间”替换 Native 层函数,除非目标模块已经加载且函数地址可访问。
对于动态加载的模块,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
function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
if (this.fileName !== undefined && this.fileName.indexOf("libmsaoaidsec.so") >= 0) {
hook_linker_call_constructors()
}
}, onLeave: function (retval) {
console.log(`dlopen onLeave fileName: ${this.fileName}`)
if (this.fileName != null && this.fileName.indexOf("libmsaoaidsec.so") >= 0) {
let JNI_OnLoad = Module.getExportByName(this.fileName, 'JNI_OnLoad')
Interceptor.attach(JNI_OnLoad, {
onLeave: function (retval) {
console.log(`JNI_OnLoad onLeave: ${retval}`)
}
});
console.log(`dlopen onLeave JNI_OnLoad: ${JNI_OnLoad - Module.getBaseAddress(this.fileName)}`)
}
}
}
);
}

function hook_linker_call_constructors() {
let linker64_base_addr = Module.getBaseAddress('linker64')
let offset = 0x51BA8 // __dl__ZN6soinfo17call_constructorsEv
let call_constructors = linker64_base_addr.add(offset)
let listener = Interceptor.attach(call_constructors,{
onEnter:function(args){
console.log('hook_linker_call_constructors onEnter')
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
if (secmodule != null){
// hook_pthred_create()
hook_sub_1c544() // 含检测
hook_sub_1b8d4()
// hook_sub_26e5c()
listener.detach()
}
}
})
}
function hook_pthred_create(){
console.log("libmsaoaidsec.so --- " + Process.findModuleByName("libmsaoaidsec.so").base)
Interceptor.attach(Module.findExportByName('libc.so','pthread_create'),{
onEnter(args){
let func_addr = args[2]
console.log(`The thread Called function address is: ${func_addr}`)
}
})
}

function hook_sub_1c544() {
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
Interceptor.replace(secmodule.base.add(0x1c544), new NativeCallback(function () {
console.log(`hook_sub_1c544 >>>>>>>>>>>>>>>>> replace`)
}, 'void', ["int64"]));
}

function hook_sub_1b8d4() {
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
Interceptor.replace(secmodule.base.add(0x1b8d4), new NativeCallback(function () {
console.log(`hook_sub_1b8d4 >>>>>>>>>>>>>>>>> replace`)
}, 'void', []));
}

function hook_sub_26e5c() {
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
Interceptor.replace(secmodule.base.add(0x26e5c), new NativeCallback(function () {
console.log(`hook_sub_26e5c >>>>>>>>>>>>>>>>> replace`)
return 1;
}, 'int64', []));
}

setImmediate(hook_dlopen)

逻辑算比较清楚,之后还剩下对业务函数的hook、利用_sig构造包。

接着分析方法e0.d.A(Request request0)。

image-20250312125816408

然后写了这个函数,每次我将这个函数放到hook_dlopen的末尾,就无法正确hook上之前的3个线程。之后我发现,可以通过cmd行输入。

1
2
3
4
5
6
7
8
9
function hook_sig(){
Java.perform(function(){
const clazz_d = Java.use("e0.d");
clazz_d.A.implementation = function (request) {
console.log(`request-header: ${request.header("Authorization")}`);
return this.A(request);
}
})
}

结果如下。

image-20250312125943864

这里就获得了Authorization的值:93f4c257daea24dfbbde470790be6e9d。

在方法d.z中,将url、method、authorization进行加密

image-20250312130152471

这里就不进去追踪了,直接获取返回值。

直接获取了_sig和_ts的值。

1
2
3
4
5
6
7
8
9
10
11
function hook_sig(){
Java.perform(function(){
const clazz_d = Java.use("e0.d");
clazz_d.A.implementation = function (request) {
console.log(`request-header: ${request.header("Authorization")}`);
let res = this.A(request);
console.log(`Pair-first: ${new String(res.first)}\nPair-second: ${new String(res.second)}`);
return res;
}
})
}

image-20250312131159267

之后要利用并构造包的话,只需要添加上自己生成的_sig参数,这里用js脚本,模仿jeb的代码构造_sig,最终,完整的代码如下。

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
var url;
var method;
var authorization;

function hook_dlopen() {
let listener = Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
if (this.fileName !== undefined && this.fileName.indexOf("libmsaoaidsec.so") >= 0) {
hook_linker_call_constructors()
listener.detach()
}
}
}
);
// hook_sig()

}

function hook_linker_call_constructors() {
let linker64_base_addr = Module.getBaseAddress('linker64')
let offset = 0x51BA8 // __dl__ZN6soinfo17call_constructorsEv
let call_constructors = linker64_base_addr.add(offset)
let listener = Interceptor.attach(call_constructors,{
onEnter:function(args){
console.log('hook_linker_call_constructors onEnter')
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
if (secmodule != null){
// hook_pthred_create()
hook_sub_1c544()
hook_sub_1b8d4()
hook_sub_26e5c()
listener.detach()
}
}
})
}
function hook_pthred_create(){
console.log("libmsaoaidsec.so --- " + Process.findModuleByName("libmsaoaidsec.so").base)
Interceptor.attach(Module.findExportByName('libc.so','pthread_create'),{
onEnter(args){
let func_addr = args[2]
console.log(`The thread Called function address is: ${func_addr}`)
}
})
}

function hook_sub_1c544() {
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
Interceptor.replace(secmodule.base.add(0x1c544), new NativeCallback(function () {
console.log(`hook_sub_1c544 >>>>>>>>>>>>>>>>> replace`)
}, 'void', ["int64"]));
}

function hook_sub_1b8d4() {
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
Interceptor.replace(secmodule.base.add(0x1b8d4), new NativeCallback(function () {
console.log(`hook_sub_1b8d4 >>>>>>>>>>>>>>>>> replace`)
}, 'void', []));
}

function hook_sub_26e5c() {
let secmodule = Process.findModuleByName("libmsaoaidsec.so")
Interceptor.replace(secmodule.base.add(0x26e5c), new NativeCallback(function () {
console.log(`hook_sub_26e5c >>>>>>>>>>>>>>>>> replace`)
return 1;
}, 'int64', []));
}

function hook_sig(){
Java.perform(function(){
const clazz_d = Java.use("e0.d");
clazz_d.A.implementation = function (request) {
authorization = request.header("Authorization");
authorization = authorization.substring(7);
url = request.url().toString();
method = request.method();
console.log(`request-header: ${authorization}`); // 打印Authorization
let res = this.A(request);
console.log(`Pair-first: ${new String(res.first.value)}\nPair-second: ${new String(res.second.value)}`); // 打印Pair
// MySig,自己模仿流程写一个
getMySig();
return res;
}
})
}

function getMySig() {
const HttpUrl = Java.use("okhttp3.HttpUrl");
const StringBuilder = Java.use("java.lang.StringBuilder");
const clazz_a = Java.use("android.support.v4.media.a");
const Uri = Java.use("android.net.Uri");

// 临时硬编码 s3
var s3 = Java.use("java.lang.String").$new("bf7dddc7c9cfe6f7");

if (!url || !method || !authorization) {
console.error("url, method, or authorization is undefined");
return;
}

var stringBuilder0 = clazz_a.g(method);
if (!stringBuilder0) {
console.error("clazz_a.g(method) returned null");
return;
}

var s4 = HttpUrl.parse(url).encodedPath();
if (!s4) {
console.error("HttpUrl.parse failed");
return;
}

var s5 = Uri.decode(s4);
if (s5.endsWith("/")) {
s5 = clazz_a.e(s5, -1, 0);
}

stringBuilder0.append("&").append(Uri.encode(s5)).append("&").append(authorization);
var v = Java.use("java.lang.System").currentTimeMillis().toString();
v = v.substring(0, v.length - 3);
stringBuilder0.append("&").append(v);

var mac0 = Java.use("javax.crypto.Mac").getInstance("HmacSHA1");
mac0.init(Java.use("javax.crypto.spec.SecretKeySpec").$new(s3.getBytes(), "HmacSHA1"));

const Base64 = Java.use("android.util.Base64");

stringBuilder0.toString()

let strrr = stringBuilder0.toString();

console.log(`typeof strrr: ${typeof strrr}`);

let bytes = Java.use("java.lang.String").$new(strrr).getBytes();

console.log(`typeof Java.use("java.lang.String").$new(strrr): ${typeof Java.use("java.lang.String").$new(strrr)}`);

var res = mac0.doFinal(bytes);

var s6 = Base64.encodeToString(res, 2);

// 确保 s6 和 v 是正确的 Java 字符串
var pair = Java.use("android.util.Pair").$new(
s6, v);
console.log(`MySig: ${pair.first}`);
return pair;
}

setImmediate(hook_dlopen)

其中,有一个问题,如下代码卡了我很久。在我眼中,这里的strrr一定是一个js代理java的String类型,是一个属于java的字符串,但被frida自动转换成了JavaScript字符串,而JS字符串没有getBytes函数。

1
2
3
4
5
6
stringBuilder0.toString()

let strrr = stringBuilder0.toString();

let bytes = Java.use("java.lang.String").$new(strrr).getBytes();
// 原本写的是strrr.getBytes()

https://reverseengineering.stackexchange.com/questions/32790/convert-string-to-byte-array-in-frida-js-script

通过js的函数,typeof去查看区别,可以发现stringBuilder0.toString()的类型是string,而Java.use(“java.lang.String”).$new(strrr)的类型是object。

image-20250313175050250

至此,脚本也算是跑出了_sig签名。