Bilibili app 给视频增加播放量

版本:v6.24.0

image-20240529210050325

请求体里面应该是做了二进制加密

image-20240529210239330

请求头,请求头内容在变更视频的时候 貌似没有变化

image-20240529210317923

打开jadx反编译搜索

image-20240530100857658

接下来我们要找到 reportClick 方法在哪实现,点击查找用例

image-20240530100920643

image-20240530101316853

能看到这里实现了一个很标准的retrofit请求,请求体就是 c0 create,看上去里面有很多参数

1
2
3
4
c0 create = c0.create(
w.d(com.hpplay.sdk.source.protocol.h.E),
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())
);

image-20240530101448077

先看一下这个d.this.H7方法,hook一下,hook到之后可以发现 ,传入的参数在这里组成了一个字典,将其组合成字符串后又进行了 t3.a.i.a.a.a.b.e.b 方法加密,我们再看一下这个加密方法

image-20240530102301637

image-20240530103213019

image-20240530102915624

sign

两个加密方法都放在了一起,里面有sha256 和 AES的字眼,其中还有一个 com.bilibili.commons.m.a.g 方法,看上去是sha256加密

image-20240530104837852

image-20240530103853975

先通过hook a 和 b加密方法,可以看到b方法传入的是参数拼接,然后b加密完成之后成为sign,然后继续拼接到参数后面 &sign=,最后由a方法完成最后加密

image-20240530103950925

1
2
3
4
b is called, params: aid=1404981072&auto_play=99&build=6240300&cid=1555575044&did=OgJhAzYENwQzBDNXK1cr&epid=&from_spmid=tm.recommend.0.0&ftime=1716987226&lv=0&mid=0&mobi_app=android&part=0&sid=0&spmid=main.ugc-video-detail-vertical.0.0&stime=1717036642&sub_type=0&type=3
b ret value is 8434ac3c8793eece06d27dd68ae62794b0c7308531fcc4f55946d2e20e54b9dc
a is called, body: aid=1404981072&auto_play=99&build=6240300&cid=1555575044&did=OgJhAzYENwQzBDNXK1cr&epid=&from_spmid=tm.recommend.0.0&ftime=1716987226&lv=0&mid=0&mobi_app=android&part=0&sid=0&spmid=main.ugc-video-detail-vertical.0.0&stime=1717036642&sub_type=0&type=3&sign=8434ac3c8793eece06d27dd68ae62794b0c7308531fcc4f55946d2e20e54b9dc
a ret value is -115,76,-99,73,100,-36,23,91,-109,48,7,14,61,-7,-70,86,72,10,83,-46,80,-20,-110,-31,1,-29,-55,98,-54,-67,100,-120,-86,81,58,50,-59,70,-119,-66,-42,-34,94,-23,-29,117,89,54,-81,52,-66,63,94,-117,11,-37,-6,-70,-80,-102,57,20,-86,-56,101,-112,-19,-47,-72,64,50,14,-110,16,22,11,-115,54,83,64,-27,-41,60,-126,47,39,69,35,26,-54,-38,-18,-52,-122,-75,-89,-49,-68,11,124,112,-108,-122,-1,64,-65,115,107,73,123,7,3,-67,12,65,-59,-93,-117,-80,105,104,-52,-110,-114,-91,-12,-107,-10,-62,40,12,15,-127,-23,106,116,-27,-90,-39,127,68,71,-19,107,108,-25,-21,-73,-43,46,-121,-19,53,-55,-19,21,-6,56,47,-94,13,-105,-78,123,41,56,37,-64,102,-113,-46,-67,-20,101,46,-127,-30,-97,62,76,-59,121,-104,118,14,18,-55,87,-98,92,79,-58,-95,82,-24,64,-75,-72,-125,21,79,74,-77,69,22,-111,-120,95,50,-1,70,-75,-6,123,71,85,26,9,-53,25,-71,64,-86,69,7,126,44,29,2,-123,-9,32,122,66,-115,44,-98,28,112,-117,-83,26,-114,96,26,52,-18,-87,-117,75,83,-16,106,-45,-35,-5,83,127,-125,92,4,29,3,94,94,52,106,-98,58,109,44,-87,-87,4,-94,-47,122,122,-15,95,70,-120,-64,62,15,-97,86,88,52,-106,-39,38,102,-122,32,-46,66,1,-75,90,-40,-72,-19,40,-60,121,20,30,-83,-97,75,19,-14,-82,29,52,-124,-30,82,-9

