背景

目前还没研究具体的协议实现,只实现了二进制还原JSON的过程。

因为 pure-live 项目,开始对企鹅电竞进行分析。直播流的获取还是比较简单的,参考 real-url 项目,发几个HTTP请求就拿到了。难的是 websocket 协议的破解。

看了 real-url 的实现是模拟了 js 的过程,但是模拟的过程非常抽象,无法理解其宏观实现。本来也打算跟着写下来,但是 pythongolang 的太多地方不同了,python 随便返回,对象里嵌套对象。但是 golang 却无法这么轻松的做到,模仿实现实在是困难。

既然无法模仿 python 代码,但是其又来自于网页的 js 代码。不如直接找 js 代码,直接用 gojagolang 里跑这段 js 不就行了?

这段未完成的模拟实现代码放到了 gist ,等待大佬完成它,效率肯定更高。

https://gist.github.com/iyear/b580a87c1a76c564c77783e02b98a364

分析

real-url 项目也没有说 js 的位置,所以我们自己抓抓看。

由于并不会 js 逆向,只能用一些土办法找到相关代码。

随便点开一个直播间,F12,Search。

根据 python 代码,就用 event_id 作为关键词搜索整个网页。结果就三个。依次查看上下逻辑,找到最像 python 实现的代码。

发现 https://static.egame.qq.com/pgg_pcweb/v2/7873f0f1dec453d99a98.js

复制到 webstorm 中,查看上下导出模块,发现应该逻辑都在 725 这个 id 中,把 725 的复制出来。

发现

这里和 python 代码非常像了,大概率就是了。去除与t赋值相关的模块导出代码,把 decodePacket 写成单独的函数,然后把整个实现逻辑单独拎出来。

先去掉那些 encode 的代码,把代码内含 encode set 等字样的代码都去掉。

写个 hex2ArrayBuffer 函数,把抓到的 websocket 包以十六进制字符串扔进去看看能解析出啥东西。 为了还原运行环境,所以先放到浏览器运行。

完美解析,但是浏览器提供的库比本地多得多,那么再在 webstorm 中运行这段代码,果然报错了。

ReferenceError: e is not defined

开始分析

b = "undefined" != typeof window ? window : void 0 !== e ? e : "undefined" != typeof self ? self : {}

发现这段代码似乎可以直接去除,直接给 b 空对象即可。去除,再次运行,报了另一个错。

Error: TextDecoder is required

跳转到代码处,查看文档,发现 TextDecoder 并非 js 原生提供的库,而是浏览器提供的。但是把整个 TextDecoder 的代码再嵌入进去就太重了。如何解决 utf8 解码?

想了很久,想到一个做法,学 golang 的时候,都知道 golang 中的 string 都是 utf8 编码的。那我把 buffer 传给 golang 转成 string 再传回来不就可以了吗?

这点用 goja 可以方便地实现(感谢这么好,实现这么全的 js 引擎库)。把 ArrayBuffer 传进来,string(buf) 即可。

vm.Set("utf8Decode", vm.ToValue(func(call goja.FunctionCall) goja.Value {
    // 替代 js 中的 TextDecoder,在 golang 中,string 默认为 utf8 格式。直接 string() 强制转换即可实现 bytes -> utf8 string
    buf := call.Argument(0).Export().(goja.ArrayBuffer)
    return vm.ToValue(string(buf.Bytes()))
}))

于是,js 中直接调用 utf8Decode 就可以解码了。

把原来代码中 I() 的实现替换成 utf8Decode

因为有外部函数,直接放到 goja 里跑

完美解析。

再稍微封装和完善一下整个 Decode 过程。

js 代码放在外部文件 decode.js 中,用 embed 引进 golang

由于 goja.Runtime 是非并发安全的,但是每次解析都 New 一个太浪费时间和内存了,就用 sync.Pool 实现 Runtime 的复用。

完整代码(把 jsdecodePacket 改成了 decode):

其实最后代码很少,但是实现的过程其实走了很多弯路,单单模拟实现就耗费了许多时间,结果最后还没用上。后来的 js 解决错误又花了很多时间(不太熟悉 js),最后才想到用 golang 解决 utf8 问题。不过最后解析成功还是很有成就感的,

Benchmark

func BenchmarkDecode(b *testing.B) {
    data, _ := hex.DecodeString("000001010012000100030000000000000000000000eb0625313634323530343138343937375f3133323231343738345f392e3231392e31312e32335f31100129000106ae023072d314161c5f722d4c43376978484142686333464f3264554641507a624141426826096979656172313030393609363636e59388e59388430000017e6ce0e1a65c6c780009060a6c6f74746572795f6964160006026c7616013006056c766e6577160130060973686f775f74797065160130060762775f7765617216027b7d060863616e5f636f70791601310604636f7079160130060963616e5f7265706c7916013106026d641600811000380c4261e69ff85261e6a002690c7c")
    for i := 0; i < b.N; i++ {
        if _, err := Decode(data); err != nil {
            b.Error(err)
            return
        }
    }
}
goos: windows
goarch: amd64
cpu: AMD Ryzen 7 5800H with Radeon Graphics         
BenchmarkDecode
BenchmarkDecode-16            1275        826234 ns/op
PASS

速度还是不错的,相对于网络传输已经可以忽略不计了,基本上解析都可以在 1ms 内完成。

完整代码等我提交到 https://github.com/iyear/pure-live-core 即可查看。

越发地对 goja 感兴趣了,有空观摩一下源码,说不定自己也能搞个小脚本语言地引擎~。

最后修改:2022 年 04 月 11 日 02 : 25 PM