白乐天

道阻且长,行则将至。

B站播放量及完播率接口逆向

APP信息

包名:tv.danmaku.bili

1

抓包分析数据包

抓包工具开启之后,打开bilibili,随便点一个视频,分析数据包,找到相关数据包

1

分析请求体

请求体是一堆二进制信息

1

反编译apk

reportClick

搜索/x/report/click/android2,找到一个接口,接口里定义了reportClick方法

1

查找用例,在如下方法中有对reportClick方法的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final void a() {
long j2;
long i = c2.f.f.c.j.a.i() / 1000;
c2.f.b0.c.b.b.a.a E = c2.f.b0.c.b.b.a.a.E();
x.h(E, "EnvironmentPrefHelper.getInstance()");
long A = E.A();
if (A == -1) {
c2.f.b0.c.b.b.a.a E2 = c2.f.b0.c.b.b.a.a.E();
x.h(E2, "EnvironmentPrefHelper.getInstance()");
E2.V(i);
j2 = i;
} else {
j2 = A;
}
byte[] H7 = d.this.H7(this.b.a(), this.b.b(), this.b.h(), i, j2, this.b.n(), this.b.m(), this.b.k(), this.b.c(), this.b.e(), this.b.l(), this.b.f());
tv.danmaku.biliplayerimpl.report.heartbeat.a aVar = (tv.danmaku.biliplayerimpl.report.heartbeat.a) com.bilibili.okretro.c.a(tv.danmaku.biliplayerimpl.report.heartbeat.a.class);
c0 create = c0.create(w.d(com.hpplay.sdk.source.protocol.h.E), H7);
x.h(create, "RequestBody.create(Media…ion/octet-stream\"), body)");
l<String> execute = aVar.reportClick(create).execute();
BLog.i("HeartBeatTracker", "player report click(vv): responseCode:" + execute.b() + ", responseMsg:" + execute.h() + ", responseBody:" + execute.a());
}

分析reportClick方法的参数create的生成过程,如下

1
2
3
4
create参数通过c0.create(w.d(com.hpplay.sdk.source.protocol.h.E), H7)方法得到
com.hpplay.sdk.source.protocol.h.E的值是"application/octet-stream",还有一个H7参数
H7参数通过d.this.H7()方法得到
byte[] H7 = d.this.H7(this.b.a(), this.b.b(), this.b.h(), i, j2, this.b.n(), this.b.m(), this.b.k(), this.b.c(), this.b.e(), this.b.l(), this.b.f());

d.this.H7()

其源码如下

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
public final byte[] H7(long j2, long j4, int i, long j5, long j6, int i2, int i3, long j7, String str, int i4, String str2, String str3) throws Exception {
long j8;
int i5;
Application f2 = BiliContext.f();
com.bilibili.lib.accounts.b client = com.bilibili.lib.accounts.b.f(f2);
AccountInfo h = BiliAccountInfo.f.a().h();
if (h != null) {
j8 = h.getMid();
i5 = h.getLevel();
} else {
j8 = 0;
i5 = 0;
}
TreeMap treeMap = new TreeMap();
treeMap.put("aid", String.valueOf(j2));
treeMap.put("cid", String.valueOf(j4));
treeMap.put("part", String.valueOf(i));
treeMap.put(EditCustomizeSticker.TAG_MID, String.valueOf(j8));
treeMap.put("lv", String.valueOf(i5));
treeMap.put("ftime", String.valueOf(j6));
treeMap.put("stime", String.valueOf(j5));
treeMap.put("did", com.bilibili.lib.biliid.utils.f.a.c(f2));
treeMap.put("type", String.valueOf(i2));
treeMap.put("sub_type", String.valueOf(i3));
treeMap.put("sid", String.valueOf(j7));
treeMap.put("epid", str);
treeMap.put("auto_play", String.valueOf(i4));
x.h(client, "client");
if (client.r()) {
treeMap.put("access_key", client.g());
}
treeMap.put("build", String.valueOf(com.bilibili.api.a.f()));
treeMap.put("mobi_app", com.bilibili.api.a.l());
treeMap.put("spmid", str2);
treeMap.put("from_spmid", str3);
StringBuilder sb = new StringBuilder();
for (Map.Entry entry : treeMap.entrySet()) {
String str4 = (String) entry.getKey();
String str5 = (String) entry.getValue();
sb.append(str4);
sb.append('=');
if (str5 == null) {
str5 = "";
}
sb.append(str5);
sb.append('&');
}
sb.deleteCharAt(sb.length() - 1);
String sb2 = sb.toString();
x.h(sb2, "builder.toString()");
String b2 = t3.a.i.a.a.a.b.e.b(sb2);
BLog.i("HeartBeatTracker", "player report click(vv), params: " + sb2 + " & sign=" + b2);
sb.append("&sign=");
sb.append(b2);
String sb3 = sb.toString();
x.h(sb3, "builder.toString()");
return t3.a.i.a.a.a.b.e.a(sb3);
}

分析

H7方法里的参数都放进了treeMap里,然后通过&拼接成字符串,调用了t3.a.i.a.a.a.b.e.b()方法进行签名,签名结果通过&符号拼接到字符串末尾,最后再调用一个t3.a.i.a.a.a.b.e.a()方法

hook H7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
function toHex(data) {
return ByteString.of(data).hex();
}

function hook_h7(){
Java.perform(function (){
let d = Java.use("tv.danmaku.biliplayerimpl.report.heartbeat.d");
d["H7"].implementation = function (j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3) {
console.log(`d.H7 is called: j2=${j2}, j4=${j4}, i=${i}, j5=${j5}, j6=${j6}, i2=${i2}, i3=${i3}, j7=${j7}, str=${str}, i4=${i4}, str2=${str2}, str3=${str3}`);
let result = this["H7"](j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3);
// console.log(`d.H7 result=${Java.use("java.lang.String").$new(result)}`);
console.log("to hex result:",toHex(result));
return result;
};
})
}

hook 结果

1
2
d.H7 is called: j2=113886342676927, j4=28065268176, i=1, j5=1737979363, j6=1737960450, i2=3, i3=0, j7=0, str=, i4=0, str2=main.ugc-video-detail.0.0, str3=tm.recommend.0.0
to hex result: b2eadf0bc65f99660f2ffbe874d802918231b6f0445bbf1c005253f1b42c251a61835063707effd59fc5800b4377d4241b252ec063dd6ca55f66a738ce7d5de3d5ef17d04714e17e0d3b592e18109e4e4d8d534666159c204dd8c31ec68c92355f57ce3798454155d57b360d9855dc4d8b131dcb9befebe638635b103d40b0f66d33c283c6dd28b49d38e724f7f7abcb7ba16a7a150941bd301275e326c56e89e97210c18b36437fa4d6fde0f7911cbb7b2e80e56051a9f9f7e238ca20da5dbd6481362a85afceb73e55816d6ff8d7f740a5d8c8e3a0c6779ba406f400cb34936c6e1d3534bf0ed2eee6ce743fcf0ce8fd8ac478980f3dc6f1aac8de96e2be031e1e5f7a32bf06ec73840f4632728a576c9d2f4fa6fc76141e961920885e1d57b3a5e4f77a88d285f6eea2398d97a949e9577bba57665c84a5f85a102e07921e

主动调用H7

找到tv.danmaku.biliplayerimpl.report.heartbeat.d类的一个实例,然后主动调用H7方法,固定输出结果

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
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
function toHex(data) {
return ByteString.of(data).hex();
}

function hook_h7(){
Java.perform(function (){
let d = Java.use("tv.danmaku.biliplayerimpl.report.heartbeat.d");
d["H7"].implementation = function (j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3) {
console.log(`d.H7 is called: j2=${j2}, j4=${j4}, i=${i}, j5=${j5}, j6=${j6}, i2=${i2}, i3=${i3}, j7=${j7}, str=${str}, i4=${i4}, str2=${str2}, str3=${str3}`);
let result = this["H7"](j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3);
// console.log(`d.H7 result=${Java.use("java.lang.String").$new(result)}`);
console.log("to hex result:",toHex(result));
return result;
};
})
}

function call_h7(){
Java.perform(function (){
Java.choose("tv.danmaku.biliplayerimpl.report.heartbeat.d",{
onMatch:function(obj){
// console.log(obj.toString());
if(obj.toString()=="tv.danmaku.biliplayerimpl.report.heartbeat.d@8ca9273"){
obj.H7(113886342676927,28065268176,1,1737979363,1737960450,3,0,0,"",0,"main.ugc-video-detail.0.0","tm.recommend.0.0");
}

},
onComplete:function(){

}
})
})
}

t3.a.i.a.a.a.b.e.b

1

hook b

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
function call_h7(){
Java.perform(function (){
Java.choose("tv.danmaku.biliplayerimpl.report.heartbeat.d",{
onMatch:function(obj){
// console.log(obj.toString());
if(obj.toString()=="tv.danmaku.biliplayerimpl.report.heartbeat.d@8ca9273"){
obj.H7(113886342676927,28065268176,1,1737979363,1737960450,3,0,0,"",0,"main.ugc-video-detail.0.0","tm.recommend.0.0");
console.log("H7 is called!")
}
},
onComplete:function(){

}
})
})
}

function hook_b(){
Java.perform(function(){
let b = Java.use("t3.a.i.a.a.a.b");
b["b"].implementation = function (params) {
console.log(`b.b is called: params=${params}`);
let result = this["b"](params);
console.log(`b.b result=${result}`);
return result;
};
})
}

hook 结果

1
2
3
b.b is called: params=aid=113886342676927&auto_play=0&build=6240300&cid=28065268176&did=PF9tVWMHYgNiUTQGegZ6&epid=&from_spmid=tm.recommend.0.0&ftime=1737960450&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1737979363&sub_type=0&type=3
b.b result=2e012b190fa7060fba2941e72bd14646d0bb2d5445838e2812f95dd1807b6208
H7 is called!

查看t3.a.i.a.a.a.b.e.b的源码发现,结果来源于com.bilibili.commons.m.a.g函数

1

它的两个参数一个是params,另一个是d,可以通过hook得到它的值

其实在这个页面往上翻一下,可以找到对d的赋值

1

d是一个定值"9cafa6466a028bfb"

com.bilibili.commons.m.a.g

1

这里还使用了一个SHA256哈希算法

hook g

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
function call_h7(){
Java.perform(function (){
Java.choose("tv.danmaku.biliplayerimpl.report.heartbeat.d",{
onMatch:function(obj){
// console.log(obj.toString());
if(obj.toString()=="tv.danmaku.biliplayerimpl.report.heartbeat.d@a68ff35"){
obj.H7(113886342676927,28065268176,1,1737979363,1737960450,3,0,0,"",0,"main.ugc-video-detail.0.0","tm.recommend.0.0");
console.log("H7 is called!")
}
},
onComplete:function(){

}
})
})
}

function hook_g(){
Java.perform(function(){
let a = Java.use("com.bilibili.commons.m.a");
a["g"].implementation = function (bArr, bArr2) {
let string_class = Java.use("java.lang.String");
console.log(`a.g is called: bArr=${bArr}, bArr2=${bArr2}`);
console.log(`a.g is called: bArr=${string_class.$new(bArr)}, bArr2=${string_class.$new(bArr2)}`);
let result = this["g"](bArr, bArr2);
console.log(`a.g result=${result}`);
return result;
};
})
}

hook 结果

1
2
3
4
a.g is called: bArr=97,105,100,61,49,49,51,56,56,54,51,52,50,54,55,54,57,50,55,38,97,117,116,111,95,112,108,97,121,61,48,38,98,117,105,108,100,61,54,50,52,48,51,48,48,38,99,105,100,61,50,56,48,54,53,50,54,56,49,55,54,38,100,105,100,61,80,70,57,116,86,87,77,72,89,103,78,105,85,84,81,71,101,103,90,54,38,101,112,105,100,61,38,102,114,111,109,95,115,112,109,105,100,61,116,109,46,114,101,99,111,109,109,101,110,100,46,48,46,48,38,102,116,105,109,101,61,49,55,51,55,57,54,48,52,53,48,38,108,118,61,48,38,109,105,100,61,48,38,109,111,98,105,95,97,112,112,61,97,110,100,114,111,105,100,38,112,97,114,116,61,49,38,115,105,100,61,48,38,115,112,109,105,100,61,109,97,105,110,46,117,103,99,45,118,105,100,101,111,45,100,101,116,97,105,108,46,48,46,48,38,115,116,105,109,101,61,49,55,51,55,57,55,57,51,54,51,38,115,117,98,95,116,121,112,101,61,48,38,116,121,112,101,61,51, bArr2=57,99,97,102,97,54,52,54,54,97,48,50,56,98,102,98
a.g is called: bArr=aid=113886342676927&auto_play=0&build=6240300&cid=28065268176&did=PF9tVWMHYgNiUTQGegZ6&epid=&from_spmid=tm.recommend.0.0&ftime=1737960450&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1737979363&sub_type=0&type=3, bArr2=9cafa6466a028bfb
a.g result=2e012b190fa7060fba2941e72bd14646d0bb2d5445838e2812f95dd1807b6208
H7 is called!

签名之后传递给t3.a.i.a.a.a.b.e.a()方法

t3.a.i.a.a.a.b.e.a

1

这里使用了aes加密

hook a

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
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
function toHex(data) {
return ByteString.of(data).hex();
}

function call_h7(){
Java.perform(function (){
Java.choose("tv.danmaku.biliplayerimpl.report.heartbeat.d",{
onMatch:function(obj){
// console.log(obj.toString());
if(obj.toString()=="tv.danmaku.biliplayerimpl.report.heartbeat.d@a68ff35"){
obj.H7(113886342676927,28065268176,1,1737979363,1737960450,3,0,0,"",0,"main.ugc-video-detail.0.0","tm.recommend.0.0");
console.log("H7 is called!")
}
},
onComplete:function(){

}
})
})
}


function hook_a(){
Java.perform(function(){
let b = Java.use("t3.a.i.a.a.a.b");
b["a"].implementation = function (body) {
console.log(`b.a is called: body=${body}`);
let result = this["a"](body);
console.log(`b.a result=${toHex(result)}`);
return result;
};
})
}

hook 结果

1
2
3
b.a is called: body=aid=113886342676927&auto_play=0&build=6240300&cid=28065268176&did=PF9tVWMHYgNiUTQGegZ6&epid=&from_spmid=tm.recommend.0.0&ftime=1737960450&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1737979363&sub_type=0&type=3&sign=2e012b190fa7060fba2941e72bd14646d0bb2d5445838e2812f95dd1807b6208
b.a result=b2eadf0bc65f99660f2ffbe874d802918231b6f0445bbf1c005253f1b42c251a61835063707effd59fc5800b4377d4241b252ec063dd6ca55f66a738ce7d5de3d5ef17d04714e17e0d3b592e18109e4e4d8d534666159c204dd8c31ec68c92355f57ce3798454155d57b360d9855dc4d8b131dcb9befebe638635b103d40b0f66d33c283c6dd28b49d38e724f7f7abcb7ba16a7a150941bd301275e326c56e89e97210c18b36437fa4d6fde0f7911cbb7b2e80e56051a9f9f7e238ca20da5dbd6481362a85afceb73e55816d6ff8d7f740a5d8c8e3a0c6779ba406f400cb34936c6e1d3534bf0ed2eee6ce743fcf0ce8fd8ac478980f3dc6f1aac8de96e2be031e1e5f7a32bf06ec73840f4632728a576c9d2f4fa6fc76141e961920885e1d57b3a5e4f77a88d285f6eea2398d97a949e9577bba57665c84a5f85a102e07921e
H7 is called!

查看源码,可在源码中,找到AES加密的Key和IV向量

1
2
Key: "fd6b639dbcff0c2a1b03b389ec763c4b"
IV : "77b07a672d57d64c"

解密请求体

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
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

def aes_decrypt(ciphertext_hex, key, iv):
"""
使用AES解密数据(hex编码的密文)
:param ciphertext_hex: hex编码的密文(str)
:param key: 密钥(bytes, 长度为16, 24或32字节)
:param iv: 初始化向量IV(bytes, 长度为16字节)
:return: 解密后的明文(str)
"""
# 将hex编码的密文转换为bytes
ciphertext = bytes.fromhex(ciphertext_hex)
# 创建AES解密对象
cipher = AES.new(key, AES.MODE_CBC, iv)
# 解密并移除填充
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
return plaintext.decode('utf-8')

