百趣云 百趣云的博客

WebSocket 协议模拟:心跳、重连与消息伪装

WebSocket 逆向和 HTTP 有什么不同

HTTP 协议模拟是"一次性请求"的复制,WebSocket 是长连接状态机的复刻:握手、鉴权、心跳、消息编解码、重连,每个环节都有坑。抓包工具对 WS 的支持也弱于 HTTP(Charles 能看但不直观),先建立正确的分析姿势。

分析姿势:先看懂消息格式

Chrome DevTools 的 Network → WS → Messages 标签页能看到所有帧。重点观察:

  1. 消息格式:JSON?protobuf?自定义二进制?(JSON 最好办,protobuf 按前文方法还原,自定义二进制最麻烦)
  2. 消息类型字段:一般有个 type/cmd/op 字段区分消息种类(心跳、业务、推送)
  3. 鉴权时机:握手 URL 带 token?还是连接后第一条消息鉴权?

模拟客户端的骨架

import asyncio, websockets, json

class WSClient:
    def __init__(self, url, token):
        self.url = url
        self.token = token
        self.seq = 0

    async def connect(self):
        async with websockets.connect(
            self.url,
            extra_headers={'User-Agent': '...', 'Origin': 'https://target.com'},
            ping_interval=None,   # 关掉库的自动心跳,自己控制
        ) as ws:
            await self.auth(ws)
            await asyncio.gather(
                self.heartbeat_loop(ws),
                self.message_loop(ws),
            )

    async def auth(self, ws):
        # 连接后首条消息鉴权(格式从抓包还原)
        await ws.send(json.dumps({
            'type': 'auth', 'token': self.token, 'seq': self.next_seq()
        }))

    async def heartbeat_loop(self, ws):
        while True:
            await asyncio.sleep(30)   # 心跳间隔按抓包观察值
            await ws.send(json.dumps({'type': 'ping', 'seq': self.next_seq()}))

    async def message_loop(self, ws):
        async for msg in ws:
            data = json.loads(msg)
            await self.handle(data)

精髓一:心跳的魔鬼细节

心跳是 WS 模拟最容易露馅的地方:

  1. 间隔要随机化。真实客户端的心跳间隔是"30 秒 ± 网络抖动",你每 30.000 秒准点发,统计学上就是机器人。sleep(30 + random.uniform(-2, 2))
  2. 心跳内容有序列号。很多协议的心跳带递增 seq,断连重连后 seq 的处理规则(重置还是继续)要按原协议来
  3. 服务端心跳超时。服务端一般 2-3 倍心跳间隔没收到就断连。你的心跳线程必须独立且可靠——主循环卡住时心跳不能停

精髓二:重连状态机

真实客户端的重连逻辑是协议的一部分:

async def run(self):
    retry = 0
    while True:
        try:
            await self.connect()
            retry = 0
        except Exception as e:
            retry += 1
            # 指数退避 + 抖动,和真实客户端行为一致
            delay = min(60, (2 ** retry)) + random.uniform(0, 3)
            await asyncio.sleep(delay)

要点:指数退避(1s、2s、4s…封顶 60s)+ 随机抖动。固定间隔重连(每 5 秒硬刚)在服务端看来是攻击特征。

精髓三:二进制协议的处理

消息是 protobuf/自定义二进制时:

# protobuf:用还原的 proto 文件编解码
msg = AuthMessage()
msg.token = self.token
await ws.send(msg.SerializeToString())

# 收消息
async for raw in ws:
    msg = ServerMessage()
    msg.ParseFromString(raw)

自定义二进制协议(无 proto)时,先用抓包样本做格式逆向:固定字段偏移、长度前缀、类型标识,逐字节对照多条消息找规律。WebSocket 帧本身有头部开销,分析时区分"WS 帧头"和"应用层协议头"。

总结

  1. WS 模拟是状态机复刻:握手、鉴权、心跳、重连缺一不可
  2. 心跳间隔随机化,序列号规则对齐原协议
  3. 重连用指数退避+抖动,别固定间隔硬刚
  4. 二进制消息先还原格式,protobuf 按字段号重建

交流微信:run1255

By 百趣云 阅读量:2 On