声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在评论区联系作者立即删除!
前言
本文章讲解一下网页版直播间的数据逆向,关键逆向点在 signature
参数、Message.payload
、ack
应答,通过补环境的方式解决这几个问题。
一、加密参数
1.1 抓包分析
抓包数据,定位到数据请求,是一个 websocket
连接,wss://webcast5-ws-web-hl.douyin.com/webcast/im/push/v2/?...
,后面有一大堆参数,只需要重点关注 room_id
和 signature
即可,这两个参数会动态变化,其他可先固定写死。
1.2 room_id
抓包数据中全局搜索 room_id
的值,定位到数据在 https://live.douyin.com/主播 ID
请求返回的 html
中。
请求没有特殊加密,直接发起请求,通过正则提取 html
中的 roomId
的值即可。
需要注意的是,请求返回了 set-cookie
,要保存 ttwid
作为后面发起 websocket
的 cookie
。
room_id
提取和 ttwid
保存代码如下。
1.3 signature
从请求调用栈定位到 websocket
发起,然后跟堆栈定位到 signature
的设置,再倒推到 signature
生成位置。
定位到
_getSocketParams
是最终 signature
的设置入口,单步调试,继续跟栈。
最终确定 signature
生成方法是 r.frontierSign
,对传入的参数 {"X-MS-STUB": a}
进行加密,得到最终的值。
1.3.1 a 参数
观察 a
的值 'b8c42842e8a17992f007bf4e0043e5e8'
,猜测是 md5
加密,分析代码,找到加密函数 x()
。
注意:需要先判断一下 md5
函数有没有被魔改,如果有,则需要进一步调试,还原算法。
加密 "123456"
字符串,判断是否和标准加密返回密文一致,如上图,经过测试,发现加密函数未作修改,直接通过标准的 md5
处理即可。
分析代码发现加密的内容需要room_id
,使用上面提取的动态 room_id
加密。
1.3.2 signature 加密
进入 webmssdk.es5.js
验证,确定 _0x5c2014
是我们要找的 signature
加密算法。
找到加密算法,剩下的就是让本地可以调用 _0x5c2014
实现动态生成 signature
了。
1.3.3 补环境
本地新建文件 signature.js
,复制 webmssdk.es5.js
内容到文件中,导出目标函数 _0x5c2014
,接下来就可以补环境啦~
验证本地生成 signature
流程。
二、直播间请求
2.1 PyExecJS
主要的请求流程通过 Python
实现,通过 PyExecJS
实现在 python
中调用上面实现的获取 signature
方法。
2.2 Websocket
构造 websocket
请求,主要实现 on_open、on_message、on_close、on_error
这几个基本的方法。在抖音直播间逆向过程中,关注 on_message
即可,每当服务器推送数据,就回进入到 on_message
,在方法中可以针对服务器传输过来的数据进行解析,处理。
测试代码,结果如下。
三、消息解析
返回来的数据是 protobuf
序列化的内容,那么需要我们找到传输数据的 proto
定义格式,去对内容进行反序列化,得到明文。
3.1 解析定位
服务端传输的数据进入到 on_message
处理中,跟进这个 e
的处理。
跟到 _receiveMessage
方法,找到 decode
关键字,继续跟进。
_decodeFrameOrResponse
跟进。
进入到 _decode
,通过 this.getType(t)
判断消息类型,拿到对应的解析方法 n
,通过 n.decode
对数据进行反序列化,得到明文数据。
可以看到最外层 PushFrame
的 proto
定义。
根据 PushFrame
结构,再解开嵌套字段的定义,至此最外层结构解析已完成。
3.2 protobuf
3.2.1 proto 定义
创建 danmu.proto
,写入 PushFrame
定义。
3.2.2 protoc
-
安装下载
protobuf
验证protobuf
下载是否成功
验证protoc
命令是否有效
-
生成
pb2
文件
执行命令,会得到一个danmu_pb2.py
文件。
-
解析消息
3.3 Response
按照上面的步骤,解析完消息的第一层 PushFrame
,发现 paylaod
依旧是序列化后的内容,那么继续找到 paylaod
的 proto
定义。
3.3.1 解压缩
回到 _decodeFrameOrResponse
,t
是解开的 PushFrame
数据,然后进行 n = yield this._extractResponse(t);
处理,跟进发现,这里对 paylaod
做了一层解压。
3.3.2 proto 定义
解压后的数据通过 _decode
执行反序列化操作,根据数据类型 Response
解析,跟进 n.decode(e)
。
得到第二层 Response
及嵌套的 Message
的 proto
定义。
3.3.3 更新 pb2
执行 protoc --python_out=. danmu.proto
生成新的 pb2.py
文件,在 Python 中调用 Response
定义解析上一层的 payload
值,重新请求解析,拿到结果。
3.4 Message
回到 let[d,p] = yield this._decodeFrameOrResponse(e)
,发现解析后的内容 d
虽然解析出了 message
结构,但还是看不到消息明文,还需要继续跟进。
3.4.1 消息体解析
回到 _receiveMessage
,我们需要解析的数据被 c
变量接收,分析代码,发现在判断消息体类型,如果是 msg
,就会对 c
进行处理,之后不再有明显对 c
进行的操作,那么可以猜测 this.emitter.emit("message", c)
对消息数据做了明文解析。
跟进 emit
方法,是调用 message
的监听事件进行处理。
跟步调试,发现最终都会走到 _decode
这个方法,通过不同的消息类型,拿到不同的解码方法,根据 n.decode
都能找到最终 Message.paylaod
的 proto
结构。
至此我们可以找到所有类型消息体的 proto
定义了,将定义补充到 danmu.proto
中重新生成 pb
文件,重新发起请求查看解析情况。
四、应答
虽然我们完成了消息的解析,但是会发现连接会过一段时间就被关闭,问题出在服务器会对客户端做应答检测。
4.1 检测位置
回到 _receiveMessage
,发现在拿到 Response
响应结果后,对 need_ack
这里做了判断,如果需要应答,会封装好数据,通过 n.send(e)
发送到服务器,这样服务器才会一直源源不断将新数据推送过来。
4.2 ack 数据结构
分析代码得知,先构建一个 ack_data
对象,结构为 {payload_type: "ack", payload: xxx, LogID: xxx}
,再通过 this.encode
编码数据,得到最后的应答数据。
4.2.1 ack_data
本地新建一个文件 ack.js
,定义生成 ack_data
的方法。ack_data
的结构数据均来自于之前解析好的 PushFrame
字段,只有 payload
需要做一下处理。
4.2.2 encode
进入到 _encode
内部,会根据不同消息类型,获取不同编码方法,最终通过 n.encode(e).finish();
实现数据的序列化。
进入到最终数据序列化的处理中,ack
消息类型主要做两个处理,看下图箭头指向。
4.2.3 webpack
跟进调试,发现上面做数据序列化的对象 t
的定义来自于 975.17c85355.js
的 5065
模块,来自于 webpack
动态加载,因此我们采用 webpack 逆向
的代码扣取方案解决,不清楚的可以查看这篇文章: 常见反爬策略整理——Webpack
将扣取的代码放到之前创建好的 ack.js
文件中,生成一个 a
对象给 encode
调用。
完整生成应答数据的流程。
4.3 保持 ws 连接
4.3.1 生成 ack_data
还是通过 PyExecJS
实现 js
调用,生成应答数据。
4.3.2 发送响应
在收到服务器数据时,提取 PushFrame
封装应答数据发送,这样就能解决服务器自动断开的问题啦~~~~
4.3.3 效果验证
大功告成🎉🎉🎉🎉🎉🎉🎉🎉
5、交流群
不会经常刷博客,有需要者可以加本人,搜索 LOVE_SELF_AD_LIFE,进逆向群聊,一起探讨技术