# 示例数据
key = b'fd6b639dbcff0c2a1b03b389ec763c4b' # 16字节密钥
iv = b'77b07a672d57d64c' # 16字节IV
ciphertext_hex = 'b2eadf0bc65f99660f2ffbe874d802918231b6f0445bbf1c005253f1b42c251a61835063707effd59fc5800b4377d4241b252ec063dd6ca55f66a738ce7d5de3d5ef17d04714e17e0d3b592e18109e4e4d8d534666159c204dd8c31ec68c92355f57ce3798454155d57b360d9855dc4d8b131dcb9befebe638635b103d40b0f66d33c283c6dd28b49d38e724f7f7abcb7ba16a7a150941bd301275e326c56e89e97210c18b36437fa4d6fde0f7911cbb7b2e80e56051a9f9f7e238ca20da5dbd6481362a85afceb73e55816d6ff8d7f740a5d8c8e3a0c6779ba406f400cb34936c6e1d3534bf0ed2eee6ce743fcf0ce8fd8ac478980f3dc6f1aac8de96e2be031e1e5f7a32bf06ec73840f4632728a576c9d2f4fa6fc76141e961920885e1d57b3a5e4f77a88d285f6eea2398d97a949e9577bba57665c84a5f85a102e07921e' # hex编码的密文

# 解密
try:
plaintext = aes_decrypt(ciphertext_hex, key, iv)
print("解密后的明文:", plaintext)
except Exception as e:
print("解密失败:", e)


# 输出
解密后的明文: aid=113886342676927&auto_play=0&build=6240300&cid=28065268176&did=PF9tVWMHYgNiUTQGegZ6&epid=&from_spmid=tm.recommend.0.0&ftime=1737960450&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1737979363&sub_type=0&type=3&sign=2e012b190fa7060fba2941e72bd14646d0bb2d5445838e2812f95dd1807b6208

Python实现请求体加密

请求体参数还原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"aid": "113886342676927",
"auto_play": 0,
"build": 6240300,
"cid": "28065268176",
"did": "PF9tVWMHYgNiUTQGegZ6",
"epid": "",
"from_spmid": "tm.recommend.0.0",
"ftime": 1737960450,
"lv": 0,
"mid": 0,
"mobi_app": "android",
"part": 1,
"sid": 0,
"spmid": "main.ugc-video-detail.0.0",
"stime": 1737979363,
"sub_type": 0,
"type": 3,
"sign": "2e012b190fa7060fba2941e72bd14646d0bb2d5445838e2812f95dd1807b6208"
}

这里面需要逆向的参数有aidbuildciddidsign

sign还原

sign值我们已经分析过了,所以先把它还原一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import hashlib

def SHA256_call(data,salt):
# 创建sha256对象
sha256 = hashlib.sha256()
sha256.update(data.encode("utf-8"))
if salt!="":
sha256.update(salt.encode("utf-8"))
# 获取加密后的数据,以16进制表示
sha256_hex = sha256.hexdigest()
return sha256_hex

param = "aid=113886342676927&auto_play=0&build=6240300&cid=28065268176&did=PF9tVWMHYgNiUTQGegZ6&epid=&from_spmid=tm.recommend.0.0&ftime=1737960450&lv=0&mid=0&mobi_app=android&part=1&sid=0&spmid=main.ugc-video-detail.0.0&stime=1737979363&sub_type=0&type=3"
salt = "9cafa6466a028bfb"
print(SHA256_call(param,salt))

>>>
2e012b190fa7060fba2941e72bd14646d0bb2d5445838e2812f95dd1807b6208

aid还原

分析源码,发现aid参数可以从Video类的内部类h类中的a()方法获取,hook一下

1
2
3
4
5
6
7
8
9
10
11
function hook_aid(){
Java.perform(function(){
let h = Java.use("tv.danmaku.biliplayerv2.service.Video$h");
h["a"].implementation = function () {
console.log(`h.a is called`);
let result = this["a"]();
console.log(`h.a result=${result}`);
return result;
};
})
}

hook结果,点击不同的视频它的结果是不一样的

1
2
h.a is called
h.a result=113886342676927

猜测aid可能与视频有关

build还原

在源码里面翻了翻,发现它是个定值6240300

1

cid还原

浏览源码,发现cid参数可以从Video类的内部类h类中的b()方法获取,hook一下

1
2
3
4
5
6
7
8
9
10
11
function hook_cid(){
Java.perform(function(){
let h = Java.use("tv.danmaku.biliplayerv2.service.Video$h");
h["b"].implementation = function () {
console.log(`h.b is called`);
let result = this["b"]();
console.log(`h.b result=${result}`);
return result;
};
})
}

hook结果,与aid参数类似的是,不同的视频,cid参数的值也是不一样的

1
2
h.b is called
h.b result=28065268176

did还原

did参数是通过com.bilibili.lib.biliid.utils.f.a.c()方法得到的

1

hook 一下c方法

1
2
3
4
5
6
7
8
9
10
11
function hook_did(){
Java.perform(function(){
let a = Java.use("com.bilibili.lib.biliid.utils.f.a");
a["c"].implementation = function (context) {
console.log(`a.c is called: context=${context}`);
let result = this["c"](context);
console.log(`a.c result=${result}`);
return result;
};
})
}

hook 结果,点击不同的视频,结果都是一样的

1
2
a.c is called: context=BiliApplication(tv.danmaku.bili)@bd968dc
a.c result=PF9tVWMHYgNiUTQGegZ6

分析其如何生成的,去看c方法的源码

c

1

1
2
3
首先判断f13193c字段是否为空,非空直接返回
如果是空的话,会调用c2.f.b0.c.a.e.k().f(context)方法来获取f字段
如果f字段还是空的话,会调用g(context)方法生成f13193c,

看g方法源码

g

1

通过f方法返回一个f字段,然后对字段f进行处理然后返回

看f方法源码

f

1

先看j2字段,它通过j(context)获取到

j

1

在这里面调用了一个c方法

c

1

它是获取mac地址的,这个东西我们也可以自己生成

接着去看f函数,有一个a2参数,它获取的是persist.service.bdroid.bdaddr,也就是蓝牙地址

f函数里找到h参数,h来源于h()方法

h

h里面调用了一个a()方法

1

它是/sys/bus/mmc/devices文件的内容,与设备总线信息有关

f函数里还有一个参数i,通过i()方法获得

i

i里面调用了a()方法

1

这里面提到了/sys/class/android_usb/android0/iSerial这个文件,它是Android 设备 USB 序列号的用户空间控制接口,与设备USB序列号有关。

总结

经过分析,在g()方法里生成的f字段与4个内容有关,分别是mac地址,蓝牙地址,设备总线和sn号,把他们通过|拼接起来,拼接起来的字符串传入b()方法。

b

1

在这里面对字符串进行处理然后通过Base64编码后返回

参数还原

接下来我们自己生成四个内容拼接起来然后通过python实现一个b方法再调用base64进行编码就可以得到did参数。

hook f

hook一下f方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import frida
import sys

process = frida.get_usb_device().attach("哔哩哔哩")

jsCode = '''
Java.perform(function(){
let a = Java.use("com.bilibili.lib.biliid.utils.f.a");
a["f"].implementation = function (context) {
console.log(`a.f is called: context=${context}`);
let result = this["f"](context);
console.log(`a.f result=${result}`);
return result;
};
})
'''

scrpt = process.create_script(jsCode)

scrpt.load()
sys.stdin.read()

hook 结果,如下只获取到了mac地址

1
2
a.f is called: context=BiliApplication(tv.danmaku.bili)@fa97dd3
a.f result=3c286deaa3e2|||

对于Mac地址参数,源码里还对它进行了处理,如下,把非0-9A-Fa-f的内容替换为空,并都转换为小写。

1
String lowerCase = j2.replaceAll("[^0-9A-Fa-f]", "").toLowerCase();

源码里对第二个参数蓝牙地址也是这样处理,把非0-9A-Fa-f的内容替换为空,并都转换为小写。

从源码里还可以看出的是,只要f()方法返回值的长度大于等于4,那么它就有效,所以这四个参数只要获取到了其中一个,就可以拿过来使用。

Python生成mac地址

1
2
3
4
5
6
7
8
9
10
11
12
import random

def g_mac():
mac = ""
for i in range(1,7):
part = random.sample("0123456789abcdef",2)
mac += "".join(part)
return mac
print(g_mac())

>>>
e5d76827d57f

Python实现b方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import random
import base64

def g_mac():
mac = ""
for i in range(1,7):
part = random.sample("0123456789abcdef",2)
mac += "".join(part)
return mac
# print(g_mac())

def b(str):
str+="|||"
byte_list = list(str.encode("utf-8"))
byte_list[0] = byte_list[0] ^ (len(byte_list) & 255)
for i in range(1,len(byte_list)):
byte_list[i] = (byte_list[i-1]^byte_list[i]) & 255
return base64.b64encode(bytes(byte_list))

print(b(g_mac()))
# print(b("e5d76827d57f|||")) # al87DDoCMAdjVmEHewd7

至此,did参数还原完成

Python实现请求体加密

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
import random
import base64
import hashlib
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import time

def g_mac():
"""
生成一个随机MAC地址(不是真实的MAC地址格式)
每个部分由2个十六进制字符组成,共6部分
"""
mac = ""
for i in range(1, 7):
part = random.sample("0123456789abcdef", 2) # 随机选择2个十六进制字符
mac += "".join(part) # 拼接成字符串
return mac


def g_did(string):
"""
生成设备ID(did),对字符串进行特殊的XOR变换,并进行Base64编码
"""
string += "|||" # 添加分隔符
byte_list = list(string.encode("utf-8")) # 转换为字节列表
byte_list[0] = byte_list[0] ^ (len(byte_list) & 255) # 首字节与长度取低8位的值异或
for i in range(1, len(byte_list)):
byte_list[i] = (byte_list[i - 1] ^ byte_list[i]) & 255 # 每个字节与前一个字节异或,并取低8位
return base64.b64encode(bytes(byte_list)).decode("utf-8") # Base64编码后返回字符串

# 生成设备ID
did = g_did(g_mac())

# 请求体参数
body = {
"aid": "113886342676927",
"auto_play": "0",
"build": "6240300",
"cid": "28065268176",
"did": did,
"epid": "",
"from_spmid": "tm.recommend.0.0",
"ftime": str(int(time.time())),
"lv": "0",
"mid": "0",
"mobi_app": "android",
"part": "1",
"sid": "0",
"spmid": "main.ugc-video-detail.0.0",
"stime": str(int(time.time())),
"sub_type": "0",
"type": "3",
}


def g_sign(data_sign, salt):
"""
计算数据签名,使用SHA-256哈希
"""
sha256 = hashlib.sha256()
sha256.update(data_sign.encode("utf-8")) # 更新数据
if salt != "":
sha256.update(salt.encode("utf-8")) # 如果有盐值,追加到哈希计算中
return sha256.hexdigest() # 返回SHA-256的十六进制字符串


salt = "9cafa6466a028bfb" # 预定义的盐值


def g_data_sign(data_sign):
"""
将请求体参数转换为字符串,以&连接键值对
"""
return "&".join([key + "=" + value for key, value in data_sign.items()])


# 计算签名
data_sign = g_data_sign(body)
print(data_sign) # 打印待签名的字符串
sign = g_sign(data_sign, salt)
print(sign) # 打印计算出的签名

# 添加签名到请求体
body["sign"] = sign
print(body) # 打印最终的请求体

# 将请求体转换为URL编码格式的字符串
body_str = ("&".join([key + "=" + value for key, value in body.items()])).encode("utf-8")

# AES加密密钥和IV(初始化向量)
key = b'fd6b639dbcff0c2a1b03b389ec763c4b' # 32字节密钥(AES-256)
iv = b'77b07a672d57d64c' # 16字节IV


def aes_encrypt(body_str, key, iv):
"""
使用AES CBC模式加密请求体
"""
cipher = AES.new(key, AES.MODE_CBC, iv) # 创建AES加密器
padded_data = pad(body_str, AES.block_size) # 对数据进行PKCS7填充
ciphertext = cipher.encrypt(padded_data) # 加密数据
return ciphertext


# 对请求体进行AES加密
encrypt_body = aes_encrypt(body_str, key, iv)
print(encrypt_body.hex()) # 以十六进制格式打印加密后的数据

请求头参数分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
":authority": "api.bilibili.com",
":method": "POST",
":path": "/x/report/click/android2",
":scheme": "https",
"content-length": "320",
"buvid": "XY17684138CCA764C73112F1B0464F485C112",
"device-id": "PF9tVWMHYgNiUTQGegZ6",
"fp_local": "2e941853742b47fa1e688cd567d1a40320250201120422d47654095c87a2e160",
"fp_remote": "2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683",
"session_id": "d2bb5a0d",
"env": "prod",
"app-key": "android",
"user-agent": "Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3 mobi_app/android build/6240300 channel/alifenfa innerVer/6240300 osVer/9 network/2",
"bili-bridge-engine": "cronet",
"content-type": "application/octet-stream",
"accept-encoding": "gzip, deflate, br"
}

这里面需要还原的参数有buviddevice-idfp_localfp_remotesession_id

buvid参数还原

通过抓包找到请求接口,反编译apk,定位接口

请求头信息一般会在拦截器当中

1

进入拦截器的类进行分析

c

1

查看e()方法

e

1

这里就看到了请求头的参数

先去分析Buvid,通过a3赋值,a3是从com.bilibili.api.c.a()这个方法里获得到的

a

1

从这里面看出a是通过str赋值的,str参数是b方法里传进去的参数,对b方法查找用例

e

查找用例找到了e方法

1

而这里面参数str来源于e方法,继续查找用例

a

查找用例找到了a方法

1

这里面str通过this.a赋值,接着找谁给this.a赋值的

d

找到了一个d方法,这里面对this.a进行了赋值

1

其实还有一种方式,通过打印调用栈信息也可以追溯到参数被赋值的位置

1
2
3
4
5
6
7
8
9
10
11
function hook_b(){
Java.perform(function(){
let c = Java.use("com.bilibili.api.c");
c["b"].implementation = function (str) {
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
console.log(`c.b is called: str=${str}`);
this["b"](str);
};
})
}
hook_b()

hook 结果

1
2
3
4
5
6
7
8
9
10
11
12
13
[Pixel 3::tv.danmaku.bili ]-> java.lang.Throwable
at com.bilibili.api.c.b(Native Method)
at c2.f.b0.c.a.d.e(BL:1)
at c2.f.b0.c.a.d.a(BL:11)
at tv.danmaku.bili.utils.x.a(BL:14)
at tv.danmaku.bili.proc.y.f(BL:1)
at tv.danmaku.bili.proc.c.run(Unknown Source:2)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.os.HandlerThread.run(HandlerThread.java:65)

c.b is called: str=XY17684138CCA764C73112F1B0464F485C112

找到了this.a之后,发现有三种赋值的情况,第一种情况MiscHelperKt.d(e.k().b())是从XML文件和内存中取buvid的值,如果都取不到,会到第二种方式MiscHelperKt.d(e.k().c())也是从XML文件和内存中取buvid的值,如果都取不到,会到第三种方式c2.f.b0.c.b.a.c.a().toUpperCase(),说明第三种方式才是生成buvid的方式

c2.f.b0.c.b.a.c.a()

1

首先调用Application f = BiliContext.f(),返回一个Application类型的字段f

1

可以看到返回的是当前的ActivityThread.currentApplication()

在这里有四种返回值的情况

先看第一种,return "XZ" + e(d) + d;,它与e()方法和d有关,d通过com.bilibili.commons.m.a.d(b2);获得

b2,它通过com.bilibili.lib.biliid.utils.f.c.b(f)获得

com.bilibili.lib.biliid.utils.f.c.b

1

返回j2,它通过g.b(context)生成

g.b

这里可以看出它是与设备状态有关

1

最后的返回值传入了com.bilibili.commons.m.a.d方法

com.bilibili.commons.m.a.d

1

这里把传进去的参数转为字节,然后调用了e()方法

