声明:
本人在此郑重声明,本文章中的所有内容仅供学习交流使用,已对抓包内容、敏感网址和数据接口做了脱敏处理。请勿将这些内容用于商业和非法用途,否则将承担所有法律后果。如果本文章内容侵犯了您的权益,请立即联系我,我将及时予以删除!
目标地址
某音东方甄选直播间
开始逆向分析
- 浏览器打开网页版抖音直播间,F12进入控制台
- 分析弹幕数据: 弹幕这种数据它的交互方式是这样,客户端可以发送弹幕数据,服务器端也可以推送所有人的弹幕数据(浏览器也不需要数据刷新,不需要向服务器重新发起请求),像我们通常用的http请求,只能做到客户端向服务器发送一次请求后,浏览器把相应数据给客户端; 我们可以看到抖音这里的
ws这一块,数据一直在实时变动,但是地址只有一直是一个,这里也就是我们的弹幕数据,也就是说,弹幕是在同一个websocket连接下进行数据传输的 - 这里先简单的介绍下websocket WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,它可以在客户端和服务器之间建立一种实时的双向通信机制。WebSocket 协议使得客户端和服务器之间可以持续地交换数据,而无需进行任何轮询或定时拉取。
相对于传统的 HTTP 协议,WebSocket 协议的优势在于:
实时性:WebSocket 协议建立的连接是全双工的,服务器可以主动向客户端发送数据,而客户端也可以随时向服务器发送数据,因此可以实现实时通信。
轻量级:WebSocket 协议的头部信息相对较小,因此数据传输的开销比较小。
节省带宽:由于 WebSocket 可以通过单个连接传输多个消息,因此相对于 HTTP 协议而言,可以节省带宽。
更好的跨域支持:WebSocket 协议支持跨域通信,因此可以更好地满足现代 Web 应用程序的需要。
WebSocket 协议支持多种编程语言和平台,可以用于构建实时聊天室、在线游戏、在线教育等需要实时通信的应用程序。
- 抓取弹幕的数据到本地主要的思路如下:
弹幕在浏览器显示的数据是在后台发送给浏览器的,可以看到这些数据是二进制数据,红的代表后台返回,绿色的代表浏览器请求后台;
既然后台是二进制的数据,js必须解析得到明文才能展示浏览器给用户,所以我们就需要找到解密二进制为明文的这个地方
- 寻找接收后台数据的位置
js与后台websocket通信,常规代码如下:
const socket = new WebSocket('ws://localhost:8080'); // 创建 WebSocket 实例
socket.addEventListener('open', function (event) {
socket.send('Hello Server!'); // 在连接建立后发送消息
});
socket.addEventListener('message', function (event) {
console.log('Message from server:', event.data); // 处理接收到的消息
});
socket.addEventListener('close', function (event) {
console.log('Connection closed'); // 处理连接关闭事件
});
我们可以搜关键字new WebSocket、 open, message等这里的; 我们搜索下new WebSocket,只有2处,都点进入打上断点,刷新页面 断点成功断住,看了一眼抖音这块代码,与上面的js websocket通讯模板是极其相似
class m {
constructor(e) {
const t = u(e);
"undefined" != typeof WebSocket && (this.socket = new WebSocket(t),
this.socket.binaryType = "arraybuffer")
}
onError(e) {
this.socket.addEventListener("error", e)
}
onMessage(e) {
this.socket.addEventListener("message", e)
}
onOpen(e) {
this.socket.addEventListener("open", e)
}
onClose(e) {
this.socket.addEventListener("close", e)
}
}
onMessage(e)就是接收到后台数据,然后进行解析处理,因此我们重点看这个里面的e 6. 寻找解密后台返回数据的位置 在onMessage(e)方法内打上断点,F8继续执行断点,进入onMessage中,光标放到参数e,它也是一个方法,点击进去它的实现位置 进入到
onMessage方法中,里面调用是bindClientMessage
this.client.onMessage((e=>{
this.bindClientMessage(e, i)
}
在点击进入到bindClientMessage方法中 代码如下:
bindClientMessage(e, t)
{
var i;
if (this.client) {
const o = c.PushFrame.deserializeBinary(e.data)
, a = r.Response.deserializeBinary(function (e) {
for (const t of Object.values(e.getHeadersList()))
if ("compress_type" === t.getKey() && "gzip" === t.getValue())
return !0;
return !1
}(o) ? (0,
n.ec)(o.getPayload()) : o.getPayload_asU8());
this.setHeaderListToObject(null !== (i = o.getHeadersList()) && void 0 !== i ? i : []);
if (a.getNeedAck()) {
let e = a.getInternalExt()
, t = a.getCursor();
(o.getHeadersList() || []).forEach((i => {
"im-internal_ext" === (null == i ? void 0 : i.getKey()) && (e = null == i ? void 0 : i.getValue()),
"im-cursor" === (null == i ? void 0 : i.getKey()) && (t = null == i ? void 0 : i.getValue())
}
)),
this.internalExt = e,
this.cursor = t;
const i = new c.PushFrame;
i.setPayloadType("ack"),
i.setPayload(function (e) {
const t = [];
for (const i of e) {
const e = i.charCodeAt(0);
e < 128 ? t.push(e) : e < 2048 ? (t.push(192 + (e >> 6)),
t.push(128 + (63 & e))) : e < 65536 && (t.push(224 + (e >> 12)),
t.push(128 + (e >> 6 & 63)),
t.push(128 + (63 & e)))
}
return Uint8Array.from(t)
}(e)),
i.setLogid(o.getLogid()),
this.client.socket.send(i.serializeBinary())
}
if ("msg" === o.getPayloadType() && (this.info("fetchSocketServer socket response: ", (() => a.toObject())),
this.emit(a)),
"close" === o.getPayloadType())
return t(new Error("close by payloadtype"))
}
}
}
我们看到代码的这一行,有很明显的介绍fetchSocketServer socket response: 这个中文解释就是获取到socket服务器的相应 打印
a.toObject()里面的payload就是后台返回的数据,不过是加密的,说明代码往后走就会有解析数据payload的代码; 这里的emit(a)方法断点进入看下
emit(e){
const t = e.getMessagesList();
t.length && t.forEach((e => {
const t = e.getMethod()
, a = "RoomMessage" === t ? t : e.getMsgId();
this.messageIdsForDistinct.has(a) || (this.messageIdsForDistinct.add(a),
this.runAllEvents(t, e))
}
))
}
再继续跟进去this.runAllEvents(t, e))
runAllEvents(e, t)
{
var a;
for (const [r, i] of this.eventsMap.entries()) {
const a = this.messageModules[r];
if (i && a && this.isCorrectEventName(r, e)) {
const o = t.getPayload_asU8()
, s = a.deserializeBinary(o);
return this.info(`emit Message Type: ${e} ${r}`),
this.info("emit Message Payload:", (() => s.toObject())),
void i.forEach((e => {
e(s, t, o)
}
))
}
}
const o = null !== (a = this.messageNotUseCache.get(e)) && void 0 !== a ? a : [];
o.length > r.ej && o.shift(),
o.push(t),
this.messageNotUseCache.set(e, o)
}
在s.toObject()这里就是获取到明文数据的地方
这里在多说一点:抖音返回websocket返回的数据类型有很多,可以到method="WebcastGiftMessage" 其实还有很多很多,这里我列举了一些:
'WebcastMemberMessage': 代表谁进入了直播间
'WebcastLikeMessage': 代表为主播点赞了
'WebcastRoomStatsMessage': 代表当前房间的状态人数
'WebcastChatMessage': 代表当前与主播的互动消息
'WebcastRoomMessage':
'WebcastGiftMessage': 代表谁送了礼物
'WebcastUpdateFanTicketMessage': XXX
"WebcastLiveShoppingMessage": XXX
WebcastLiveEcomGeneralMessage
WebcastProductChangeMessage
我们想要什么数据的话,可以跟进method来判断,然后在获取对应的数据
- 开始注入我们的代码:
浏览器启用本地替换,注入我们代码后,保存并替换,刷新浏览器,让我们注入的代码生效
- 启动我们本地服务器websocket服务端进行接收转发的明文数据
到这里可以看到东方甄选直播间的弹幕数据被我们劫持下来了,当然你也可以获取其他数据