前几天有位老哥找我说要做pdd弹幕获取,他说pdd弹幕有加密,我就看了一下,发现并没有加密,相对来说,挺简单的,在这里发出分析记录
首先介绍一下现代直播间弹幕的实现原理
通信方面,普遍是websocket,也有http轮询的,但是由于效率低开销大所以非常少
数据方面
普遍是protobuf
其次是数据头+(protobuf/jcestream)+数据AES加密/数据gzip压缩/数据lz4压缩
然后便是 自定义结构包装json(如bilibili)
最冷门的就是极为抽象但是又节省流量又方便解析的自定义文本结构(如斗yu)
所以,我们对直播间网页分析的时候,先看看是不是http轮询,不是那就监听ws数据,然后把hex转文本,如果可以看到明文部分,那恭喜,没有加密
如果不能看到明文部分,那么检查是否存在gzip压缩头/lz4压缩头,存在那就是压缩了,不存在那就是加密了...
总之,原理就那些,分析的时候带着原理去分析,就能一点点解开
接下来就讲讲pdd实战,首先通过浏览器打开pdd直播间分享链接,查看网络请求,发现很明显不是http轮询,那就要考虑是websocket了,打开性能窗口并录制
找函数调用,一个一个排查,首先看这里有对函数i的调用,i内调用了f,我们点进f看看f的代码
这里有一个decode的调用,并且输出了格式化数据l,很明显,这里存在对ws数据的解析处理,我们在decode下一个断点,看看数据
可以看到输入了ws的buffer数据,在内存检查器内可以看到具体数据
结合下面的返回数据格式,显然,收到ws数据后,执行了:
[JavaScript] 纯文本查看 复制代码
let magic = bytes.readInt16BE(0);
let cmd = bytes.readInt16BE(2);
let ctx = bytes.readInt32BE(4);
let reserve = bytes.readInt32BE(8);
let bodyLen = bytes.readInt32BE(12);
let data = bytes.subarray(16, 16 + bodyLen);
data 就是 decode函数处理之前的payload
现在我们跳到decode函数,看看它的实现,如图:
粗略一看就可以发现,skipType很显眼,再观察一下收到的数据,这显然是一个protobuf数据的解析过程,我们下几个断点,看看执行过程
发现t定义了一个protobuf节点表,而这个函数循环解析,取出数据内的所有节点(字段)
根据这个节点表,我们可以手搓下表层的proto
好了,这样表层就分析结束了,使用此.proto对data进行解析,就实现了第一个decode函数
我们继续往下执行,发现下面有一个解压缩的判断:
当compress属性==1时,对第一个函数的返回结果l的body属性解gzip
但到这里我们还没拿到弹幕数据,因为body也是个proto
那么网页必然在其他地方对payload.body进行了解析,所以我们搜索:payload.body
找到它后,果然发现了另一个decode函数,而且也是返回格式化数据,那必然也是个proto解析函数了
下个断点跟进函数
果然,和第一个decode函数大差不差,那么下个断点,看看t,应该也是个proto节点列表
果然,那么再次根据列表搓proto
实现body的定义
继续往下执行
可以看到,经过第二个函数的反序列化后,string类型的payload已经是我们需要的弹幕数据了
说是弹幕数据,实际上,这是个json,包括了弹幕等其他事件
那么pdd的ws数据结构就完全透明了,nodejs代码实现如下:
[JavaScript] 纯文本查看 复制代码
async decodemessage_pdd(bytes) { //逆向 react_live_room_xxx.js s.decode = function(e, r)
if (bytes.length <= 16) return;
try {
let magic = bytes.readInt16BE(0);
let cmd = bytes.readInt16BE(2);
let ctx = bytes.readInt32BE(4);
let reserve = bytes.readInt32BE(8);
let bodyLen = bytes.readInt32BE(12);
let data = bytes.subarray(16, 16 + bodyLen);
let result = pddCommand.decode(data);
data = null;
if (!result.body) return;
if (result.compress === 1) {
if (result.body && result.body.length > 0) result.body = await new Promise((resolve) => {
zlib.gunzip(result.body, (err, rr) => { if (err) { resolve(false); } else { resolve(rr); } });
});
if (result.extension && result.extension.length > 0) result.extension = await new Promise((resolve) => {
zlib.gunzip(result.extension, (err, rr) => { if (err) { resolve(false); } else { resolve(rr); } });
});
}
if (!result.body || !result.body.length) return;
result = pddBody.decode(result.body);
if (result.payload) {
result = JSON.parse(result.payload); //原始数据
}
result = null;
} catch (err) { }
return false;
}
这是不能直接运行的,得自己写proto文件,利用protobufjs引入proto,再调用decode
proto文件,你们可以照着我的抄,或者回复拿我写好的现成的.proto
需要注意的是,我这个proto仅支持直播间的事件,实际上pdd网页还有一些其他东西也是ws,所以要加try
本主题是记录下分析过程,不建议真的拿来使用,因为pdd直播间网页是有风控的,比如登录频繁你就看不见直播间了,以及其他的乱七八糟的风控
其他的直播平台其实原理都大差不差,包括Tao宝、zfb、京东等,都是同一个思路
syntax = "proto3";
message Command {
string command = 1;
uint32 protocol = 2;
uint32 errorCode = 3;
uint32 bizCode = 4;
string bizErrorMsg = 5;
uint32 compress = 6;
bytes extension = 9;
bytes body = 10;
uint64 downstreamSeq = 11;
uint64 conId = 12;
uint64 ctxId = 13;
}
message Body {
uint32 bizType = 1;
string groupId = 2;
string msgId = 3;
string payload = 4;
bool needAck = 5;
}