1

这里是把传进去的参数再放到h方法里进行了MD5哈希算法

1

还没完,哈希算法之后对结果又进行了处理。

生成的结果要传入e(d)这个方法里

e

1

它是取字符串的第2位,第12位,第22位拼接起来了

最后把XZe(d)d拼接起来就是buvid

接下来看第二种情况

com.bilibili.lib.biliid.utils.f.a.j

1

它调用com.bilibili.droid.g.c(context)来生成返回值

它与mac地址有关

1

后面调用的方法与第一种情况相同

接下来看第三种情况

com.bilibili.lib.biliid.utils.f.c.a

1

这里面调用g.a(context)来生成返回值

1

它返回的是android_id也就是如下的a2

1

如果a2不为空,会调用com.bilibili.commons.m.a.d,然后返回"XX" + e(d4) + d4

如果a2是空的话,会调用e.k().i().replace(com.bilibili.base.util.d.f, ""),也就是调用i()方法,返回通过replace对字符进行替换

e.k().i()方法如下

1

它其实就是生成了随机的uuid然后字符替换把-设置为空

然后return "XW" + e(replace) + replace

这样看的话,a2为空的这种情况还原buvid是最简单的

Python还原buvid

1
2
3
4
5
import uuid
uuid_data = str(uuid.uuid4()).replace("-","")
t2_12_22 = uuid_data[2]+uuid_data[12]+uuid_data[22]
buvid = ("XW"+t2_12_22+uuid_data).upper()
print(buvid)

session_id参数还原

如下,session_id的值是字段m2,它是com.bilibili.api.a.m()方法的返回值

1

去分析m方法

com.bilibili.api.a.m()

这里通过b调用了gerSessionId()返回的session_id

1

看了一下getSessionId方法,发现它是一个接口方法,要找其实现类

那就去看b是谁,找到一个o方法

1

这个方法接受一个 b 类型的参数 bVar,并将其赋值给 b 静态变量,通过调用 o() 方法来设置 b 变量为一个实现了接口 b 的对象实例。

接下来可以通过hook o方法查看传入的参数bVar是什么类型

1
2
3
4
5
6
7
8
9
function hook_o(){
Java.perform(function(){
let a = Java.use("com.bilibili.api.a");
a["o"].implementation = function (bVar) {
console.log(`a.o is called: bVar=${JSON.stringify(bVar)}`);
this["o"](bVar);
};
})
}

hook 结果

1
2
3
a.o is called: bVar="<instance: com.bilibili.api.a$b, $className: tv.danmaku.bili.utils.p$a>"
接口类型是:com.bilibili.api.a$b
具体类型是:tv.danmaku.bili.utils.p$a

去找tv.danmaku.bili.utils.p$a

tv.danmaku.bili.utils.p.a

在这个类里面找到了gerSessionId()的实现

1

这里是通过com.bilibili.lib.foundation.e.b()来调用的gerSessionId()

b方法的内容

1

它又调用了d.g.b().d()

d方法的内容

1

它返回的是this.a

去找谁给this.a赋值的

1

this.aDefaultApps类型

DefaultApps里面就会有对gerSessionId()的重写,找到了

gerSessionId()

1

调用getString()方法返回一个字符串,e()返回的是SharedPreferences类型, getString()从保存的数据中找键为foundation:session_id的值,如果没有找到,返回l

接下来去找l是怎么赋值的,如下

1

这段代码生成了一个由4个随机字节组成的字节数组,并将这些字节转换为一个十六进制字符串。随后,这个十六进制字符串通过 x.h() 方法进行处理,并赋值给变量 lx.h()用于对hex进行是否为空的检测。

Python生成session_id

1
2
3
4
import random

def g_session_id():
return random.randbytes(4).hex()

fp_local参数还原

如下,fp_local的值为字段j2,它是通过com.bilibili.api.a.j()方法获取的

1

com.bilibili.api.a.j()

1

这里的F()方法是接口方法,b是其实现类,它是在一个o方法里赋值的

1

找到o方法的上层调用,在tv.danmaku.bili.utils.p.b()方法里

1

里面传入的是一个a对象,在a类里有对F()方法的实现

1

F()方法里返回了一个字段a,它使用过c2.f.b0.c.a.c.a(),这个方法获得的

c2.f.b0.c.a.c.a()

1

a方法返回的是Fingerprint.h.c()方法的返回值

Fingerprint.h.c()方法

1

这里是首先定义了一个str为空,然后把类字段a赋值给了str,然后进行返回,所以返回的值其实是a,接下来找a是怎么来的

如下,找到了一个地方对a进行了赋值

1

通过字段d2a进行赋值,接下来找d2是怎么生成的

1

先调用env.d()方法在内存中找fp_local,找不到的话就调用d2 = com.bilibili.lib.biliid.internal.fingerprint.a.a.a(buvidLegacy, aVar);来生成

先分析a()方法里的两个参数是怎么来的

buvidLegacy

buvidLegacy通过String buvidLegacy = c3.a();方法来获取

1

这个a()方法与生成buvid时的a()方法是一样的,它的返回值就是buvid

aVar

aVar通过com.bilibili.lib.biliid.internal.fingerprint.b.a aVar = b;来赋值,

字段b来源于b = Data.a();

这里我hook了一下a()方法

1
2
3
4
5
6
7
8
9
10
11
12
function hook_a(){
Java.perform(function(){
let Data = Java.use("com.bilibili.lib.biliid.internal.fingerprint.data.DataKt");
Data["a"].implementation = function () {
console.log(`Data.a is called`);
let result = this["a"]();
console.log(`Data.a result=${result}`);
return result;
};
})
}
hook_a()

hook 结果

1
2
3
Data.a is called
Data.a result=
Data(main={str_brightness=32, app_version=6.24.0, cpuModel=ARMv8 Processor rev 13 (v8l), speed_sensor=1, adb_enabled=1, screen=1080,2028,440, ui_version=pd1a.180720.030, linear_speed_sensor=1, virtualproc=[], sensors_info=[{"name":"TMD2725 Proximity (wake-up)","vendor":"AMS","version":"1","type":"8","maxRange":"5.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"BMP380 Barometer","vendor":"Bosch","version":"8709","type":"6","maxRange":"1250.0","resolution":"0.1","power":"0.7","minDelay":"40000"}, {"name":"BMP380 Temperature","vendor":"Bosch","version":"8709","type":"33172003","maxRange":"1.0","resolution":"0.1","power":"0.3","minDelay":"200000"}, {"name":"LIS2MDL Magnetometer Uncalibrated","vendor":"STMicro","version":"262","type":"14","maxRange":"4915.2","resolution":"0.1","power":"0.2","minDelay":"10000"}, {"name":"BMI160 Temperature","vendor":"BOSCH","version":"1","type":"33172002","maxRange":"1.0","resolution":"0.1","power":"0.18","minDelay":"200000"}, {"name":"camera v-sync 2","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"camera v-sync 1","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"camera v-sync 0","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"BMI160 Gyroscope Uncalibrated","vendor":"BOSCH","version":"1","type":"16","maxRange":"34.906555","resolution":"0.0010652635","power":"0.9","minDelay":"2500"}, {"name":"BMI160 Gyroscope","vendor":"BOSCH","version":"1","type":"4","maxRange":"34.906555","resolution":"0.0010652635","power":"0.9","minDelay":"2500"}, {"name":"LIS2MDL Temperature","vendor":"STMicro","version":"262","type":"33172004","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"10000"}, {"name":"MAX11261","vendor":"Maxim","version":"1","type":"33172001","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"10000"}, {"name":"BMI160 Accelerometer Uncalibrated","vendor":"BOSCH","version":"1","type":"35","maxRange":"1539.5468","resolution":"4.6983238E-5","power":"0.18","minDelay":"2500"}, {"name":"LIS2MDL Magnetometer","vendor":"STMicro","version":"262","type":"2","maxRange":"4915.2","resolution":"0.1","power":"0.2","minDelay":"10000"}, {"name":"TMD2725 Ambient Light","vendor":"AMS","version":"1","type":"5","maxRange":"1.0","resolution":"0.001","power":"0.001","minDelay":"0"}, {"name":"BMI160 Accelerometer","vendor":"BOSCH","version":"1","type":"1","maxRange":"1539.5468","resolution":"4.6983238E-5","power":"0.18","minDelay":"2500"}, {"name":"Binned Brightness","vendor":"Google","version":"1","type":"65541","maxRange":"255.0","resolution":"1.0","power":"0.2","minDelay":"1000000"}, {"name":"Device Pickup Sensor","vendor":"Google","version":"1","type":"25","maxRange":"1.0","resolution":"1.0","power":"0.25","minDelay":"-1"}, {"name":"Proximity Gated Double Tap Gesture","vendor":"Google","version":"1","type":"65543","maxRange":"1.0","resolution":"0.0","power":"0.001","minDelay":"-1"}, {"name":"Double Twist","vendor":"Google","version":"1","type":"65537","maxRange":"1.0","resolution":"1.0","power":"1.0","minDelay":"0"}, {"name":"Game Rotation Vector Sensor","vendor":"Google","version":"1","type":"15","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Geomagnetic Rotation Vector Sensor","vendor":"Google","version":"1","type":"20","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Gravity Sensor","vendor":"Google","version":"1","type":"9","maxRange":"9.81","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Linear Acceleration Sensor","vendor":"Google","version":"1","type":"10","maxRange":"156.96","resolution":"1.0E-5","power":"1.0","minDelay":"20000"}, {"name":"Orientation Sensor","vendor":"Google","version":"1","type":"3","maxRange":"360.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Rotation Vector Sensor","vendor":"Google","version":"1","type":"11","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Significant Motion","vendor":"Google","version":"1","type":"17","maxRange":"1.0","resolution":"1.0","power":"0.25","minDelay":"-1"}, {"name":"Step Counter","vendor":"Google","version":"1","type":"19","maxRange":"1.8446744E19","resolution":"1.0","power":"0.1","minDelay":"0"}, {"name":"Step Detector","vendor":"Google","version":"1","type":"18","maxRange":"1.0","resolution":"1.0","power":"0.1","minDelay":"0"}, {"name":"Tilt Sensor","vendor":"Google","version":"1","type":"22","maxRange":"1.0","resolution":"0.0","power":"0.25","minDelay":"0"}, {"name":"Device Orientation","vendor":"Google","version":"1","type":"27","maxRange":"3.0","resolution":"1.0","power":"1.0","minDelay":"0"}], app_version_code=6240300, batteryState=BATTERY_STATUS_CHARGING, aaid=, model=Pixel 3, band=g845-00023-180815-B-4956438, wifimac=3C:28:6D:EA:A3:E2, net=["dummy0,fe80::64cc:afff:fe17:88eb%dummy0,66:cc:af:17:88:eb", "r_rmnet_data0,fe80::9615:8f49:b195:c0c2%r_rmnet_data0,", "lo,::1,127.0.0.1,", "wlan0,fe80::3e28:6dff:feea:a3e2%wlan0,192.168.0.112,3c:28:6d:ea:a3:e2", "rmnet_data0,fe80::f39b:f668:3162:3fd1%rmnet_data0,", "rmnet_ipa0,,"], app_id=1, brand=google, cpuCount=8, biometric=1, maps=, btmac=, cpuVendor=Qualcomm, device_angle=-0.011197972,2.7591743,-0.0041870493, str_battery=96, vaid=, build_id=PD1A.180720.030, androidappcnt=260, guid=a05fc3dd-ed19-4d54-b7f7-e0a628a91c2a, files=/data/user/0/tv.danmaku.bili/files, sensor=["TMD2725 Proximity (wake-up),AMS", "BMP380 Barometer,Bosch", "BMP380 Temperature,Bosch", "LIS2MDL Magnetometer Uncalibrated,STMicro", "BMI160 Temperature,BOSCH", "camera v-sync 2,google", "camera v-sync 1,google", "camera v-sync 0,google", "BMI160 Gyroscope Uncalibrated,BOSCH", "BMI160 Gyroscope,BOSCH", "LIS2MDL Temperature,STMicro", "MAX11261,Maxim", "BMI160 Accelerometer Uncalibrated,BOSCH", "LIS2MDL Magnetometer,STMicro", "TMD2725 Ambient Light,AMS", "BMI160 Accelerometer,BOSCH", "Binned Brightness,Google", "Device Pickup Sensor,Google", "Proximity Gated Double Tap Gesture,Google", "Double Twist,Google", "Game Rotation Vector Sensor,Google", "Geomagnetic Rotation Vector Sensor,Google", "Gravity Sensor,Google", "Linear Acceleration Sensor,Google", "Orientation Sensor,Google", "Rotation Vector Sensor,Google", "Significant Motion,Google", "Step Counter,Google", "Step Detector,Google", "Tilt Sensor,Google", "Device Orientation,Google"], gadid=, fstorage=33928413184, virtual=0, memory=3723137024, mid=, oid=, emu=000, is_root=true, battery=96, mac=3C:28:6D:EA:A3:E2, network=WIFI, uid=10243, data_connect_state=0, glimit=, adid=5abb19ceea116b31, mem=3723137024, countryIso=CN, sim=1, root=1, sdkver=0.2.4, light_intensity=4.1807923, boot=976254663, str_app_id=1, oaid=, apps=["1230796800000,com.google.omadm.trigger,1,1.0,1,1230796800000","1230796800000,com.google.android.carriersetup,1,9,28,1230796800000","1230796800000,com.android.cts.priv.ctsshim,1,8.1.0-4396705,27,1230796800000","1230796800000,com.google.android.youtube,1,20.02.40,1552287168,1738224905212","1230796800000,com.vzw.apnlib,1,13.0,14,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.corner,1,1.0,1,1230796800000","1230796800000,com.google.android.ext.services,1,1,1,1230796800000","1729696867893,com.example.hellojnitest,0,1.0,1,1729737782103","1230796800000,com.android.internal.display.cutout.emulation.double,1,1.0,1,1230796800000","1230796800000,com.android.providers.telephony,1,9,28,1230796800000","1230796800000,com.android.sdm.plugins.connmo,1,1.0,1,1230796800000","1230796800000,com.google.android.googlequicksearchbox,1,16.2.40.ve.arm64,301459242,1738224848635","1230796800000,com.android.providers.calendar,1,9,28,1230796800000","1711181586493,net.typeblog.socks,0,1.0.4,13,1711181586493","1230796800000,com.android.providers.media,1,9,900,1230796800000","1711199319522,com.qiyi.video,0,11.5.0,800110550,1711199319522","1230796800000,com.google.android.onetimeinitializer,1,9,28,1230796800000","1230796800000,com.google.android.ext.shared,1,1,1,1230796800000","1230796800000,com.qualcomm.ltebc_vzw,1,MSDC_LA_4.2.01.11.0,42010110,1230796800000","1230796800000,com.quicinc.cne.CNEService,1,1.1,1,1230796800000"], androidsysapp20=["1230796800000,com.google.omadm.trigger,1,1.0,1,1230796800000","1230796800000,com.google.android.carriersetup,1,9,28,1230796800000","1230796800000,com.android.cts.priv.ctsshim,1,8.1.0-4396705,27,1230796800000","1230796800000,com.google.android.youtube,1,20.02.40,1552287168,1738224905212","1230796800000,com.vzw.apnlib,1,13.0,14,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.corner,1,1.0,1,1230796800000","1230796800000,com.google.android.ext.services,1,1,1,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.double,1,1.0,1,1230796800000","1230796800000,com.android.providers.telephony,1,9,28,1230796800000","1230796800000,com.android.sdm.plugins.connmo,1,1.0,1,1230796800000","1230796800000,com.google.android.googlequicksearchbox,1,16.2.40.ve.arm64,301459242,1738224848635","1230796800000,com.android.providers.calendar,1,9,28,1230796800000","1230796800000,com.android.providers.media,1,9,900,1230796800000","1230796800000,com.google.android.onetimeinitializer,1,9,28,1230796800000","1230796800000,com.google.android.ext.shared,1,1,1,1230796800000","1230796800000,com.qualcomm.ltebc_vzw,1,MSDC_LA_4.2.01.11.0,42010110,1230796800000","1230796800000,com.quicinc.cne.CNEService,1,1.1,1,1230796800000","1230796800000,com.qualcomm.qti.smcinvokepkgmgr,1,1.0,1,1230796800000","1230796800000,com.android.documentsui,1,9,28,1230796800000","1230796800000,android.auto_generated_rro__,1,1.0,1,1230796800000"], proc=tv.danmaku.bili, fts=1738507220, os=android, languages=zh, systemvolume=0, free_memory=1727660032, totalSpace=55379472384, accessibility_service=["com.google.android.marvin.talkback/.TalkBackService", "com.google.android.marvin.talkback/com.google.android.accessibility.accessibilitymenu.AccessibilityMenuService", "com.google.android.marvin.talkback/com.google.android.accessibility.selecttospeak.SelectToSpeakService", "com.google.android.marvin.talkback/com.android.switchaccess.SwitchAccessService", "bin.mt.plus/bin.mt.function.ar.ActivityRecordService", "com.estrongs.android.pop/com.estrongs.fs.impl.local.AutoAuthService", "com.qihoo.appstore/.accessibility.AppstoreAccessibility", "com.wandoujia.phoenix2/com.pp.assistant.accessibility.AccessibilityService", "com.xingin.xhs/com.xingin.alpha.common.access.GameRecordAccessibilityService"], osver=9, chid=alifenfa, androidapp20=["1729696867893,com.example.hellojnitest,0,1.0,1,1729737782103","1711181586493,net.typeblog.socks,0,1.0.4,13,1711181586493","1711199319522,com.qiyi.video,0,11.5.0,800110550,1711199319522","1728583109623,com.zx.Justmeplush,0,2.0,2,1728583109623","1737391958923,com.douban.frodo,0,7.89.0,307,1737391958923","1730733792580,com.github.kr328.clash,0,3.0.3.premium,300003,1730733792580","1730769260488,com.example.packerapp,0,1.0,1,1730799296361","1725493032640,com.example.javahookdemo,0,1.0,1,1725974937739","1737130307244,com.ushaqi.zhuishushenqi,0,4.85.73,7973,1737130307244","1732863178328,com.yaotong.crackme,0,1.0,1,1732863178328","1730294059865,com.example.unidbgtest,0,1.0,1,1730295569462","1728375218269,com.izen.abc,0,1.0.9,1,1728375218269","1729958664139,com.example.aestool,0,1.0,1,1729958783366","1730368506985,com.example.develop_test,0,1.0,1,1730373040462","1723908516582,com.example.activitytest,0,1.0,1,1723909442352","1732874628219,com.xingin.xhs,0,8.57.0,8570985,1732961699211","1738373185269,com.zj.wuaipojie2025,0,1.0,1,1738373185269","1726495470725,com.example.plugindex,0,1.0,1,1726495470725","1732961995889,com.netease.community,0,1.2,67,1732961995889","1732244828849,vz.bwu,0,1.0,1,1732244828849"], biometrics=touchid, last_dump_ts=1738576816411, brightness=32, gyroscope_sensor=1, t=1738586160751, kernel_version=4.9.96-g340a9aaf92bc-ab4959234, usb_connected=1, cpuFreq=1766400, gps_sensor=1, data_activity_state=0, axposed=false, first=false}, propery={net.hostname=, ro.boot.hardware=blueline, gsm.sim.state=, ro.build.date.utc=1534990449, ro.product.device=blueline, persist.sys.language=, ro.debuggable=0, net.gprs.local-ip=, ro.build.tags=release-keys, http.proxy=, ro.serialno=, persist.sys.country=, ro.boot.serialno=, gsm.network.type=Unknown, net.eth0.gw=, net.dns1=, sys.usb.state=, http.agent=}, sys={product=blueline, cpu_abi=armeabi-v7a, serial=unknown, display=PD1A.180720.030, fingerprint=google/blueline/blueline:9/PD1A.180720.030/4972053:user/release-keys, cpu_abi2=armeabi, device=blueline, manufacturer=Google, hardware=blueline})

