因为脚本开发需要,为了获取 b 站弹幕的引用,琢磨了一整天 b 站源码。
一. 查看全局变量
首先,用 js 扫描 b 站所有自定义变量和方法
var results
(function () {
var currentWindow,
iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
currentWindow = Object.getOwnPropertyNames(window);
results = currentWindow.filter(function(prop) {
return !iframe.contentWindow.hasOwnProperty(prop);
}).sort((a, b) => a.localeCompare(b));
document.body.removeChild(iframe);
}());
var rs = {}
results.map(r => rs[r] = window[r])
console.log('all varibles')
console.log(rs)
var prop = {}
results.filter(r => typeof(r) !== 'function').map(r => prop[r] = window[r])
console.log('all properties')
console.log(prop)
扫描结果如下:
经过一段时间的翻看,没有找到想要的弹幕数组。说明弹幕的藏身之处不在这,应该换一种方法
二. 源码断点
因为到要取弹幕,所以考虑在弹幕下断点。
选择一个滚动弹幕,右键 => break on => attribute modification,我们可以监听此弹幕属性的变化
下一瞬间,弹幕的滚动,导致弹幕身上的属性发生变化。因此它会自动断点到对应代码
格式化压缩的代码,提高可读性
然后,在控制台打印一下同一作用域(函数)内前面的变量,查看是否有想要的东西。
打印 this
没有找到弹幕数组,但是发现每条弹幕的数据结构,this.textData
比对一下 b 站的 xml 弹幕文件,可以看出二者的对应关系
在断点处点击上一个调用栈,定位到上一层函数
继续打印 this,这次找到弹幕数组的引用
至此,通过断点的方式,可以找到弹幕数组。但总不能每次都自己手动操作,现在问题转化为,如何编写 js,自动引用这个变量?
三. 源码分析
分析 Fa
将整段代码拷贝到代码编辑器,然后格式化,折叠所有能折叠的函数块,可以看出整体结构
我们断的行数在 32810,位于 function Fa() {...} 内,现在分析一下这个 Fa。
不同于其他代码块(立即执行函数), Fa 是全局函数,回顾一下第一步扫描到的全局变量(图 3),确实能看到 Fa。
既然是全局函数,那么意味着两点,我们一可以直接调用,二可以进行修改。通过如下方式:
eval("var Fa="+ Fa.toString().replace('{', '{alert(1234567);')) || Fa()
下面通过一个简单的例子说明这行代码生效的原理
现在试一下修改 Fa 函数,在之前我们断点得到弹幕数组的代码段附近,插入 window.foo = this,尝试将局部变量提升为全局。
eval("var Fa="+ Fa.toString().replace('this.Mw(b)', 'window.foo=this;this.Mw(b)')) || Fa()
注:这里
|| Fa()
指在函数修改后立即运行它。根据 js 语法,一个函数如果没有返回语句,默认返回 undefined。eval 就是这样的函数,所以它的执行结果总为 undefined。根据兜底写法,||
左边为假,那么右边就会执行
返回控制台,打印 window.foo,然而没有想要的结果
这有点困惑,明明只要弹幕还在滚动,这一段就应该被执行。说明修改的这一段,并没有经过。可能是修改的时机太晚,也可能是这段代码虽然通过 Fa 定义, 但却是借助其他方式执行。
现在在 Fa 函数第一句下断点,看看到底是谁,在调用它
这是一段有点特别的代码,特别在它不在立即执行函数内,是全局代码
La, Va 都可以在全局变量找到。其实就是把Fa() 执行结果的一个成员函数,暴露为全局函数。
window.bilibiliPlayer = Fa().bilibiliPlayer
回顾一下前面扫描的全局变量,bilibiliPlayer 确实是其中之一。
作为 Fa() 调用结果返回的 bilibiliPlayer,是 Fa 留下的对外接口,接着分析这个函数
分析 bilibiliPlayer
在一个函数上面下断点,除了像前面通过调用栈或者直接搜索函数名找到,还有一种方法:console.dir
往往在断点后 log 出一个匿名函数时,配合 console.dir 可以很轻易地找到到该函数的定义
在第 5915 行断点, log 一下参数
虽然说参数里的 cid, aid, attribute,在之前扫描的全局变量里可以找到(window.cid, window.aid, window.grayManager.playerParams),
但是不够全,这里可以点击上一层调用栈,查看调用 bilibiliPlayer 的语句以及它传入的参数
可以看出 bilibiliPlayer 是一个构造函数,传入 u,返回全局变量 player。
对照参数表,可知 u 就是 b,而因为缺省第二参数 c 为 undefined
查看上文,u 只在这几句赋值
var u = this.update_params(!0) 与 this 有关,所以看一下 this
比对全局变量,很快找到对应
稍改上面代码,用 js 就能生成想要的参数
var paramU = GrayManager.update_params(!0);
paramU.p = window.pageno || GrayManager.HashManage.get("page") || GrayManager.GetUrlValue("p") || 1,
paramU.urlparam && (paramU.extra_params = window.decodeURIComponent(paramU.urlparam))
由于 player = new bilibiliPlayer(u),所以 5914 行函数作用域内的 this,其实指的是 player。
继续断点执行,可以发现 5916 行的 f 是一个重量型的数据,虽然在断点分析过程中,身上没有弹幕数组(断点较早,还没处理弹幕请求),但是当我们用 temp1 将引用指向它,并在弹幕出现后再查看,可以在它上面找到弹幕
找到了弹幕内存的引用,那么下一步就是修改函数,将对应的变量提升为全局变量
eval("var Fa="+ Fa.toString().replace('play:function', 'f,play:function')) || Fa()
这里参照前面截图第 5916 行到 5919 行,把 f 嵌入全局变量 player 里,暴露在全局作用域
当然这里也可以在 5616 到 5617 行添加 window.f = f,上面的写法只是考虑到代码混淆希望被替换的代码特征越明显、越不易被混淆越好
钩子放好了,现在执行 player = new bilibiliPlayer(u) 函数,使代码经过
var paramU = GrayManager.update_params(!0);
paramU.p = window.pageno || GrayManager.HashManage.get("page") || GrayManager.GetUrlValue("p") || 1,
paramU.urlparam && (paramU.extra_params = window.decodeURIComponent(paramU.urlparam))
player = new bilibiliPlayer(paramU)
然后查看 player.f
有了
展开至弹幕数组
有了!
按捺激动的心情,修改一下弹幕数组的内容,看看视频里的弹幕会不会改变
player.f.g.g.xd.map(d => d.text = 'rng牛逼')
直接生效。
四. 解决方案
eval("var Fa="+ Fa.toString().replace('play:function', 'f,play:function')) || Fa()
var paramU = GrayManager.update_params(!0);
paramU.p = window.pageno || GrayManager.HashManage.get("page") || GrayManager.GetUrlValue("p") || 1,
paramU.urlparam && (paramU.extra_params = window.decodeURIComponent(paramU.urlparam))
player = new bilibiliPlayer(paramU)
执行代码后,player.f.g.g.xd 就是弹幕的引用。所有对它的修改对视频里即将出现的弹幕影响都是实时的(注:这个方案只适用于旧版 b 站)
五. 应用
大费周章,当然不仅仅是为了修改弹幕。通过弹幕引用,还可以实现以下功能
1. 实时弹幕
做一个实时弹幕,一种方法是连接服务器,以网址(或 av 号)为 id 划分房间推送&获取信息。但是这样的话人少没有效果,人多又会给服务器带来压力。
那为何不利用 b 站所有使用弹幕的用户呢?
回顾一下之前解析的弹幕参数
stime 代表弹幕的出现时间,date 代表弹幕的创建时间 (以秒为单位)
b 站的弹幕,从出现到消失的持续时间与内容和形式无关,与播放器宽度有关,实测普通宽度 5s,全屏 15s
根据这点,我们可以用一句代码,过滤 stime 在视频进度附近且创建时间在 10s 内的弹幕
player.f.g.g.xd.filter(d => d.stime - player.getCurrentTime() < 5000 && +(new Date) / 1E3 - d.date < 10)
自定义颜色,可以区分实时弹幕与普通弹幕;设置定时器,可以让刷新实时化
const color = 11206570
setInterval(() => {
player.f.g.g.xd.filter(d => d.stime - player.getCurrentTime() < 5000 && +(new Date) / 1E3 - d.date < 10).map(d => d.color = color)
}, 100)
在旧版 b 站实测,打开一部刚刚更新的百万追番视频,很完美地达成了预期效果
新建一个书签,然后将下面的代码保存到地址栏。在视频播放时直接点击,即可加载脚本
javascript:var s=document.createElement('script');s.src='https://scripts.simenchan.xyz/rd.js';document.body.append(s)
2. 旧番复活
b 站曾经各种原因下架了很多番剧。
之前写了一个从 b 站上传本地视频的 脚本,可以将本地视频直接上传到 b 站观看
只不过视频可以替换,弹幕却不能。现在既然能够获得弹幕内存的引用,顺便解决了弹幕嵌入的问题。那么添加一个功能,上传本地弹幕就非常轻松。
顺便一提的是,之前已经有人下载上传了 b 站大量下架视频弹幕,并集成搜索引擎。其中既包含从 b 站下架的火影,银魂等被版权的动漫IP,也有一些因为审查下架的番剧,还有各种国产、英剧、美剧以及日剧弹幕
视频 + 弹幕,直接让已经消失的旧番在 b 站重现
新建一个书签,将地址保存为下方代码,需要时点击即可
javascript:var s=document.createElement('script');s.src='https://dantecsm.github.io/bilibili-localplayer/index.js';document.body.append(s)
实际使用过程中,发现画面和弹幕有时会出现不同步问题,所以针对它加了一些便利机能。 这里是完整项目
3. 通过弹幕反查发送者
弹幕参数里,有一个参数叫 uid。一般而言,用户的 uid 应该都是数字,不会出现字母,这里很明显是通过加密。
找到 GrayManager.get_ip_crc
函数,能看出实际上就是利用 crc32
实现转换。
那么反推回去即可
下面是解密函数,通过调用 hash2raw('弹幕的 uid 参数')
,就能拿到用户的真实 uid
const CRC_Polynomial = 0xEDB88320
var table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D";
var xtable = table.split(' ').map(n => +`0x${n}`)
function unsigned_shift(i, n) {
var unsigned = String(i)
return unsigned >>> n
}
function CRC_32(input) {
if(typeof input !== 'string') {
input = String(input)
}
var CRC_start = 0xFFFFFFFF
for(var i=0; i<input.length; ++i) {
var index = (CRC_start ^ input.charCodeAt(i)) & 0xff
CRC_start = unsigned_shift(CRC_start, 8) ^ xtable[index]
}
return CRC_start
}
function CRC_32_last_index(input) {
if(typeof input !== 'string') {
input = String(input)
}
var CRC_start = 0xFFFFFFFF
var index
for(var i=0; i<input.length; ++i) {
index = (CRC_start ^ input.charCodeAt(i)) & 0xff
CRC_start = unsigned_shift(CRC_start, 8) ^ xtable[index]
}
return index
}
function get_CRC_index(input) {
for(var i=0; i<256; i++) {
if(unsigned_shift(xtable[i], 24) === input) {
return i
}
}
return -1
}
function deepCheck(input, index) {
var tc
var str = ''
var hash = CRC_32(input)
tc = hash & 0xff ^ index[2]
if(!(tc<=57 && tc>=48)) {
return [0]
}
str += (tc - 48)
hash = xtable[index[2]] ^ unsigned_shift(hash, 8)
tc = hash & 0xff ^ index[1]
if(!(tc<=57 && tc>=48)) {
return [0]
}
str += (tc - 48)
hash = xtable[index[1]] ^ unsigned_shift(hash, 8)
tc = hash & 0xff ^ index[0]
if(!(tc<=57 && tc>=48)) {
return [0]
}
str += (tc - 48)
return [1, str]
}
function hash2raw(input) {
var index = new Array()
var ht = parseInt(input, 16) ^ 0xFFFFFFFF
var snum
var i
var last_index
var deepCheckData
for(var i=3; i>=0; i--) {
index[3-i] = get_CRC_index(unsigned_shift(ht, (i*8)))
snum = xtable[index[3-i]]
ht = ht ^ unsigned_shift(snum, ((3-i)*8))
}
for(var i=0; i<500000000; i++) {
last_index = CRC_32_last_index(i)
if(last_index === index[3]) {
deepCheckData = deepCheck(i, index)
if(deepCheckData[0]) {
break
}
}
}
if(i === 500000000) {
return -1
}
return `${i}${deepCheckData[1]}`
}
一言蔽之,现在通过任一条弹幕,我们可以找到弹幕的发送者
暂无评论