我们再看一下 com.bilibili.commons.m.a.g 方法,这个d 在上面,这个d应该就是g方法的 盐,我们在顺带hook一下g

image-20240530105010207

image-20240530105103742

image-20240530105030559

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
Java.perform(function () {
var 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('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 ret = this.H7(j2, j4, i, j5, j6, i2, i3, j7, str, i4, str2, str3);
console.log('H7 ret value is ' + ret);
return ret;
};

let b = Java.use("t3.a.i.a.a.a.b");
b["b"].implementation = function (params) {
console.log('b is called' + ', ' + 'params: ' + params);
let ret = this.b(params);
console.log('b ret value is ' + ret);
return ret;
};

let a = Java.use("com.bilibili.commons.m.a");
a["g"].implementation = function (bArr, bArr2) {
console.log('g is called' + ', ' + 'bArr: ' + bArr + ', ' + 'bArr2: ' + bArr2);
let ret = this.g(bArr, bArr2);
console.log('g ret value is ' + ret);
return ret;
};

b["a"].implementation = function (body) {
console.log('a is called' + ', ' + 'body: ' + body);
let ret = this.a(body);
console.log('a ret value is ' + ret);
return ret;
};

});

很显然 b方法的核心加密就是在g方法中,这里也可将这些字节转换成字符串,那是根据我们前面代码的推断,g方法传入的第一个参数就是b方法传入的参数拼接转换成的字节数组,第二个参数就是加盐d

我们可以写一个sha256加密试一下,算法还原后加密结果是一致的,这样我们就得到了sign,继续拼接到参数后面 用a方法加密

image-20240530105504407

image-20240530105943861

请求体

再回到a方法,能看到 key iv,看上去应该就是一个AES加密了,首先看key, key是 str b转换成字节, b在上面,值是 fd6b639dbcff0c2a1b03b389ec763c4b , iv是 str2, str2又是f22907c,f22907c也在上面 值是 77b07a672d57d64c

然后再看一下 com.bilibili.droid.g0.a.i 方法

image-20240530110435441

image-20240530110800913

i方法看上去是一个很标准的AES加密,也可以顺便hook一下i方法,这里就不hook了,写一个标准的aes加密看一下结果

image-20240530110912284

image-20240530112421037

设备id

接下来我们要获取这些参数, 我们多刷几个视频,看下哪些固定 哪些需要去逆向,经过多次观察,这里面最重要的还是这个did

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
{
"aid": "211800701", # 视频相关ID
"auto_play": "0",
"build": "6240300",
"cid": "518087166", # 视频相关ID
"did": "KREhESMUckN2EyMQbBBsFm5fOAw-XS1HIw", # 设备ID
"epid": "",
"from_spmid": "tm.recommend.0.0",
"ftime": "1652364011", # 首次运行时间
"lv": "0",
"mid": "0",
"mobi_app": "android",
"part": "0",
"sid": "0",
"spmid": "main.ugc-video-detail-vertical.0.0",
"stime": "1654324378", # 当前时间
"sub_type": "0",
"type": "3",
"sign": "49fd0a286217e03941e775ee98b6366998b896e455f11b8518c375d66caa3bc8"
}

{
"aid": "1555222668",
"auto_play": "99",
"build": "6240300",
"cid": "1562722514",
"did": "OgJhAzYENwQzBDNXK1cr",
"epid": "",
"from_spmid": "tm.recommend.0.0",
"ftime": "1716987226",
"lv": "0",
"mid": "0",
"mobi_app": "android",
"part": "0",
"sid": "0",
"spmid": "main.ugc-video-detail-vertical.0.0",
"stime": "1717039914",
"sub_type": "0",
"type": "3",
"sign": "e3e6de95a5d55530896731a4d74f7c3a04359e9ecd7e1b28075767c49c865e97"
}

image-20240530114247366

进去看一下这个 com.bilibili.lib.biliid.utils.f.a.c(f2) 这个方法 先去内存中找 f13197c,如果有的话就直接取,没有的话先有 c2.f.b0.c.a.e.k().f(context) 生成,如果还是没有的话就再去找g方法

