省流
vd_source
分析
算法
应对
移动端分享链接
buvid
mid
b站网页端的视频页面会自动在地址栏加上vd_source参数,算法是不太正确的md5调用,点击分享按钮获得的链接也会包含vd_source,算法是正确的md5(mid)。现代CPU每秒可以计算百万次MD5,所以可以轻易从vd_source还原出分享者的mid。使用Bilibili Evolved脚本可以禁止网页自动加上vd_source。
移动端的分享链接是在服务端生成,生成的链接的参数里有AES加密过的mid,无法破解

从2022年开始,b站网页端会在视频的链接后加上vd_source参数。前几天我才知道这个参数和mid有关,但是这个参数生成的算法,好像没有太多人去研究。
为什么这么说呢,因为一开始我去网上搜这个算法没搜到,自己逆向完后再一搜,搜到了。。。下面就简单写一下分析的过程吧,如果不想看过程可以直接跳到最后。

播放视频的页面只有一个主要的js文件,直接在里面搜索vd_source关键字,可以看到vd_source是调用wt()函数生成的,参数就是用户id,下面去看看wt()里面是啥



看上去应该是md5,也有可能魔改过了,先把代码全抠出来再说。
var r = {
}
r.stringToBytes= function(t) {
for (var e = [], n = 0; n < t.length; n++)
e.push(255 & t.charCodeAt(n));
return e
},
r.bytesToWords = function (bytes) {
for (var words = [], i = 0, b = 0; i < bytes.length; i++, b += 8)
words[b >>> 5] |= bytes[i] << (24 - b % 32);
return words;
}
r.endian= function(t) {
if (t.constructor == Number)
return 16711935 & r.rotl(t, 8) | 4278255360 & r.rotl(t, 24);
for (var e = 0; e < t.length; e++)
t[e] = r.endian(t[e]);
return t
}
r.rotl= function(t, e) {
return t << e | t >>> 32 - e
}
r.bytesToHex= function(t) {
for (var e = [], n = 0; n < t.length; n++)
e.push((t[n] >>> 4).toString(16)),
e.push((15 & t[n]).toString(16));
return e.join("")
}
r.wordsToBytes= function(t) {
for (var e = [], n = 0; n < 32 * t.length; n += 8)
e.push(t[n >>> 5] >>> 24 - n % 32 & 255);
return e
}
const gg = function(t, e, n, r, i, o, a) {
var s = t + (e & r | n & ~r) + (i >>> 0) + a;
return (s << o | s >>> 32 - o) + e
}
const ff = function(t, e, n, r, i, o, a) {
var s = t + (e & n | ~e & r) + (i >>> 0) + a;
return (s << o | s >>> 32 - o) + e
}
const hh = function(t, e, n, r, i, o, a) {
var s = t + (e ^ n ^ r) + (i >>> 0) + a;
return (s << o | s >>> 32 - o) + e
}
const ii = function(t, e, n, r, i, o, a) {
var s = t + (n ^ (e | ~r)) + (i >>> 0) + a;
return (s << o | s >>> 32 - o) + e
}
const o = function(t) {
/*
return null != t && (n(t) || function(t) {
return "function" == typeof t.readFloatLE && "function" == typeof t.slice && n(t.slice(0, 0))
}(t) || !!t._isBuffer)
*/
return false
}
function t(e, n) {
e.constructor == String ? e = n && "binary" === n.encoding ? r.stringToBytes(e) : r.stringToBytes(e) : o(e) ? e = Array.prototype.slice.call(e, 0) : Array.isArray(e) || e.constructor === Uint8Array || (e = e.toString());
for (var s = r.bytesToWords(e), c = 8 * e.length, u = 1732584193, l = -271733879, f = -1732584194, d = 271733878, p = 0; p < s.length; p++)
s[p] = 16711935 & (s[p] << 8 | s[p] >>> 24) | 4278255360 & (s[p] << 24 | s[p] >>> 8);
s[c >>> 5] |= 128 << c % 32,
s[14 + (c + 64 >>> 9 << 4)] = c;
var h = ff
, v = gg
, m = hh
, y = ii;
for (p = 0; p < s.length; p += 16) {
var g = u
, b = l
, w = f
, A = d;
u = h(u, l, f, d, s[p + 0], 7, -680876936),
d = h(d, u, l, f, s[p + 1], 12, -389564586),
f = h(f, d, u, l, s[p + 2], 17, 606105819),
l = h(l, f, d, u, s[p + 3], 22, -1044525330),
u = h(u, l, f, d, s[p + 4], 7, -176418897),
d = h(d, u, l, f, s[p + 5], 12, 1200080426),
f = h(f, d, u, l, s[p + 6], 17, -1473231341),
l = h(l, f, d, u, s[p + 7], 22, -45705983),
u = h(u, l, f, d, s[p + 8], 7, 1770035416),
d = h(d, u, l, f, s[p + 9], 12, -1958414417),
f = h(f, d, u, l, s[p + 10], 17, -42063),
l = h(l, f, d, u, s[p + 11], 22, -1990404162),
u = h(u, l, f, d, s[p + 12], 7, 1804603682),
d = h(d, u, l, f, s[p + 13], 12, -40341101),
f = h(f, d, u, l, s[p + 14], 17, -1502002290),
u = v(u, l = h(l, f, d, u, s[p + 15], 22, 1236535329), f, d, s[p + 1], 5, -165796510),
d = v(d, u, l, f, s[p + 6], 9, -1069501632),
f = v(f, d, u, l, s[p + 11], 14, 643717713),
l = v(l, f, d, u, s[p + 0], 20, -373897302),
u = v(u, l, f, d, s[p + 5], 5, -701558691),
d = v(d, u, l, f, s[p + 10], 9, 38016083),
f = v(f, d, u, l, s[p + 15], 14, -660478335),
l = v(l, f, d, u, s[p + 4], 20, -405537848),
u = v(u, l, f, d, s[p + 9], 5, 568446438),
d = v(d, u, l, f, s[p + 14], 9, -1019803690),
f = v(f, d, u, l, s[p + 3], 14, -187363961),
l = v(l, f, d, u, s[p + 8], 20, 1163531501),
u = v(u, l, f, d, s[p + 13], 5, -1444681467),
d = v(d, u, l, f, s[p + 2], 9, -51403784),
f = v(f, d, u, l, s[p + 7], 14, 1735328473),
u = m(u, l = v(l, f, d, u, s[p + 12], 20, -1926607734), f, d, s[p + 5], 4, -378558),
d = m(d, u, l, f, s[p + 8], 11, -2022574463),
f = m(f, d, u, l, s[p + 11], 16, 1839030562),
l = m(l, f, d, u, s[p + 14], 23, -35309556),
u = m(u, l, f, d, s[p + 1], 4, -1530992060),
d = m(d, u, l, f, s[p + 4], 11, 1272893353),
f = m(f, d, u, l, s[p + 7], 16, -155497632),
l = m(l, f, d, u, s[p + 10], 23, -1094730640),
u = m(u, l, f, d, s[p + 13], 4, 681279174),
d = m(d, u, l, f, s[p + 0], 11, -358537222),
f = m(f, d, u, l, s[p + 3], 16, -722521979),
l = m(l, f, d, u, s[p + 6], 23, 76029189),
u = m(u, l, f, d, s[p + 9], 4, -640364487),
d = m(d, u, l, f, s[p + 12], 11, -421815835),
f = m(f, d, u, l, s[p + 15], 16, 530742520),
u = y(u, l = m(l, f, d, u, s[p + 2], 23, -995338651), f, d, s[p + 0], 6, -198630844),
d = y(d, u, l, f, s[p + 7], 10, 1126891415),
f = y(f, d, u, l, s[p + 14], 15, -1416354905),
l = y(l, f, d, u, s[p + 5], 21, -57434055),
u = y(u, l, f, d, s[p + 12], 6, 1700485571),
d = y(d, u, l, f, s[p + 3], 10, -1894986606),
f = y(f, d, u, l, s[p + 10], 15, -1051523),
l = y(l, f, d, u, s[p + 1], 21, -2054922799),
u = y(u, l, f, d, s[p + 8], 6, 1873313359),
d = y(d, u, l, f, s[p + 15], 10, -30611744),
f = y(f, d, u, l, s[p + 6], 15, -1560198380),
l = y(l, f, d, u, s[p + 13], 21, 1309151649),
u = y(u, l, f, d, s[p + 4], 6, -145523070),
d = y(d, u, l, f, s[p + 11], 10, -1120210379),
f = y(f, d, u, l, s[p + 2], 15, 718787259),
l = y(l, f, d, u, s[p + 9], 21, -343485551),
u = u + g >>> 0,
l = l + b >>> 0,
f = f + w >>> 0,
d = d + A >>> 0
}
return r.endian([u, l, f, d])
}
function calc(num) {
return r.bytesToHex(r.wordsToBytes(t(num)))
} 仔细分析一下可以发现
参数是字符串类型时,这个函数就是标准的md5
参数是数字时,会把每个数字的内容作为int放入数组,比如”12“ -> [1,2],再进行md5,地址栏的vd_source就是这种情况。
你可以使用其他语言来写,以获得更快的速度。我这里用Java试了下,遍历6亿个id耗时92秒,比node大概快15倍。
下面给一个Java的实现
public static String calc(int n) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
digest.reset();
String s = String.valueOf(n);
int index = 0;
byte[] hash = new byte[s.length()];
for (byte b : s.getBytes()) {
hash[index++] = (byte) (b-48);
}
StringBuilder hexString = new StringBuilder();
for (byte b : digest.digest(hash)) {
hexString.append(String.format("%02x", b & 0xFF));
}
return hexString.toString();
}
public static void main(String[] args) {
System.out.println(calc(451537183));
}