这个结果很长,先放在这里,后面分析。

a()

buvidLegacyaVar这两个参数知道是什么了,接下来再去看a()方法

1

这里面调用了一系列函数得到返回值先看MiscHelperKt.a(f(buvidLegacy, data))里的f(buvidLegacy, data)

f()

1

这里面是调用aVar.a()得到a2

1

这里的a2就是前面aVar里的main

1
main={str_brightness=32, app_version=6.24.0, cpuModel=ARMv8 Processor rev 13 (v8l), speed_sensor=1, adb_enabled=1, screen=1080,2028,440, ui_version=pd1a.180720.030, linear_speed_sensor=1, virtualproc=[], sensors_info=[{"name":"TMD2725 Proximity (wake-up)","vendor":"AMS","version":"1","type":"8","maxRange":"5.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"BMP380 Barometer","vendor":"Bosch","version":"8709","type":"6","maxRange":"1250.0","resolution":"0.1","power":"0.7","minDelay":"40000"}, {"name":"BMP380 Temperature","vendor":"Bosch","version":"8709","type":"33172003","maxRange":"1.0","resolution":"0.1","power":"0.3","minDelay":"200000"}, {"name":"LIS2MDL Magnetometer Uncalibrated","vendor":"STMicro","version":"262","type":"14","maxRange":"4915.2","resolution":"0.1","power":"0.2","minDelay":"10000"}, {"name":"BMI160 Temperature","vendor":"BOSCH","version":"1","type":"33172002","maxRange":"1.0","resolution":"0.1","power":"0.18","minDelay":"200000"}, {"name":"camera v-sync 2","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"camera v-sync 1","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"camera v-sync 0","vendor":"google","version":"1","type":"33172005","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"0"}, {"name":"BMI160 Gyroscope Uncalibrated","vendor":"BOSCH","version":"1","type":"16","maxRange":"34.906555","resolution":"0.0010652635","power":"0.9","minDelay":"2500"}, {"name":"BMI160 Gyroscope","vendor":"BOSCH","version":"1","type":"4","maxRange":"34.906555","resolution":"0.0010652635","power":"0.9","minDelay":"2500"}, {"name":"LIS2MDL Temperature","vendor":"STMicro","version":"262","type":"33172004","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"10000"}, {"name":"MAX11261","vendor":"Maxim","version":"1","type":"33172001","maxRange":"1.0","resolution":"0.1","power":"0.001","minDelay":"10000"}, {"name":"BMI160 Accelerometer Uncalibrated","vendor":"BOSCH","version":"1","type":"35","maxRange":"1539.5468","resolution":"4.6983238E-5","power":"0.18","minDelay":"2500"}, {"name":"LIS2MDL Magnetometer","vendor":"STMicro","version":"262","type":"2","maxRange":"4915.2","resolution":"0.1","power":"0.2","minDelay":"10000"}, {"name":"TMD2725 Ambient Light","vendor":"AMS","version":"1","type":"5","maxRange":"1.0","resolution":"0.001","power":"0.001","minDelay":"0"}, {"name":"BMI160 Accelerometer","vendor":"BOSCH","version":"1","type":"1","maxRange":"1539.5468","resolution":"4.6983238E-5","power":"0.18","minDelay":"2500"}, {"name":"Binned Brightness","vendor":"Google","version":"1","type":"65541","maxRange":"255.0","resolution":"1.0","power":"0.2","minDelay":"1000000"}, {"name":"Device Pickup Sensor","vendor":"Google","version":"1","type":"25","maxRange":"1.0","resolution":"1.0","power":"0.25","minDelay":"-1"}, {"name":"Proximity Gated Double Tap Gesture","vendor":"Google","version":"1","type":"65543","maxRange":"1.0","resolution":"0.0","power":"0.001","minDelay":"-1"}, {"name":"Double Twist","vendor":"Google","version":"1","type":"65537","maxRange":"1.0","resolution":"1.0","power":"1.0","minDelay":"0"}, {"name":"Game Rotation Vector Sensor","vendor":"Google","version":"1","type":"15","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Geomagnetic Rotation Vector Sensor","vendor":"Google","version":"1","type":"20","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Gravity Sensor","vendor":"Google","version":"1","type":"9","maxRange":"9.81","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Linear Acceleration Sensor","vendor":"Google","version":"1","type":"10","maxRange":"156.96","resolution":"1.0E-5","power":"1.0","minDelay":"20000"}, {"name":"Orientation Sensor","vendor":"Google","version":"1","type":"3","maxRange":"360.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Rotation Vector Sensor","vendor":"Google","version":"1","type":"11","maxRange":"1.0","resolution":"1.0E-5","power":"1.0","minDelay":"5000"}, {"name":"Significant Motion","vendor":"Google","version":"1","type":"17","maxRange":"1.0","resolution":"1.0","power":"0.25","minDelay":"-1"}, {"name":"Step Counter","vendor":"Google","version":"1","type":"19","maxRange":"1.8446744E19","resolution":"1.0","power":"0.1","minDelay":"0"}, {"name":"Step Detector","vendor":"Google","version":"1","type":"18","maxRange":"1.0","resolution":"1.0","power":"0.1","minDelay":"0"}, {"name":"Tilt Sensor","vendor":"Google","version":"1","type":"22","maxRange":"1.0","resolution":"0.0","power":"0.25","minDelay":"0"}, {"name":"Device Orientation","vendor":"Google","version":"1","type":"27","maxRange":"3.0","resolution":"1.0","power":"1.0","minDelay":"0"}], app_version_code=6240300, batteryState=BATTERY_STATUS_CHARGING, aaid=, model=Pixel 3, band=g845-00023-180815-B-4956438, wifimac=3C:28:6D:EA:A3:E2, net=["dummy0,fe80::64cc:afff:fe17:88eb%dummy0,66:cc:af:17:88:eb", "r_rmnet_data0,fe80::9615:8f49:b195:c0c2%r_rmnet_data0,", "lo,::1,127.0.0.1,", "wlan0,fe80::3e28:6dff:feea:a3e2%wlan0,192.168.0.112,3c:28:6d:ea:a3:e2", "rmnet_data0,fe80::f39b:f668:3162:3fd1%rmnet_data0,", "rmnet_ipa0,,"], app_id=1, brand=google, cpuCount=8, biometric=1, maps=, btmac=, cpuVendor=Qualcomm, device_angle=-0.011197972,2.7591743,-0.0041870493, str_battery=96, vaid=, build_id=PD1A.180720.030, androidappcnt=260, guid=a05fc3dd-ed19-4d54-b7f7-e0a628a91c2a, files=/data/user/0/tv.danmaku.bili/files, sensor=["TMD2725 Proximity (wake-up),AMS", "BMP380 Barometer,Bosch", "BMP380 Temperature,Bosch", "LIS2MDL Magnetometer Uncalibrated,STMicro", "BMI160 Temperature,BOSCH", "camera v-sync 2,google", "camera v-sync 1,google", "camera v-sync 0,google", "BMI160 Gyroscope Uncalibrated,BOSCH", "BMI160 Gyroscope,BOSCH", "LIS2MDL Temperature,STMicro", "MAX11261,Maxim", "BMI160 Accelerometer Uncalibrated,BOSCH", "LIS2MDL Magnetometer,STMicro", "TMD2725 Ambient Light,AMS", "BMI160 Accelerometer,BOSCH", "Binned Brightness,Google", "Device Pickup Sensor,Google", "Proximity Gated Double Tap Gesture,Google", "Double Twist,Google", "Game Rotation Vector Sensor,Google", "Geomagnetic Rotation Vector Sensor,Google", "Gravity Sensor,Google", "Linear Acceleration Sensor,Google", "Orientation Sensor,Google", "Rotation Vector Sensor,Google", "Significant Motion,Google", "Step Counter,Google", "Step Detector,Google", "Tilt Sensor,Google", "Device Orientation,Google"], gadid=, fstorage=33928413184, virtual=0, memory=3723137024, mid=, oid=, emu=000, is_root=true, battery=96, mac=3C:28:6D:EA:A3:E2, network=WIFI, uid=10243, data_connect_state=0, glimit=, adid=5abb19ceea116b31, mem=3723137024, countryIso=CN, sim=1, root=1, sdkver=0.2.4, light_intensity=4.1807923, boot=976254663, str_app_id=1, oaid=, apps=["1230796800000,com.google.omadm.trigger,1,1.0,1,1230796800000","1230796800000,com.google.android.carriersetup,1,9,28,1230796800000","1230796800000,com.android.cts.priv.ctsshim,1,8.1.0-4396705,27,1230796800000","1230796800000,com.google.android.youtube,1,20.02.40,1552287168,1738224905212","1230796800000,com.vzw.apnlib,1,13.0,14,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.corner,1,1.0,1,1230796800000","1230796800000,com.google.android.ext.services,1,1,1,1230796800000","1729696867893,com.example.hellojnitest,0,1.0,1,1729737782103","1230796800000,com.android.internal.display.cutout.emulation.double,1,1.0,1,1230796800000","1230796800000,com.android.providers.telephony,1,9,28,1230796800000","1230796800000,com.android.sdm.plugins.connmo,1,1.0,1,1230796800000","1230796800000,com.google.android.googlequicksearchbox,1,16.2.40.ve.arm64,301459242,1738224848635","1230796800000,com.android.providers.calendar,1,9,28,1230796800000","1711181586493,net.typeblog.socks,0,1.0.4,13,1711181586493","1230796800000,com.android.providers.media,1,9,900,1230796800000","1711199319522,com.qiyi.video,0,11.5.0,800110550,1711199319522","1230796800000,com.google.android.onetimeinitializer,1,9,28,1230796800000","1230796800000,com.google.android.ext.shared,1,1,1,1230796800000","1230796800000,com.qualcomm.ltebc_vzw,1,MSDC_LA_4.2.01.11.0,42010110,1230796800000","1230796800000,com.quicinc.cne.CNEService,1,1.1,1,1230796800000"], androidsysapp20=["1230796800000,com.google.omadm.trigger,1,1.0,1,1230796800000","1230796800000,com.google.android.carriersetup,1,9,28,1230796800000","1230796800000,com.android.cts.priv.ctsshim,1,8.1.0-4396705,27,1230796800000","1230796800000,com.google.android.youtube,1,20.02.40,1552287168,1738224905212","1230796800000,com.vzw.apnlib,1,13.0,14,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.corner,1,1.0,1,1230796800000","1230796800000,com.google.android.ext.services,1,1,1,1230796800000","1230796800000,com.android.internal.display.cutout.emulation.double,1,1.0,1,1230796800000","1230796800000,com.android.providers.telephony,1,9,28,1230796800000","1230796800000,com.android.sdm.plugins.connmo,1,1.0,1,1230796800000","1230796800000,com.google.android.googlequicksearchbox,1,16.2.40.ve.arm64,301459242,1738224848635","1230796800000,com.android.providers.calendar,1,9,28,1230796800000","1230796800000,com.android.providers.media,1,9,900,1230796800000","1230796800000,com.google.android.onetimeinitializer,1,9,28,1230796800000","1230796800000,com.google.android.ext.shared,1,1,1,1230796800000","1230796800000,com.qualcomm.ltebc_vzw,1,MSDC_LA_4.2.01.11.0,42010110,1230796800000","1230796800000,com.quicinc.cne.CNEService,1,1.1,1,1230796800000","1230796800000,com.qualcomm.qti.smcinvokepkgmgr,1,1.0,1,1230796800000","1230796800000,com.android.documentsui,1,9,28,1230796800000","1230796800000,android.auto_generated_rro__,1,1.0,1,1230796800000"], proc=tv.danmaku.bili, fts=1738507220, os=android, languages=zh, systemvolume=0, free_memory=1727660032, totalSpace=55379472384, accessibility_service=["com.google.android.marvin.talkback/.TalkBackService", "com.google.android.marvin.talkback/com.google.android.accessibility.accessibilitymenu.AccessibilityMenuService", "com.google.android.marvin.talkback/com.google.android.accessibility.selecttospeak.SelectToSpeakService", "com.google.android.marvin.talkback/com.android.switchaccess.SwitchAccessService", "bin.mt.plus/bin.mt.function.ar.ActivityRecordService", "com.estrongs.android.pop/com.estrongs.fs.impl.local.AutoAuthService", "com.qihoo.appstore/.accessibility.AppstoreAccessibility", "com.wandoujia.phoenix2/com.pp.assistant.accessibility.AccessibilityService", "com.xingin.xhs/com.xingin.alpha.common.access.GameRecordAccessibilityService"], osver=9, chid=alifenfa, androidapp20=["1729696867893,com.example.hellojnitest,0,1.0,1,1729737782103","1711181586493,net.typeblog.socks,0,1.0.4,13,1711181586493","1711199319522,com.qiyi.video,0,11.5.0,800110550,1711199319522","1728583109623,com.zx.Justmeplush,0,2.0,2,1728583109623","1737391958923,com.douban.frodo,0,7.89.0,307,1737391958923","1730733792580,com.github.kr328.clash,0,3.0.3.premium,300003,1730733792580","1730769260488,com.example.packerapp,0,1.0,1,1730799296361","1725493032640,com.example.javahookdemo,0,1.0,1,1725974937739","1737130307244,com.ushaqi.zhuishushenqi,0,4.85.73,7973,1737130307244","1732863178328,com.yaotong.crackme,0,1.0,1,1732863178328","1730294059865,com.example.unidbgtest,0,1.0,1,1730295569462","1728375218269,com.izen.abc,0,1.0.9,1,1728375218269","1729958664139,com.example.aestool,0,1.0,1,1729958783366","1730368506985,com.example.develop_test,0,1.0,1,1730373040462","1723908516582,com.example.activitytest,0,1.0,1,1723909442352","1732874628219,com.xingin.xhs,0,8.57.0,8570985,1732961699211","1738373185269,com.zj.wuaipojie2025,0,1.0,1,1738373185269","1726495470725,com.example.plugindex,0,1.0,1,1726495470725","1732961995889,com.netease.community,0,1.2,67,1732961995889","1732244828849,vz.bwu,0,1.0,1,1732244828849"], biometrics=touchid, last_dump_ts=1738576816411, brightness=32, gyroscope_sensor=1, t=1738586160751, kernel_version=4.9.96-g340a9aaf92bc-ab4959234, usb_connected=1, cpuFreq=1766400, gps_sensor=1, data_activity_state=0, axposed=false, first=false}

返回值调用了一个e()方法e(str + a2.get(PersistEnv.KEY_PUB_MODEL) + a2.get("band")),这里面的strbuvidPersistEnv.KEY_PUB_MODEL的值为model,即手机型号,band是设备编号标识,他们拼接在一起作为e()方法的参数

e()

1

它是把拼接的字符串进行了MD5哈希算法

这个可以Python实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import uuid
import hashlib


def g_buvid():
uuid_data = str(uuid.uuid4()).replace("-","")
t2_12_22 = uuid_data[2]+uuid_data[12]+uuid_data[22]
buvid = ("XW"+t2_12_22+uuid_data).upper()
return buvid

def md5_call(data):
# 创建md5对象
md5 = hashlib.md5()
# 更新要加密的数据,参数要是字节类型
md5.update(data.encode("utf-8"))
# 获取加密后的数据,以字节码表示
md5_byte = md5.digest()
return md5_byte

def e(buvid,model,band):
data = buvid+model+band
return md5_call(data)

MiscHelperKt.a()

接下来分析MiscHelperKt.a(f(buvidLegacy, data))

1

通过调用Oe方法返回字段Oe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static String Oe(byte[] bArr, CharSequence charSequence, CharSequence charSequence2, CharSequence charSequence3, int i2, CharSequence charSequence4, kotlin.jvm.c.l lVar, int i3, Object obj) {
if ((i3 & 1) != 0) {
charSequence = ", ";
}
CharSequence charSequence5 = (i3 & 2) != 0 ? "" : charSequence2;
CharSequence charSequence6 = (i3 & 4) == 0 ? charSequence3 : "";
int i4 = (i3 & 8) != 0 ? -1 : i2;
if ((i3 & 16) != 0) {
charSequence4 = "...";
}
CharSequence charSequence7 = charSequence4;
if ((i3 & 32) != 0) {
lVar = null;
}
return Fe(bArr, charSequence, charSequence5, charSequence6, i4, charSequence7, lVar);
}

这里面调用了Fe()方法

1
2
3
4
5
6
7
8
9
10
public static final String Fe(byte[] joinToString, CharSequence separator, CharSequence prefix, CharSequence postfix, int i2, CharSequence truncated, kotlin.jvm.c.l<? super Byte, ? extends CharSequence> lVar) {
kotlin.jvm.internal.x.q(joinToString, "$this$joinToString");
kotlin.jvm.internal.x.q(separator, "separator");
kotlin.jvm.internal.x.q(prefix, "prefix");
kotlin.jvm.internal.x.q(postfix, "postfix");
kotlin.jvm.internal.x.q(truncated, "truncated");
String sb = ((StringBuilder) ne(joinToString, new StringBuilder(), separator, prefix, postfix, i2, truncated, lVar)).toString();
kotlin.jvm.internal.x.h(sb, "joinTo(StringBuilder(), …ed, transform).toString()");
return sb;
}

Fe方法返回一个字段sbsb通过ne()方法获得

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
public static final <A extends Appendable> A ne(byte[] joinTo, A buffer, CharSequence separator, CharSequence prefix, CharSequence postfix, int i2, CharSequence truncated, kotlin.jvm.c.l<? super Byte, ? extends CharSequence> lVar) {
kotlin.jvm.internal.x.q(joinTo, "$this$joinTo");
kotlin.jvm.internal.x.q(buffer, "buffer");
kotlin.jvm.internal.x.q(separator, "separator");
kotlin.jvm.internal.x.q(prefix, "prefix");
kotlin.jvm.internal.x.q(postfix, "postfix");
kotlin.jvm.internal.x.q(truncated, "truncated");
buffer.append(prefix);
int i3 = 0;
for (byte b2 : joinTo) {
i3++;
if (i3 > 1) {
buffer.append(separator);
}
if (i2 >= 0 && i3 > i2) {
break;
}
if (lVar != null) {
buffer.append(lVar.invoke(Byte.valueOf(b2)));
} else {
buffer.append(String.valueOf((int) b2));
}
}
if (i2 >= 0 && i3 > i2) {
buffer.append(truncated);
}
buffer.append(postfix);
return buffer;
}

接下来先对ne()方法里涉及到的参数查找,往前推找到

  • joinTo为前面的MD5哈希字节数组

  • prefix的值为""

  • separator:

  • postfix""

  • i2为-1

  • truncated...

  • lVar为匿名内部类,其invoke方法是对字节型参数进行十六进制格式化,返回其十六进制字符串。

用Python复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def MiscHelperKt_a(joinTo):
buffer = ""
# separator = ":"
postfix = ""
i2 = -1
truncated = "..."
i3 = 0
for i in joinTo:
i3 +=1
# if(i3>1):
# buffer+=separator
if(i2>=0 and i3>i2):
break
buffer+= f"{(i & 0xff):02x}"
if (i2>=0 and i3>i2):
buffer+=truncated
buffer+=postfix
return buffer

h()

接下来分析h()方法

1

返回一个表示当前时间和日期的 Date 对象,精确到毫秒,转为字符串类型。

format是一个表示当前时间和日期的 Date 对象的格式化字符串。

可以通过hook h()查看效果

1
2
3
4
5
6
7
function call_h(){
Java.perform(function(){
let a = Java.use("com.bilibili.lib.biliid.internal.fingerprint.a.a");
var result = a.h();
console.log(result); // 20250203232823
})
}

g()

接下来分析g()方法

1

它返回的是字段a2a2通过com.bilibili.commons.e.a(8)方法获得

1

new了一个8字节的数组,然后随机生成8字节的值,然后返回

Python实现

1
2
3
4
import random

def g_g():
return random.randbytes(8)

g()生成的结果也是作为参数传入了MiscHelperKt.a()

前面这些得到的结果拼接成字符串得到字段str

b()

str作为参数传递给了b()方法

1

这里面先通过q.n1(0, Math.min(fpEntity.length() - 1, 62))方法得到n1,然后调用q.S0(n1, 2)方法得到S0,通过S0获取ghi2的值,这里面可以发现,传入n1方法的参数fpEntity.length()是定值,那么后面的ghi2的值也是定值

可以通过hook b方法,然后主动调用b方法打印出ghi2的值来查看

hook b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook_b(){
Java.perform(function(){
let a = Java.use("com.bilibili.lib.biliid.internal.fingerprint.a.a");
a["b"].implementation = function (fpEntity) {
console.log(`a.b is called: fpEntity=${fpEntity}`);
let result = this["b"](fpEntity);
console.log(`a.b result=${result}`);
// 2e941853742b47fa1e688cd567d1a4032025020418163278d25eb4002944e6 return:2d
// 2e941853742b47fa1e688cd567d1a403202502041817339cfda327f7a0e755 return:b6
return result;
};


})
}
hook_b()

hook_g_h_i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function hook_g_h_i(){
Java.perform(function(){
let i = Java.use("kotlin.g0.i");
i["g"].implementation = function () {
console.log(`i.g is called`);
let result = this["g"]();
console.log(`i.g result=${result}`);
return result;
};
i["h"].implementation = function () {
console.log(`i.h is called`);
let result = this["h"]();
console.log(`i.h result=${result}`);
return result;
};
i["i"].implementation = function () {
console.log(`i.i is called`);
showStacks()
let result = this["i"]();
console.log(`i.i result=${result}`);
return result;
};
})
}

call b

1
2
3
4
5
6
7
function call_b(){
Java.perform(function(){
let a = Java.use("com.bilibili.lib.biliid.internal.fingerprint.a.a");
var tt = a.b(Java.use("java.lang.String").$new("2e941853742b47fa1e688cd567d1a403202502041817339cfda327f7a0e755"));
console.log(tt);
})
}

hook 结果

1
2
3
4
5
6
i.g is called
i.g result=0
i.h is called
i.h result=60
i.i is called
i.i result=2

得出g,h,i2的值分别为0,60,2

使用Python还原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def g_b(fpEntity):
g = 0
h = 60
i2 = 2
i = 0
while(True):
substring = fpEntity[g:g+2]
a2 = 16
i += int(substring,a2)
if (g == h):
break
g+=i2
strformat = f"{i%256:02x}"
return strformat

print(g_b("2e941853742b47fa1e688cd567d1a403202502041817339cfda327f7a0e755"))
>>>
b6

Python还原fp_local参数

结合上面分析的参数生成过程,整合生成fp_local参数

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
import uuid
import hashlib
import random

def MiscHelperKt_a(byte_data):
buffer = ""
# separator = ":"
postfix = ""
i2 = -1
truncated = "..."
i3 = 0
for i in byte_data:
i3 +=1
# if(i3>1):
# buffer+=separator
if(i2>=0 and i3>i2):
break
buffer+= f"{(i & 0xff):02x}"
if (i2>=0 and i3>i2):
buffer+=truncated
buffer+=postfix
return buffer

def g_buvid():
uuid_data = str(uuid.uuid4()).replace("-","")
t2_12_22 = uuid_data[2]+uuid_data[12]+uuid_data[22]
buvid = ("XW"+t2_12_22+uuid_data).upper()
print(buvid)
return buvid

def md5_call(data):
# 创建md5对象
md5 = hashlib.md5()
# 更新要加密的数据,参数要是字节类型
md5.update(data.encode("utf-8"))
# 获取加密后的数据,以字节码表示
md5_byte = md5.digest()
return md5_byte

def e(buvid):
print(type(buvid))
model = "Pixel 3"
band = "g845-00023-180815-B-4956430"
data = buvid+model+band
return md5_call(data)

def h():
return str(20250203232823+random.randint(1,100))

def g():
return random.randbytes(8)

def b(str):
print(str)
g = 0
h = 60
i2 = 2
i = 0
while(True):
substring = str[g:g+2]
a2 = 16
i += int(substring,a2)
if (g == h):
break
g+=i2
strformat = f"{i%256:02x}"
return strformat


def g_d2():
str = MiscHelperKt_a(e(g_buvid()))+h()+MiscHelperKt_a(g())
return str+b(str)

print(g_d2())

fp_remote参数还原

hook生成fp_localfp_remote的两个方法,发现他们的值相似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_fp(){
Java.perform(function(){
let a = Java.use("com.bilibili.api.a");
a["j"].implementation = function () {
console.log(`a.j is called`);
let result = this["j"]();
console.log(`a.j result=${result}`);
return result;
};

a["k"].implementation = function () {
console.log(`a.k is called`);
let result = this["k"]();
console.log(`a.k result=${result}`);
return result;
};
})
}
hook_fp()

hook 结果

1
2
3
4
a.j is called
a.j result=2e941853742b47fa1e688cd567d1a40320250204210435027583fd9976406624
a.k is called
a.k result=2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683

暂时使用生成fp_local的方法来生成fp_remote

整合代码

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
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import random
import uuid

def g_buvid():
uuid_data = str(uuid.uuid4()).replace("-","")
t2_12_22 = uuid_data[2]+uuid_data[12]+uuid_data[22]
buvid = ("XW"+t2_12_22+uuid_data).upper()
print(buvid)
return buvid

buvid = g_buvid()



def g_session_id():
# return random.randbytes(4).hex()
return random.randbytes(4)

session_id = g_session_id()


import hashlib

def MiscHelperKt_a(byte_data):
buffer = ""
# separator = ":"
postfix = ""
i2 = -1
truncated = "..."
i3 = 0
for i in byte_data:
i3 +=1
# if(i3>1):
# buffer+=separator
if(i2>=0 and i3>i2):
break
buffer+= f"{(i & 0xff):02x}"
if (i2>=0 and i3>i2):
buffer+=truncated
buffer+=postfix
return buffer

def md5_call(data):
# 创建md5对象
md5 = hashlib.md5()
# 更新要加密的数据,参数要是字节类型
md5.update(data.encode("utf-8"))
# 获取加密后的数据,以字节码表示
md5_byte = md5.digest()
return md5_byte

def e(buvid):
print(type(buvid))
model = "Pixel 3"
band = "g845-00023-180815-B-4956430"
data = buvid+model+band
return md5_call(data)

def h():
return str(20250203232823+random.randint(1,100))

def g():
return random.randbytes(8)

def b(str):
print(str)
g = 0
h = 60
i2 = 2
i = 0
while(True):
substring = str[g:g+2]
a2 = 16
i += int(substring,a2)
if (g == h):
break
g+=i2
strformat = f"{i%256:02x}"
return strformat


def g_d2(buvid):
str = MiscHelperKt_a(e(buvid))+h()+MiscHelperKt_a(g())
return str+b(str)

def g_fp_local(buvid):
return g_d2(buvid)

fp_local = g_fp_local(buvid)

print(fp_local)

url = "https://api.bilibili.com/x/report/click/android2"


headers = {
# ":authority": "api.bilibili.com",
# ":method": "POST",
# ":path": "/x/report/click/android2",
# ":scheme": "https",
"content-length": "320",
"buvid": "XY17684138CCA764C73112F1B0464F485C112",# buvid,
"device-id": "PF9tVWMHYgNiUTQGegZ6",
"fp_local": "2e941853742b47fa1e688cd567d1a40320250204210435027583fd9976406624", #fp_local,
"fp_remote": "2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683",
"session_id": "49efcb1f", # session_id, #
"env": "prod",
"app-key": "android",
"user-agent": "Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3 mobi_app/android build/6240300 channel/alifenfa innerVer/6240300 osVer/9 network/2",
"bili-bridge-engine": "cronet",
"content-type": "application/octet-stream",
"accept-encoding": "gzip, deflate, br"
}

body_hex = "DDB2205C8585FFD98B6681BD50CDDE7374C61B9C7EC72C8A7CCFF1BA613AA29731C109318B776408E264545723171BF9E8C857152EB6B26785B1FF0E839632D16ED7749C73C69AC0E8B4223F3DA7235A0C8B8E0737F44BF11487940E98A79494679F494D72E8DD8D75E0843E82EB966473AC272133CCBEDACCB5D0F7EA37152E915E678CCE2DB802A61BAB2032AE155691262E32B7C5AABC65CDF5072A0C0874E59D08DDCEB3C0056198873652763C77B9EBE3756A9360A8B183FE20E608D26FD02661A25C67DDB71068E250B52C1794B26B99A95BD88360842E7E1D20CA716897E681931BD7EAF9BC77B3745ED0F521108D2D11A185140523062BDF7B7256462D44EBF2501945A7091FE66F8F1A7E95FD1008ACCC94649442EA232273F15B063647F993D7C40EFDCB2450683BC508A4AAA390283300874C5ADE14B338D1EEAB"
# print(list(bytes.fromhex(body)))
body = bytes.fromhex(body_hex)
print(body)





def aes_decrypt(ciphertext_hex, key, iv):
# 将hex编码的密文转换为bytes
ciphertext = bytes.fromhex(ciphertext_hex)
# 创建AES解密对象
cipher = AES.new(key, AES.MODE_CBC, iv)
# 解密并移除填充
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
return plaintext.decode('utf-8')

# 示例数据
key = b'fd6b639dbcff0c2a1b03b389ec763c4b' # 16字节密钥
iv = b'77b07a672d57d64c' # 16字节IV
ciphertext_hex = body_hex
# 'b2eadf0bc65f99660f2ffbe874d802918231b6f0445bbf1c005253f1b42c251a61835063707effd59fc5800b4377d4241b252ec063dd6ca55f66a738ce7d5de3d5ef17d04714e17e0d3b592e18109e4e4d8d534666159c204dd8c31ec68c92355f57ce3798454155d57b360d9855dc4d8b131dcb9befebe638635b103d40b0f66d33c283c6dd28b49d38e724f7f7abcb7ba16a7a150941bd301275e326c56e89e97210c18b36437fa4d6fde0f7911cbb7b2e80e56051a9f9f7e238ca20da5dbd6481362a85afceb73e55816d6ff8d7f740a5d8c8e3a0c6779ba406f400cb34936c6e1d3534bf0ed2eee6ce743fcf0ce8fd8ac478980f3dc6f1aac8de96e2be031e1e5f7a32bf06ec73840f4632728a576c9d2f4fa6fc76141e961920885e1d57b3a5e4f77a88d285f6eea2398d97a949e9577bba57665c84a5f85a102e07921e'
# 解密
try:
plaintext = aes_decrypt(ciphertext_hex, key, iv)
print("解密后的明文:", plaintext)
except Exception as e:
print("解密失败:", e)


response = requests.post(url,headers=headers,data=body)
print(response.status_code)
print(response.text)
print(response.content)

播放量接口抓包分析

在app里播放量请求要发送两次,一次是在视频开始的时候发送,一次是在视频结束的时候发送

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
url:https://api.bilibili.com/x/report/heartbeat/mobile
请求方式:POST
请求头:
{
":authority": "api.bilibili.com",
":method": "POST",
":path": "/x/report/heartbeat/mobile",
":scheme": "https",
"content-length": "752",
"buvid": "XY17684138CCA764C73112F1B0464F485C112",
"device-id": "PF9tVWMHYgNiUTQGegZ6",
"fp_local": "2e941853742b47fa1e688cd567d1a40320250204210435027583fd9976406624",
"fp_remote": "2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683",
"session_id": "a858237d",
"env": "prod",
"app-key": "android",
"user-agent": "Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3 mobi_app/android build/6240300 channel/alifenfa innerVer/6240300 osVer/9 network/2",
"bili-bridge-engine": "cronet",
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
"accept-encoding": "gzip, deflate, br"
}
请求体:
{
"actual_played_time": "0",
"aid": "113870689534703",
"appkey": "1d8b6e7d45233436", # 设备ID,可自己生成
"auto_play": "0",
"build": "6240300",
"c_locale": "zh-Hans_CN",
"channel": "alifenfa",
"cid": "28010612956",
"epid": "0",
"epid_status": "",
"from": "7",
"from_spmid": "tm.recommend.0.0",
"last_play_progress_time": "0",
"list_play_time": "0",
"max_play_progress_time": "0",
"mid": "0",
"miniplayer_play_time": "0",
"mobi_app": "android",
"network_type": "1",
"paused_time": "0",
"platform": "android",
"play_status": "0",
"play_type": "1",
"played_time": "0",
"quality": "32",
"s_locale": "zh-Hans_CN",
"session": "6935c68dd72b2363eea3e661be4f85c119a40538",
"sid": "0",
"spmid": "main.ugc-video-detail.0.0",
"start_ts": "0",
"statistics": "{\"appId\":1,\"platform\":3,\"version\":\"6.24.0\",\"abtest\":\"\"}",
"sub_type": "0",
"total_time": "0",
"ts": "1738736854",
"type": "3",
"user_status": "0",
"video_duration": "120",
"sign": "e5834de364c3f2863314293077d0c540"
}

请求头参数已经分析过了

下面只需要看请求体参数

需要分析的参数有sessionsign

session参数分析还原

jadx反编译apk搜索report/heartbeat/mobile,双击跳转到接口

1

找到reportV2查找用例,找到如下两个进行分析

1

reportV2()方法转入了一个参数N7,然后去找N7是如何生成的

N7()

1

N7参数通过调用N7()方法返回,如下是N7()源码

1
2
3
private final HeartbeatParams N7(h hVar, boolean z) {
return new HeartbeatParams(hVar.p1(), hVar.r1(), hVar.a1(), hVar.H0(), hVar.J0(), hVar.s1(), hVar.M0(), hVar.C1(), hVar.w1(), hVar.o1(), z ? 0L : hVar.B1(), z ? 0L : hVar.e1(), z ? 0L : hVar.k1(), hVar.E1(), hVar.i1(), hVar.c1(), z ? 0L : hVar.V0(), z ? 0L : hVar.Y0(), hVar.S0(), hVar.N0(), hVar.t1(), hVar.L0(), hVar.f1(), hVar.D1(), z ? 0L : hVar.G0(), hVar.I0(), z ? 0L : hVar.W0(), z ? 0L : hVar.b1());
}

它返回的是new HeartbeatParams(),然后去看HeartbeatParams的构造方法,它里面的参数就是请求体参数

HeartbeatParams()

1

传给session的参数是str,接下来去找str是如何初始化的

回到N7方法找它的第二个参数

1

第二个参数是通过hVar.r1()方法返回的

r1()

1

r1()方法返回的是this.d,找谁给this.d赋值,如下,通过t2()方法传进去的参数strthis.d赋值

t2()

1

t2()查找用例,找到一个b()方法,它里面的参数是g.a.a()的返回值

b()

1

查看g.a.a(),如下

a()

1

这里面new了一个StringBuilder,然后拼接E.t()、系统时间戳和一个1000000内的随机整数,然后对拼接的字符串进行SHA1哈希算法

首先看E.t(),它获取的是buvid

t()

1

python生成sb2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import time
import uuid
import random

def g_buvid():
uuid_data = str(uuid.uuid4()).replace("-","")
t2_12_22 = uuid_data[2]+uuid_data[12]+uuid_data[22]
buvid = ("XW"+t2_12_22+uuid_data).upper()
return buvid

def g_time_mills():
return str(int(time.time()*1000))

def g_randint():
return str(random.randint(0,1000000))

def g_sb2():
return g_buvid()+g_time_mills()+g_randint()

print(g_sb2())

>>>
# 示例输出:XWB4602B50BA2B05246E08EFC046FC23DB8791738743742069992167

生成的参数sb2方法传递给了com.bilibili.commons.m.a.i(sb2)

查看i()方法

i()

1

i()方法里调用了j()方法

查看j()方法

j()

1

j()方法里把参数和SHA1对象传递给了h()方法

查看h()方法

h()

1

这里面对传入的参数进行了SHA1哈希算法

接下来通过hook i()方法来验证这里h()里面是不是标准的SHA1()

1
2
3
4
5
6
7
8
9
10
11
12
function hook_i(){
Java.perform(function(){
let m_a = Java.use("com.bilibili.commons.m.a");
m_a["i"].implementation = function (str) {
console.log(`a.i is called: str=${str}`);
let result = this["i"](str);
console.log(`a.i result=${result}`);
return result;
};
})
}
hook_i()

hook 结果

1
2
a.i is called: str=1738746513112310995
a.i result=ea3ce10248acdfe7ec65d4f6a2791df6465d0261

如下证明h()方法是标准版的SHA1算法

1

Python还原session参数

整合上面的代码

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
import hashlib
import time
import uuid
import random

def g_buvid():
uuid_data = str(uuid.uuid4()).replace("-","")
t2_12_22 = uuid_data[2]+uuid_data[12]+uuid_data[22]
buvid = ("XW"+t2_12_22+uuid_data).upper()
return buvid

def g_time_mills():
return str(int(time.time()*1000))
def g_randint():
return str(random.randint(0,1000000))

def g_sb2():
return g_buvid()+g_time_mills()+g_randint()

def SHA1_call(data):
# 创建sha1对象
sha1 = hashlib.sha1()
sha1.update(data.encode("utf-8"))
# 获取加密后的数据,以16进制表示
sha1_hex = sha1.hexdigest()
return sha1_hex

# 获取加密后的数据,以字节码表示
# sha1_bytes = sha1.digest()
# return sha1_bytes

print(SHA1_call(g_sb2()))

sign参数分析还原

首先像找session参数一样在源码中找是没有找到的,可能是根据已有的参数进行加密后拼接到原来的参数后面的。

分析sign的值,其长度是16字节,猜测其是MD5加密生成的,把请求体中除了sign值的其他参数拼接起来进行MD5哈希算法,发现结果不一致,可能有加盐。换思路分析。

猜测:可能在拦截器中进行的加密,也可能在so文件中进行的加密。同时在拼接sign参数的时候有很大可能会出现&sign=字样。

在jadx里搜索查找&sign=

经过测试验证得到生成sign值的类为如下类

1

双击进去到了toString()方法里

1

hook toString()

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_toString(){
Java.perform(function(){
let SignedQuery = Java.use("com.bilibili.nativelibrary.SignedQuery");
SignedQuery["toString"].implementation = function () {
console.log(`SignedQuery.toString is called`);
let result = this["toString"]();
console.log(`SignedQuery.toString result=${result}`);
showStacks()
return result;
};
})
}
hook_toString()

hook 结果

根据参数找到对应的hook结果

1
2
3
4
5
6
7
8
9
10
11
12
13
SignedQuery.toString is called
SignedQuery.toString result=
actual_played_time=0&aid=113825760153422&appkey=1d8b6e7d45233436&auto_play=0&build=6240300&c_locale=zh-Hans_CN&channel=alifenfa&cid=27862828315&epid=0&epid_status=&from=7&from_spmid=tm.recommend.0.0&last_play_progress_time=0&list_play_time=0&max_play_progress_time=0&mid=0&miniplayer_play_time=0&mobi_app=android&network_type=1&paused_time=0&platform=android&play_status=0&play_type=1&played_time=0&quality=32&s_locale=zh-Hans_CN&session=b027341a1332c43ad68e4072d38d59a2da7a1a88&sid=0&spmid=main.ugc-video-detail.0.0&start_ts=0&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226.24.0%22%2C%22abtest%22%3A%22%22%7D&sub_type=0&total_time=0&ts=1738756372&type=3&user_status=0&video_duration=79&sign=cdc782cf0541724bd87ab0c15950fc0d

java.lang.Throwable
at com.bilibili.nativelibrary.SignedQuery.toString(Native Method)
at com.bilibili.okretro.f.a.c(BL:16)
at com.bilibili.okretro.f.a.a(BL:6)
at com.bilibili.okretro.d.a.execute(BL:24)
at com.bilibili.okretro.d.a$a.run(BL:2)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)

通过调用堆栈找到上一层函数com.bilibili.okretro.f.a.c

1

这里看到在h(hashMap)里传入了一个参数hashMap,通过hook打印hashMap的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function printMap(map){
let key = map.keySet();
let it = key.iterator();
while(it.hasNext()){
let keystr = it.next();
let valuestr = map.get(keystr);
console.log("key:",keystr,"| value:",valuestr);
}
}

function hook_h(){
Java.perform(function(){
let a = Java.use("com.bilibili.okretro.f.a");
a["h"].implementation = function (map) {
console.log(`a.h is called: map=${map}`);
printMap(map)
let result = this["h"](map);
console.log(`a.h result=${result}`);
return result;
};
})
}
hook_h()

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
key: platform | value: android
key: sid | value: 0
key: sub_type | value: 0
key: from | value: 7
key: total_time | value: 24
key: max_play_progress_time | value: 23
key: user_status | value: 0
key: played_time | value: 23
key: mobi_app | value: android
key: epid | value: 0
key: epid_status | value:
key: c_locale | value: zh-Hans_CN
key: actual_played_time | value: 23
key: quality | value: 32
key: last_play_progress_time | value: 23
key: play_type | value: 1
key: miniplayer_play_time | value: 0
key: build | value: 6240300
key: appkey | value: 1d8b6e7d45233436
key: network_type | value: 1
key: aid | value: 113838208844297
key: from_spmid | value: tm.recommend.0.0
key: play_status | value: 0
key: cid | value: 27904771672
key: statistics | value: {"appId":1,"platform":3,"version":"6.24.0","abtest":""}
a.h result=actual_played_time=23&aid=113838208844297&appkey=1d8b6e7d45233436&auto_play=0&build=6240300&c_locale=zh-Hans_CN&channel=alifenfa&cid=27904771672&epid=0&epid_status=&from=7&from_spmid=tm.recommend.0.0&last_play_progress_time=23&list_play_time=0&max_play_progress_time=23&mid=0&miniplayer_play_time=0&mobi_app=android&network_type=1&paused_time=1&platform=android&play_status=0&play_type=1&played_time=23&quality=32&s_locale=zh-Hans_CN&session=2a130cffe590492932d849d889e360e7246dc256&sid=0&spmid=main.ugc-video-detail.0.0&start_ts=0&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226.24.0%22%2C%22abtest%22%3A%22%22%7D&sub_type=0&total_time=24&ts=1738759385&type=3&user_status=0&video_duration=249&sign=5c3cfa0aa7c7d171e1821669a6b55e7e

加密过程应该就在h()方法里

1

h()方法里调用了g()方法

1

g()方法里调用了s()方法

1

s()方法是一个native方法

hook s

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_s(){
Java.perform(function(){
let LibBili = Java.use("com.bilibili.nativelibrary.LibBili");
LibBili["s"].implementation = function (sortedMap) {
console.log(`LibBili.s is called: sortedMap=${sortedMap}`);
printMap(sortedMap)
let result = this["s"](sortedMap);
console.log(`LibBili.s result=${result}`);
return result;
};
})
}
hook_s()

hook 结果

1
2
3
4
LibBili.s is called: sortedMap=[object Object]
map: {actual_played_time=1, aid=113945784358610, appkey=1d8b6e7d45233436, auto_play=0, build=6240300, c_locale=zh-Hans_CN, channel=alifenfa, cid=28214757335, epid=0, epid_status=, from=7, from_spmid=tm.recommend.0.0, last_play_progress_time=1, list_play_time=0, max_play_progress_time=1, mid=0, miniplayer_play_time=0, mobi_app=android, network_type=1, paused_time=2, platform=android, play_status=0, play_type=1, played_time=1, quality=32, s_locale=zh-Hans_CN, session=22b3446fdb1b406d22b87ba3d09bbeb0bb170e21, sid=0, spmid=main.ugc-video-detail.0.0, start_ts=1738823412, statistics={"appId":1,"platform":3,"version":"6.24.0","abtest":""}, sub_type=0, total_time=3, type=3, user_status=0, video_duration=284}
LibBili.s result=
actual_played_time=1&aid=113945784358610&appkey=1d8b6e7d45233436&auto_play=0&build=6240300&c_locale=zh-Hans_CN&channel=alifenfa&cid=28214757335&epid=0&epid_status=&from=7&from_spmid=tm.recommend.0.0&last_play_progress_time=1&list_play_time=0&max_play_progress_time=1&mid=0&miniplayer_play_time=0&mobi_app=android&network_type=1&paused_time=2&platform=android&play_status=0&play_type=1&played_time=1&quality=32&s_locale=zh-Hans_CN&session=22b3446fdb1b406d22b87ba3d09bbeb0bb170e21&sid=0&spmid=main.ugc-video-detail.0.0&start_ts=1738823412&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226.24.0%22%2C%22abtest%22%3A%22%22%7D&sub_type=0&total_time=3&ts=1738823572&type=3&user_status=0&video_duration=284&sign=8c0eaba9d3108d2049b4d46777662214

so的名字是

1

通过hook_NewStringUTF定位

如果sign是在so中加密的,那么在返回字符串的时候一定会调用newStringUTF方法返回字符串

可以通过hook_NewStringUTF来打印相关信息

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
function hook_NewStringUTF(){

let symbols = Module.enumerateSymbolsSync("libart.so");

let addrNewStringUTF = null;

for(let i=0;i<symbols.length;i++){
let symbol = symbols[i];

if(symbol.name.indexOf("NewStringUTF")!=-1 && symbol.name.indexOf("CheckJNI")==-1){

addrNewStringUTF = symbol.address;
console.log("NewStringUTF's addr:",symbol.address," name:",symbol.name);
break
}
}

if(addrNewStringUTF != null){
Interceptor.attach(addrNewStringUTF,{
onEnter:function(args){
var c_string = args[1];
var dataString = c_string.readCString();
if(dataString){
// console.log(dataString)
if(dataString.length===32){
console.log("dataString:",dataString);

console.log(Thread.backtrace(this.context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n')+"\n");
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
}
},
onLeave:function(){
}
})
}
}

在hook结果中找到相关信息,如下,根据抓包中得到的sign在hook结果中搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dataString: d7472d2094ecc6fb5796cedaa92a82a7
dataString: d644c98adea1d82b959db7174774f5cd
0xbe7691a5 libbili.so!0x31a5
0xc84bd08b base.odex!0x43008b

java.lang.Throwable
at com.bilibili.nativelibrary.LibBili.s(Native Method)
at com.bilibili.nativelibrary.LibBili.g(BL:1)
at com.bilibili.okretro.f.a.h(BL:1)
at com.bilibili.okretro.f.a.c(BL:14)
at com.bilibili.okretro.f.a.a(BL:6)
at com.bilibili.okretro.d.a.execute(BL:24)
at com.bilibili.okretro.d.a$a.run(BL:2)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)

hook结果中显示了入参,so名字和函数地址

so分析

用IDA打开so,在静态注册函数中没有找到s()函数

动态注册查找

hook registernatives

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
function hook_RegisterNatives() {
var RegisterNatives_addr = null;
var symbols = Process.findModuleByName("libart.so").enumerateSymbols();
for (var i = 0; i < symbols.length; i++) {
var symbol = symbols[i].name;
if ((symbol.indexOf("CheckJNI") == -1) && (symbol.indexOf("JNI") >= 0)) {
if (symbol.indexOf("RegisterNatives") >= 0) {
RegisterNatives_addr = symbols[i].address;
console.log("RegisterNatives_addr: ", RegisterNatives_addr);
}
}
}
Interceptor.attach(RegisterNatives_addr, {
onEnter: function (args) {
var env = args[0];
var jclass = args[1];
var class_name = Java.vm.tryGetEnv().getClassName(jclass);
var methods_ptr = ptr(args[2]);
var method_count = args[3].toInt32();
// console.log("RegisterNatives method counts: ", method_count);
for (var i = 0; i < method_count; i++) {
var name = methods_ptr.add(i * Process.pointerSize * 3).readPointer().readCString();
var sig = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize).readPointer().readCString();
var fnPtr_ptr = methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2).readPointer();
var find_module = Process.findModuleByAddress(fnPtr_ptr);
if(class_name.indexOf("com.bilibili.nativelibrary.LibBili")!=-1){
console.log("ClasssName: ", class_name, "MethodName: ", name, "Sig: ", sig, "Function_addr: ", fnPtr_ptr, "ModuleName: ", find_module.name, "Fun_Offset: ", ptr(fnPtr_ptr).sub(find_module.base));

}

}
},
onLeave: function () {}
});
}
hook_RegisterNatives()

hook 结果

1
2
3
4
5
6
7
8
ClasssName:  com.bilibili.nativelibrary.LibBili MethodName:  a Sig:  (Ljava/lang/String;)Ljava/lang/String; Function_addr:  0xbc4b3c7d ModuleName:  libbili.so Fun_Offset:  0x1c7d       
ClasssName: com.bilibili.nativelibrary.LibBili MethodName: ao Sig: (Ljava/lang/String;II)Ljava/lang/String; Function_addr: 0xbc4b3c83 ModuleName: libbili.so Fun_Offset: 0x1c83
ClasssName: com.bilibili.nativelibrary.LibBili MethodName: b Sig: (Ljava/lang/String;)Ljavax/crypto/spec/IvParameterSpec; Function_addr: 0xbc4b3c91 ModuleName: libbili.so Fun_Offset: 0x1c91
ClasssName: com.bilibili.nativelibrary.LibBili MethodName: s Sig: (Ljava/util/SortedMap;)Lcom/bilibili/nativelibrary/SignedQuery; Function_addr: 0xbc4b3c97 ModuleName: libbili.so Fun_Offset: 0x1c97
ClasssName: com.bilibili.nativelibrary.LibBili MethodName: so Sig: (Ljava/util/SortedMap;II)Lcom/bilibili/nativelibrary/SignedQuery; Function_addr: 0xbc4b3c9d ModuleName: libbili.so Fun_Offset: 0x1c9d
ClasssName: com.bilibili.nativelibrary.LibBili MethodName: so Sig: (Ljava/util/SortedMap;[B)Lcom/bilibili/nativelibrary/SignedQuery; Function_addr: 0xbc4b3cab ModuleName: libbili.so Fun_Offset: 0x1cab
ClasssName: com.bilibili.nativelibrary.LibBili MethodName: getCpuCount Sig: ()I Function_addr: 0xbc4b3cb3 ModuleName: libbili.so Fun_Offset: 0x1cb3
ClasssName: com.bilibili.nativelibrary.LibBili MethodName: getCpuId Sig: ()I Function_addr: 0xbc4b3cb7 ModuleName: libbili.so Fun_Offset: 0x1cb7

s()函数在偏移为0x1c97

使用IDA跳转至该地址处进行分析

首先看s()方法的返回值,它返回的是一个对象,里面有两个参数是v13v14

1

我们回过头去看jadx里的s()方法,它传入一个sortedMap参数,返回一个SignedQuery对象,返回SignedQuery对象需要调用SignedQuery类的构造方法

我们再去看SignedQuery类的构造方法

1

这里面传入了两个参数,从toString()方法里可以看到其中this.b就是sign值,也就是IDA里看到的返回值中的参数v14

接下来找v14怎么生成的

1

这里的反编译代码不好读,剩下只能靠猜了

查看sub_227C()函数,这个函数进行的数据初始化,看到这四个值很熟悉,是MD5的初始化数据

1

在分析Java层的时候已经猜出来了它并不是标准的MD5,可能加盐了

那么sub_22B0()函数可能就是加盐的函数

尝试对它进行hook

hook_22B0()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook_22B0(){
let function_addr = Module.findBaseAddress("libbili.so").add(0x22B0+1);
Interceptor.attach(function_addr,{
onEnter:function(args){
// console.log("arg0:",args[0].readCString())
console.log("arg1:",args[1].readCString())
console.log("arg2:",args[2])
},
onLeave:function(){
console.log("----over----")

}
})
}
hook_22B0()

hook 结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
arg1: actual_played_time=1&aid=113945784358610&appkey=1d8b6e7d45233436&auto_play=0&build=6240300&c_locale=zh-Hans_CN&channel=alifenfa&cid=28214757335&epid=0&epid_status=&from=7&from_spmid=tm.recommend.0.0&last_play_progress_time=1&list_play_time=0&max_play_progress_time=1&mid=0&miniplayer_play_time=0&mobi_app=android&network_type=1&paused_time=2&platform=android&play_status=0&play_type=1&played_time=1&quality=32&s_locale=zh-Hans_CN&session=22b3446fdb1b406d22b87ba3d09bbeb0bb170e21&sid=0&spmid=main.ugc-video-detail.0.0&start_ts=1738823412&statistics=%7B%22appId%22%3A1%2C%22platform%22%3A3%2C%22version%22%3A%226.24.0%22%2C%22abtest%22%3A%22%22%7D&sub_type=0&total_time=3&ts=1738823572&type=3&user_status=0&video_duration=284
arg2: 0x242
----over----
arg1: 560c52cc
arg2: 0x8
----over----
arg1: d288fed0
arg2: 0x8
----over----
arg1: 45859ed1
arg2: 0x8
----over----
arg1: 8bffd973
arg2: 0x8
----over----
arg1: �
arg2: 0xa
----over----
arg1: p
arg2: 0x8
----over----

主动调用s()方法,并对so方法进行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
function call_s(){
Java.perform(function(){
let LibBili = Java.use("com.bilibili.nativelibrary.LibBili").$new();
let hashmap = Java.use("java.util.HashMap").$new();
hashmap.put("actual_played_time","1");
hashmap.put("aid","113945784358610");
hashmap.put("appkey","1d8b6e7d45233436");
var treemap = Java.use("java.util.TreeMap").$new(hashmap);
let signedquery = LibBili.s(treemap);
console.log("signedquery:",signedquery);
})
}

function hook_22B0(){
let function_addr = Module.findBaseAddress("libbili.so").add(0x22B0+1);
Interceptor.attach(function_addr,{
onEnter:function(args){
// console.log("arg0:",args[0].readCString())
console.log("arg1:",args[1].readCString())
console.log("arg2:",args[2])
},
onLeave:function(){
console.log("----over----")

}
})
}
hook_22B0()

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
call_s()
LibBili.s is called: sortedMap=[object Object]
map: {actual_played_time=1, aid=113945784358610, appkey=1d8b6e7d45233436}
arg1: actual_played_time=1&aid=113945784358610&appkey=1d8b6e7d45233436&ts=1738825391
arg2: 0x4e
----over----
arg1: 560c52cc
arg2: 0x8
----over----
arg1: d288fed0
arg2: 0x8
----over----
arg1: 45859ed1
arg2: 0x8
----over----
arg1: 8bffd973
arg2: 0x8
----over----
arg1: �
arg2: 0xa
----over----
arg1: p
arg2: 0x8
----over----
LibBili.s result=actual_played_time=1&aid=113945784358610&appkey=1d8b6e7d45233436&ts=1738825391&sign=cd3eb3e0e90c07304828f1a2c9581d06
signedquery: actual_played_time=1&aid=113945784358610&appkey=1d8b6e7d45233436&ts=1738825391&sign=cd3eb3e0e90c07304828f1a2c9581d06

分析

在主动调用的时候,在so里面会拼接一个ts参数,当前时间戳,而且如果map长度太短不会生成sign

经过测试MD5加了四次盐,加的盐是560c52ccd288fed045859ed18bffd973

s()方法里做的事情就是给map拼接ts参数,然后加盐,进行MD5哈希算法,如下

1

Python实现

代码整合

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
import requests
import hashlib
import time
import uuid
import random
import urllib.parse
import hashlib

def g_buvid():
uuid_data = str(uuid.uuid4()).replace("-","")
t2_12_22 = uuid_data[2]+uuid_data[12]+uuid_data[22]
buvid = ("XW"+t2_12_22+uuid_data).upper()
return buvid

buvid = g_buvid()

def g_time_mills():
return str(int(time.time()*1000))
def g_randint():
return str(random.randint(0,1000000))

def g_sb2(buvid):
return buvid+g_time_mills()+g_randint()

def SHA1_call(data):
# 创建sha1对象
sha1 = hashlib.sha1()
sha1.update(data.encode("utf-8"))
# 获取加密后的数据,以16进制表示
sha1_hex = sha1.hexdigest()
return sha1_hex

def g_session(buvid):
return SHA1_call(g_sb2(buvid))

session = g_session(buvid)



def md5_call(data):
data+="560c52ccd288fed045859ed18bffd973"
# 创建md5对象
md5 = hashlib.md5()
# 更新要加密的数据,参数要是字节类型
md5.update(data.encode("utf-8"))
# 获取加密后的数据,以16进制表示
md5_hex = md5.hexdigest()
return md5_hex


def g_sign(body):
# body["statistics"] = urllib.parse.quote("{\"appId\":1,\"platform\":3,\"version\":\"6.24.0\",\"abtest\":\"\"}")
body["statistics"] = urllib.parse.quote(body["statistics"])
body_str = "&".join([key+"="+value for key,value in body.items()])
# print(body_str)
sign = md5_call(body_str)
body["statistics"] = urllib.parse.unquote(body["statistics"])
print(md5_call(body_str))
return sign

url = "https://api.bilibili.com/x/report/heartbeat/mobile"

headers = {
"content-length": "751",
"buvid": buvid, # "XY17684138CCA764C73112F1B0464F485C112",
"device-id": "PF9tVWMHYgNiUTQGegZ6",
"fp_local": "2e941853742b47fa1e688cd567d1a40320250204210435027583fd9976406624",
"fp_remote": "2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683",
"session_id": "e5274c78",
"env": "prod",
"app-key": "android",
"user-agent": "Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3 mobi_app/android build/6240300 channel/alifenfa innerVer/6240300 osVer/9 network/2",
"bili-bridge-engine": "cronet",
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
"accept-encoding": "gzip, deflate, br"
}

data = {
"actual_played_time": "0",
"aid": "113954760167085",
"appkey": "1d8b6e7d45233436",
"auto_play": "0",
"build": "6240300",
"c_locale": "zh-Hans_CN",
"channel": "alifenfa",
"cid": "28237826361",
"epid": "0",
"epid_status": "",
"from": "7",
"from_spmid": "tm.recommend.0.0",
"last_play_progress_time": "0",
"list_play_time": "0",
"max_play_progress_time": "0",
"mid": "0",
"miniplayer_play_time": "0",
"mobi_app": "android",
"network_type": "1",
"paused_time": "0",
"platform": "android",
"play_status": "0",
"play_type": "1",
"played_time": "0",
"quality": "32",
"s_locale": "zh-Hans_CN",
"session": session, #"307d1c27886c3c46ae604c3a95e41bc4aaa1f71e", #session,#
"sid": "0",
"spmid": "main.ugc-video-detail.0.0",
"start_ts": "0",
"statistics": "{\"appId\":1,\"platform\":3,\"version\":\"6.24.0\",\"abtest\":\"\"}",
"sub_type": "0",
"total_time": "0",
"ts": str(int(time.time())),# "1738829640", #str(int(time.time())),#
"type": "3",
"user_status": "0",
"video_duration": "36",
}

data["sign"] = g_sign(data) # "e0d490be7bc919094d266999c6c9d8be"
# print(data)
# print(data["sign"])
# print("&".join([key+"="+value for key,value in data.items()]))

response = requests.post(url=url,headers=headers,data=data)
print(response.status_code)
print(response.text)

抓包数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
url:"https://api.bilibili.com/x/report/click/android2"

headers:{
":authority": "api.bilibili.com",
":method": "POST",
":path": "/x/report/click/android2",
":scheme": "https",
"content-length": "320",
"buvid": "XY17684138CCA764C73112F1B0464F485C112",
"device-id": "PF9tVWMHYgNiUTQGegZ6",
"fp_local": "2e941853742b47fa1e688cd567d1a40320250204210435027583fd9976406624",
"fp_remote": "2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683",
"session_id": "e5274c78",
"env": "prod",
"app-key": "android",
"user-agent": "Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3 mobi_app/android build/6240300 channel/alifenfa innerVer/6240300 osVer/9 network/2",
"bili-bridge-engine": "cronet",
"content-type": "application/octet-stream",
"accept-encoding": "gzip, deflate, br"
}

data:
96 1B 90 1A 66 5C 24 11 A2 97 15 58 D9 50 5C 18 B6 E0 ED 42 82 0D 00 5E D3 F8 63 32 7E BD 37 36 EB DC C5 30 74 52 78 1A 06 7D 24 59 01 3E E4 3D 10 95 70 D3 F9 6D D8 E3 9F 1B B4 B5 0B F5 87 2A 6F 65 FE 5D C1 D9 A6 0A 50 44 AE 20 53 26 D5 AC D3 5C F8 D7 EB 2E B3 D3 07 E6 02 E7 B9 27 3D EC C9 DD C4 F8 C7 FE E9 E6 13 DC 51 99 28 8D E5 19 A7 E9 78 52 C5 8B F6 8B FC 7C 7D DD B7 4E 91 59 D1 A3 93 7A 3F E0 A4 AA 0F A3 84 7A 8B 9F F4 24 FE 96 D0 62 BA D2 26 D5 2C FC 7B 39 9E 1A 4D C8 40 4D A7 62 75 6F 03 F6 62 55 B5 81 7D 27 BA 3F B7 8C FA 0C 33 A6 4C 2F 2B 51 D5 1B 0B 73 0D E5 CA AD 78 26 85 32 16 C5 FB B5 75 11 0A CE 48 E3 9A 3E D5 03 63 03 30 44 38 87 1C 64 A9 AF 64 B0 6D 39 BC 37 DA 48 0E DB E7 65 E9 36 CD C8 27 77 D2 68 83 FB 99 18 ED 7A CD B2 4B AD B6 05 81 60 1C 47 4B A4 0D 57 0C 28 BD A0 57 35 1A D6 56 81 ED 6B C3 58 AF 74 3E 02 BB 72 EC 56 7B A0 F7 56 D3 59 FA E7 42 05 61 6A DD 57 C0 D1 32 05 C0 67 77 35 A9 58 65 31 36 A6 A7 AC 85 CE 1C 78 AB 4D
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
url:"https://api.bilibili.com/x/report/heartbeat/mobile"

headers:{
":authority": "api.bilibili.com",
":method": "POST",
":path": "/x/report/heartbeat/mobile",
":scheme": "https",
"content-length": "751",
"buvid": "XY17684138CCA764C73112F1B0464F485C112",
"device-id": "PF9tVWMHYgNiUTQGegZ6",
"fp_local": "2e941853742b47fa1e688cd567d1a40320250204210435027583fd9976406624",
"fp_remote": "2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683",
"session_id": "e5274c78",
"env": "prod",
"app-key": "android",
"user-agent": "Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3 mobi_app/android build/6240300 channel/alifenfa innerVer/6240300 osVer/9 network/2",
"bili-bridge-engine": "cronet",
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
"accept-encoding": "gzip, deflate, br"
}

data:{
"actual_played_time": "0",
"aid": "113954760167085",
"appkey": "1d8b6e7d45233436",
"auto_play": "0",
"build": "6240300",
"c_locale": "zh-Hans_CN",
"channel": "alifenfa",
"cid": "28237826361",
"epid": "0",
"epid_status": "",
"from": "7",
"from_spmid": "tm.recommend.0.0",
"last_play_progress_time": "0",
"list_play_time": "0",
"max_play_progress_time": "0",
"mid": "0",
"miniplayer_play_time": "0",
"mobi_app": "android",
"network_type": "1",
"paused_time": "0",
"platform": "android",
"play_status": "0",
"play_type": "1",
"played_time": "0",
"quality": "32",
"s_locale": "zh-Hans_CN",
"session": "307d1c27886c3c46ae604c3a95e41bc4aaa1f71e",
"sid": "0",
"spmid": "main.ugc-video-detail.0.0",
"start_ts": "0",
"statistics": "{\"appId\":1,\"platform\":3,\"version\":\"6.24.0\",\"abtest\":\"\"}",
"sub_type": "0",
"total_time": "0",
"ts": "1738829640",
"type": "3",
"user_status": "0",
"video_duration": "36",
"sign": "e0d490be7bc919094d266999c6c9d8be"
}
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
url:"https://api.bilibili.com/x/report/heartbeat/mobile"

headers:{
":authority": "api.bilibili.com",
":method": "POST",
":path": "/x/report/heartbeat/mobile",
":scheme": "https",
"content-length": "765",
"buvid": "XY17684138CCA764C73112F1B0464F485C112",
"device-id": "PF9tVWMHYgNiUTQGegZ6",
"fp_local": "2e941853742b47fa1e688cd567d1a40320250204210435027583fd9976406624",
"fp_remote": "2e941853742b47fa1e688cd567d1a4032024123013422042cd4fe24a164fc683",
"session_id": "e5274c78",
"env": "prod",
"app-key": "android",
"user-agent": "Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3 mobi_app/android build/6240300 channel/alifenfa innerVer/6240300 osVer/9 network/2",
"bili-bridge-engine": "cronet",
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
"accept-encoding": "gzip, deflate, br"
}

data:{
"actual_played_time": "10",
"aid": "113954760167085",
"appkey": "1d8b6e7d45233436",
"auto_play": "0",
"build": "6240300",
"c_locale": "zh-Hans_CN",
"channel": "alifenfa",
"cid": "28237826361",
"epid": "0",
"epid_status": "",
"from": "7",
"from_spmid": "tm.recommend.0.0",
"last_play_progress_time": "36",
"list_play_time": "0",
"max_play_progress_time": "36",
"mid": "0",
"miniplayer_play_time": "0",
"mobi_app": "android",
"network_type": "1",
"paused_time": "1",
"platform": "android",
"play_status": "0",
"play_type": "1",
"played_time": "10",
"quality": "32",
"s_locale": "zh-Hans_CN",
"session": "307d1c27886c3c46ae604c3a95e41bc4aaa1f71e",
"sid": "0",
"spmid": "main.ugc-video-detail.0.0",
"start_ts": "1738829483",
"statistics": "{\"appId\":1,\"platform\":3,\"version\":\"6.24.0\",\"abtest\":\"\"}",
"sub_type": "0",
"total_time": "11",
"ts": "1738829651",
"type": "3",
"user_status": "0",
"video_duration": "36",
"sign": "e1e9b0a874cf0575cc7fffd50df09ae8"
}

unidbg

模拟执行s方法

1

so位置libbili.so

分析

s方法的调用位置,如下,被g()方法调用

1
2
3
public static SignedQuery g(Map<String, String> map) {
return s(map == null ? new TreeMap() : new TreeMap(map));
}

返回一个SignedQuery对象

1
2
3
4
public SignedQuery(String str, String str2) {
this.a = str;
this.b = str2;
}

我们的目标是得到SignedQuery里的this.b字段

在补环境的过程中,我们一定会补SignedQuery,在补它的时候传入这两个参数,而我们可以在这里直接拿到这两个参数,就不需要继续补环境了。

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
public class LibBili extends AbstractJni {
private final AndroidEmulator emulator;
private String process = "";
private final Memory memory;
private final VM vm;
private DalvikModule dm;
private Module module;


public LibBili() {
emulator = AndroidEmulatorBuilder
.for32Bit() // for32Bit()
.setProcessName(process)
.setRootDir(new File("target/rootfs"))
.build();
// 设置执行多少条指令切换一次线程
// emulator.getBackend().registerEmuCountHook(10000);
// 开启线程调度器
// emulator.getSyscallHandler().setEnableThreadDispatcher(true);
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/tv/danmaku/bili/bili_6.24.0.apk"));
vm.setJni(this);
vm.setVerbose(true);
dm = vm.loadLibrary("bili",true);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
}

public void sign(){
DvmClass LibBiliClass = vm.resolveClass("com.bilibili.nativelibrary.LibBili");
SortedMap map = new TreeMap();
map.put("actual_played_time", "1");
map.put("aid", "113945784358610");
map.put("appkey", "1d8b6e7d45233436");
map.put("ts", "1738825391");
// map.put("actual_played_time", "1");
// map.put("aid", "113945784358610");
// map.put("appkey", "1d8b6e7d45233436");
// map.put("auto_play", "0");
// map.put("build", "6240300");
// map.put("c_locale", "zh-Hans_CN");
// map.put("channel", "alifenfa");
// map.put("cid", "28214757335");
// map.put("epid", "0");
// map.put("epid_status", "");
// map.put("from", "7");
// map.put("from_spmid", "tm.recommend.0.0");
// map.put("last_play_progress_time", "1");
// map.put("list_play_time", "0");
// map.put("max_play_progress_time", "1");
// map.put("mid", "0");
// map.put("miniplayer_play_time", "0");
// map.put("mobi_app", "android");
// map.put("network_type", "1");
// map.put("paused_time", "2");
// map.put("platform", "android");
// map.put("play_status", "0");
// map.put("play_type", "1");
// map.put("played_time", "1");
// map.put("quality", "32");
// map.put("s_locale", "zh-Hans_CN");
// map.put("session", "22b3446fdb1b406d22b87ba3d09bbeb0bb170e21");
// map.put("sid", "0");
// map.put("spmid", "main.ugc-video-detail.0.0");
// map.put("start_ts", "1738823412");
// map.put("statistics", "{appId:1, platform:3, version:\"6.24.0\", abtest:\"\"}");
// map.put("sub_type", "0");
// map.put("total_time", "3");
// map.put("ts","1738823572");
// map.put("type", "3");
// map.put("user_status", "0");
// map.put("video_duration", "284");
ProxyDvmObject map_proxyobj = (ProxyDvmObject) ProxyDvmObject.createObject(vm,map);
LibBiliClass.callStaticJniMethodObject(emulator,"s(Ljava/util/SortedMap;)Lcom/bilibili/nativelibrary/SignedQuery;",map_proxyobj);
}

public static void main(String[] args) {
LibBili bili = new LibBili();
bili.sign();
}
}

补环境

补isEmpty()

1
2
java.lang.UnsupportedOperationException: java/util/Map->isEmpty()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callBooleanMethod(AbstractJni.java:598)

1
2
3
4
case "java/util/Map->isEmpty()Z":{
Map map = (Map) dvmObject.getValue();
return map.isEmpty();
}

运行,继续补环境

补get(Ljava/lang/Object;)

1
2
java.lang.UnsupportedOperationException: java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)

1
2
3
4
5
6
7
case "java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;":{
Map map = (Map) dvmObject.getValue();
String key = (String) varArg.getObjectArg(0).getValue();
System.out.println(key);
String get = (String) map.get(key);
return ProxyDvmObject.createObject(vm,get);
}

这里打印出来的key

1
2
appkey
ts

运行,继续补环境

补put(Ljava/lang/Object;Ljava/lang/Object;)

1
2
java.lang.UnsupportedOperationException: java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:933)

1
2
3
4
5
6
7
8
9
case "java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;":{
Map map = (Map) dvmObject.getValue();
String key = (String) varArg.getObjectArg(0).getValue();
System.out.println(key);
String value = (String) varArg.getObjectArg(1).getValue();
System.out.println(value);
map.put(key, value);
return ProxyDvmObject.createObject(vm,value);
}

运行,继续补环境

补r(Ljava/util/Map;)

1
2
java.lang.UnsupportedOperationException: com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethod(AbstractJni.java:433)

这里补的是SignedQuery类的r方法,但是Unidbg里没有SignedQuery类,这个类需要自己实现,到jadx里copy过来

然后进行更正,更正完再补

1
2
3
4
5
6
7
case "com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String;":{
Map map = (Map) varArg.getObjectArg(0).getValue();
System.out.println(map);
String ret = SignedQuery.r(map);
System.out.println(ret);
return ProxyDvmObject.createObject(vm,ret);
}

<init>

1
2
java.lang.UnsupportedOperationException: com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:753)