image-20240530114341582

先看一下f方法,入眼就是 SharedPreferences 这里就是从xml文件中提取,如果xml文件中没有的话,那就走g方法

image-20240530115503374

再看下g方法,先调用了一个f方法,如果长度小于4的话,就取做拼接处理,然后再用b方法返回,跟进去看f方法,一直往里跟进,看到c方法

image-20240530115737806

image-20240530120851970

image-20240530120901464

image-20240530120916494

所以一开始j2应该是一个mac地址,c方法读取了网卡信息,后面返回给了j2,然后拼接到了 stringBuffer 中,下面又读取了其它信息,这里需要一个个点进去看一下,我们可以先hook一下f方法,在hook之前需要先退出app 并且清除数据,因为他是先读取内存数据

image-20240530142549595

可以看到一共用|拼接了四个字符串,里面都是一些设备信息,我们可以看一下最后一个 i ,往里跟进去的话能看到这里获取到的是手机的sn号,不管是mac地址还是sn号 都是可以通过随机值生成的

image-20240530142947961

image-20240530143048587

image-20240530143254532

然后回到g方法,最后返回了一个b(f),接下来点进去看下b方法,b方法,先把字符串变成字节, 随后又做了位运算,最后做了一个base64编码,我们用python代码还原一下

image-20240530143706068

image-20240530143838948

最后did的结果

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
import random
import string
import base64


def base64_encrypt(data_string):
data_bytes = bytearray(data_string.encode('utf-8'))
data_bytes[0] = data_bytes[0] ^ (len(data_bytes) & 0xFF)
for i in range(1, len(data_bytes)):
data_bytes[i] = (data_bytes[i - 1] ^ data_bytes[i]) & 0xFF
res = base64.encodebytes(bytes(data_bytes))
return res.strip().strip(b"==").decode('utf-8')


def create_random_mac(sep=":"):
data_list = []
for i in range(1, 7):
part = "".join(random.sample("0123456789ABCDEF", 2))
data_list.append(part)
mac = sep.join(data_list)
return mac


def gen_sn():
return "".join(random.sample("123456789" + string.ascii_lowercase, 10))


mac_string = create_random_mac(sep="")
sn = gen_sn()

did = base64_encrypt("{}|||{}".format(mac_string, sn))
print(did)

代码整合

现在sign和did已经获取到了,现在需要获取视频aid 和 cid,这两个id可以通过直接搜索bv号的形式获取目标视频id,接口地址是

1
2
https://api.bilibili.com/x/player/pagelist?bvid=BV16n4y1R7SP&jsonp=jsonp
https://api.bilibili.com/x/web-interface/view?cid={}&bvid=BV16n4y1R7SP

其实请求头里面还有很多参数可以去逆向,通过一些算法生成,这里就不在深追了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def get_video_id_info(exec_url):
session = requests.Session()
bvid = exec_url.rsplit('/')[-1]
url = "https://api.bilibili.com/x/player/pagelist?bvid={}&jsonp=jsonp".format(bvid)
res = session.get(
url, headers=headers
)
cid = res.json()['data'][0]['cid']
url2 = "https://api.bilibili.com/x/web-interface/view?cid={}&bvid={}".format(cid, bvid)
print(url2)
res = session.get(
url=url2,
headers=headers
)
res_json = res.json()
aid = res_json['data']['aid']
view_count = res_json['data']['stat']['view']
duration = res_json['data']['duration']
session.close()
print(aid, bvid, cid, duration, int(view_count))
return aid, bvid, cid, duration, int(view_count)


def create_random_mac(sep=":"):
""" 随机生成mac地址 """
data_list = []
for i in range(1, 7):
part = "".join(random.sample("0123456789ABCDEF", 2))
data_list.append(part)
mac = sep.join(data_list)

return mac


def create_device_id(mac):
"""
根据mac地址生成 3.device_id
:param mac: 传入参数的格式是 00:00:00:00:00
:return:
"""

def gen_sn():
return "".join(random.sample("123456789" + string.ascii_lowercase, 10))