上面的程序输出3f134867a7ce5d2911ebe492f213efcb,和上图一致。
另外要注意只有在浏览器地址栏自动加上的vd_source是不标准的md5算法,如果是点击分享按钮获得的链接,这个vd_source就是正常的md5(mid)
那么如何去掉这个vd_source呢?虽然对于大多数人来说,就算别人知道了你的mid也没啥影响,但是看着就是不舒服。。。
这里可以使用Bilibili-Evolved: https://github.com/the1812/Bilibili-Evolved

安装后会在网页左侧显示配置按钮,添加这个组件:https://cdn.jsdelivr.net/gh/the1812/Bilibili-Evolved@master/registry/dist/components/utils/url-params-clean.js

再次刷新页面就会发现不地址栏没有那些参数了。

移动端的分享链接是在服务端生成的。

生成的短链接访问后会重定向到完整的链接
看上去比较有用的参数是buvid和mid。
反编译apk,绕来绕去,找到了buvid的生成逻辑:


大致是一个前缀+md5(x)的第2,12,22个字符+md5(x),和实际看到的buvid是对的上的,但是没找到x是啥。换了两个账号,buvid是一样的,估计这个参数是设备指纹吧。
试了两个账号的mid值
/Todf1sb+JzK3DxeX4w/xX8FTQ/SZMtL1rElX6M3iMo=
so/5QmJU9FyVlzrwUtH9FQ== 不同的账号id长度,一个是短的id451537183,一个长的3546848329468598,结果的长度也不一样了,应该是AES加密了,这个就不可能破解了。