1
2
3
4
5
case "com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V":{
String sign = (String) varArg.getObjectArg(1).getValue();
System.out.println(sign);
return vm.resolveClass("com/bilibili/nativelibrary/SignedQuery").newObject(null);
}

over

运行就出结果了。

打包Python调用

修改,可传入参数

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
package com.tv.danmaku.bili;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

public class LibBili extends AbstractJni {
private final AndroidEmulator emulator;
private String process = "";
private final Memory memory;
private final VM vm;
private DalvikModule dm;
private Module module;


public LibBili() {
emulator = AndroidEmulatorBuilder
.for32Bit() // for32Bit()
.setProcessName(process)
.setRootDir(new File("target/rootfs"))
.build();
// 设置执行多少条指令切换一次线程
// emulator.getBackend().registerEmuCountHook(10000);
// 开启线程调度器
// emulator.getSyscallHandler().setEnableThreadDispatcher(true);
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("apks/bili_6.24.0.apk"));
vm.setJni(this);
// vm.setVerbose(true);
dm = vm.loadLibrary("bili",true);
dm.callJNI_OnLoad(emulator);
module = dm.getModule();
}

public void sign(String ts){
DvmClass LibBiliClass = vm.resolveClass("com.bilibili.nativelibrary.LibBili");
SortedMap map = new TreeMap();
map.put("actual_played_time", "1");
map.put("aid", "113945784358610");
map.put("appkey", "1d8b6e7d45233436");
map.put("ts", ts);
// map.put("actual_played_time", "1");
// map.put("aid", "113945784358610");
// map.put("appkey", "1d8b6e7d45233436");
// map.put("auto_play", "0");
// map.put("build", "6240300");
// map.put("c_locale", "zh-Hans_CN");
// map.put("channel", "alifenfa");
// map.put("cid", "28214757335");
// map.put("epid", "0");
// map.put("epid_status", "");
// map.put("from", "7");
// map.put("from_spmid", "tm.recommend.0.0");
// map.put("last_play_progress_time", "1");
// map.put("list_play_time", "0");
// map.put("max_play_progress_time", "1");
// map.put("mid", "0");
// map.put("miniplayer_play_time", "0");
// map.put("mobi_app", "android");
// map.put("network_type", "1");
// map.put("paused_time", "2");
// map.put("platform", "android");
// map.put("play_status", "0");
// map.put("play_type", "1");
// map.put("played_time", "1");
// map.put("quality", "32");
// map.put("s_locale", "zh-Hans_CN");
// map.put("session", "22b3446fdb1b406d22b87ba3d09bbeb0bb170e21");
// map.put("sid", "0");
// map.put("spmid", "main.ugc-video-detail.0.0");
// map.put("start_ts", "1738823412");
// map.put("statistics", "{appId:1, platform:3, version:\"6.24.0\", abtest:\"\"}");
// map.put("sub_type", "0");
// map.put("total_time", "3");
// map.put("ts","1738823572");
// map.put("type", "3");
// map.put("user_status", "0");
// map.put("video_duration", "284");

ProxyDvmObject map_proxyobj = (ProxyDvmObject) ProxyDvmObject.createObject(vm,map);
LibBiliClass.callStaticJniMethodObject(emulator,"s(Ljava/util/SortedMap;)Lcom/bilibili/nativelibrary/SignedQuery;",map_proxyobj);
}