def base64_encrypt(data_string):
data_bytes = bytearray(data_string.encode('utf-8'))
data_bytes[0] = data_bytes[0] ^ (len(data_bytes) & 0xFF)
for i in range(1, len(data_bytes)):
data_bytes[i] = (data_bytes[i - 1] ^ data_bytes[i]) & 0xFF
res = base64.encodebytes(bytes(data_bytes))
return res.strip().strip(b"==").decode('utf-8')

# 1. 生成mac地址(保证mac中的每个元素是不重复的,例如:0000000000)
mac_str = mac

# 2. 去除IP地址中的符号,只保留 48e1e828e02e(变小写)
mac_str = re.sub("[^0-9A-Fa-f]", "", mac_str)
mac_str = mac_str.lower()

# 3. 获取手续序列号
sn = gen_sn()

# 4. 拼接并进行base64加密
total_string = "{}|||{}".format(mac_str, sn)
return base64_encrypt(total_string)

aid, bvid, cid, duration, view_count = get_video_id_info("https://www.bilibili.com/video/BV12H4y1f7o9")

salt = "9cafa6466a028bfb"
key = "fd6b639dbcff0c2a1b03b389ec763c4b"
iv = "77b07a672d57d64c"


def sha_256_encrypt(data_string):
sha = hashlib.sha256()
sha.update(data_string.encode('utf-8'))
sha.update(salt.encode('utf-8'))
return sha.hexdigest()


def aes_encrypt(data_string):
aes = AES.new(
key=key.encode('utf-8'),
mode=AES.MODE_CBC,
iv=iv.encode('utf-8')
)
raw = pad(data_string.encode('utf-8'), 16)
return aes.encrypt(raw)


ctime = int(time.time())

dict1 = {
"aid": "620737824",
"auto_play": "0",
"build": "6240300",
"cid": "1326718543",
"did": "OgJhAzYENwQzBDNXK1cr",
"epid": "",
"from_spmid": "main.ugc-video-detail.0.0",
"ftime": "1717050307",
"lv": "0",
"mid": "0",
"mobi_app": "android",
"part": "1",
"sid": "0",
"spmid": "main.ugc-video-detail.0.0",
"stime": "1717052213",
"sub_type": "0",
"type": "3",
"sign": "7eeecfcb3b06d24683628612dab45923f4e8aea7cc537b6c24c6a1fa79858442"
}
wifi_mac = create_random_mac().upper()
device_id = create_device_id(wifi_mac) # did
info = {
'aid': aid,
'cid': cid,
'part': 1,
'mid': 0,
'lv': 0,
'ftime': ctime - random.randint(100, 1000),
'stime': ctime,
'did': device_id,
'type': 3,
'sub_type': 0,
'sid': '0',
'epid': '',
'auto_play': 0,
'build': 6240300,
'mobi_app': 'android',
'spmid': 'main.ugc-video-detail.0.0',
'from_spmid': 'search.search-result.0.0'
}
data = "&".join(["{}={}".format(key, info[key]) for key in sorted(info.keys())])
sign = sha_256_encrypt(data).lower()
data = "{}&sign={}".format(data, sign)
aes_string = aes_encrypt(data)
print(aes_string)
res = requests.post(
url="https://api.bilibili.com/x/report/click/android2",
headers={
'Host': 'api.bilibili.com',
'buvid': 'XY9CD6A99C42F4B32C6F9507871DE539CBA22',
'device-id': 'OgJhAzYENwQzBDNXK1cr',
'fp_local': '33d543efacc693efde738dd2e22f8683202405292053429f6e44ba95d7198e3d',
'fp_remote': '33d543efacc693efde738dd2e22f8683202405292053429f6e44ba95d7198e3d',
'session_id': '54879107',
'env': 'prod',
'app-key': 'android',
'user-agent': 'Mozilla/5.0 BiliDroid/6.24.0 (bbcallen@gmail.com) os/android model/Pixel 3a mobi_app/android build/6240300 channel/xiaomi innerVer/6240300 osVer/9 network/2',
'bili-bridge-engine': 'cronet',
'content-type': 'application/octet-stream',
},
data=aes_string,
timeout=10

)
print(res.text)
res.close()

最后也是成功的给视频播放量加了个 1

image-20240530155151255

image-20240530155243159