@Override
public boolean callBooleanMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "java/util/Map->isEmpty()Z":{
Map map = (Map) dvmObject.getValue();
return map.isEmpty();
}
}
return super.callBooleanMethod(vm, dvmObject, signature, varArg);
}

@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature){
case "java/util/Map->get(Ljava/lang/Object;)Ljava/lang/Object;":{
Map map = (Map) dvmObject.getValue();
String key = (String) varArg.getObjectArg(0).getValue();
// System.out.println(key);
String get = (String) map.get(key);
return ProxyDvmObject.createObject(vm,get);
}
case "java/util/Map->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;":{
Map map = (Map) dvmObject.getValue();
String key = (String) varArg.getObjectArg(0).getValue();
// System.out.println(key);
String value = (String) varArg.getObjectArg(1).getValue();
// System.out.println(value);
map.put(key, value);
return ProxyDvmObject.createObject(vm,value);
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}

@Override
public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "com/bilibili/nativelibrary/SignedQuery->r(Ljava/util/Map;)Ljava/lang/String;":{
Map map = (Map) varArg.getObjectArg(0).getValue();
// System.out.println(map);
String ret = SignedQuery.r(map);
// System.out.println(ret);
return ProxyDvmObject.createObject(vm,ret);
}
}
return super.callStaticObjectMethod(vm, dvmClass, signature, varArg);
}

@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature){
case "com/bilibili/nativelibrary/SignedQuery-><init>(Ljava/lang/String;Ljava/lang/String;)V":{
String sign = (String) varArg.getObjectArg(1).getValue();
System.out.println(sign);
return vm.resolveClass("com/bilibili/nativelibrary/SignedQuery").newObject(null);
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}

public static void main(String[] args) {
String ts = args[0];
LibBili bili = new LibBili();
bili.sign(ts);
}
}
1
2
3
4
5
6
import subprocess
arg = "1738825391"

cmd = "java -jar unidbg.jar "+arg
signature = subprocess.check_output(cmd,shell="True",cwd = "unidbg_jar").decode("utf-8")
print(signature)
1
cd3eb3e0e90c07304828f1a2c9581d06