JS逆向中WebSocket二进制数据加解密分析思路

作者:键盘侠

时间:2026-11-02

浏览:599

系统梳理JavaScript逆向工程中面对WebSocket二进制数据(ArrayBuffer与Blob)的分析思路,从Hook定位、协议分类识别(Protobuf、压缩、对称加密、私有封包),到解密与反序列化还原的全流程实战指南。

文章目录

打开浏览器的开发者工具,切换到 Network 面板的 WS 标签页,在 Messages 列表中看到的不是明朗直观的 JSON 字符串,而是一行行标记为 Binary Message 的数据帧;点击预览区,呈现出的是一长串十六进制字节码或无法辨认的乱码字符。面对这种 WebSocket 二进制通信,很多开发者容易陷入在庞大打包代码中无头绪搜索 AES、decrypt 等关键词的误区。解决此类问题的核心思路是构建一条确切的追踪链路:拦截底层传输载体(Hook 原生 API) → 沿调用栈逆向定位分发入口 → 辨识载荷特征(数据压缩、标准序列化、算法加解密或自定义私有包头) → 剥离解码逻辑 → 独立工程化重放并维持长连接心跳。

JS逆向中WebSocket二进制数据加解密分析思路
图 0:WebSocket 二进制数据逆向分析体系总览

一、截获传输载体:底层 API 的 Hook 与断点策略

WebSocket 协议原生支持文本帧(Text Frame, Opcode 0x1)和二进制帧(Binary Frame, Opcode 0x2)。在浏览器 JavaScript 运行时中,二进制数据通常以 ArrayBuffer 或 Blob 对象形式呈现,具体取决于套接字实例的 binaryType 属性设置。要找到解密与反序列化的源头,第一步是在浏览器创建连接或收发数据时对其建立拦截。

现代前端工程通常采用 Webpack 或 Vite 模块化打包,底层的 WebSocket 连接实例多被深层封装在框架(如 Socket.IO、SockJS、自研长连 SDK)内部。最直接且通用的拦截手段是在全局作用域中代理 window.WebSocket 构造函数及其原型链方法。

在控制台或油猴脚本(Tampermonkey)的 document-start 阶段注入以下拦截脚本:

(function hookWebSocket() {
  const RealWebSocket = window.WebSocket;

  window.WebSocket = function (url, protocols) {
    const ws = protocols ? new RealWebSocket(url, protocols) : new RealWebSocket(url);

    // 1. 劫持客户端上行发送 (send)
    const originalSend = ws.send;
    ws.send = function (data) {
      if (data instanceof ArrayBuffer || ArrayBuffer.isView(data) || data instanceof Blob) {
        console.warn('[WS-Send Binary]', {
          length: data.byteLength || data.size,
          data: data
        });
        // 此处可打条件断点,分析上行序列化或加密前调用栈
        // debugger;
      }
      return originalSend.apply(this, arguments);
    };

    // 2. 劫持服务端下行推送 (addEventListener 形式)
    const originalAddEventListener = ws.addEventListener;
    ws.addEventListener = function (type, listener, options) {
      if (type === 'message') {
        const hookedListener = function (event) {
          if (typeof event.data !== 'string') {
            console.warn('[WS-Recv Binary via EventListener]', {
              type: Object.prototype.toString.call(event.data),
              length: event.data.byteLength || event.data.size,
              raw: event.data
            });
            // 收到二进制消息时触发断点
            if ((event.data.byteLength || event.data.size) > 32) {
              debugger;
            }
          }
          return listener.apply(this, arguments);
        };
        return originalAddEventListener.call(this, type, hookedListener, options);
      }
      return originalAddEventListener.apply(this, arguments);
    };

    // 3. 劫持 onmessage 属性赋值 (setter 形式)
    let _onmessage = null;
    Object.defineProperty(ws, 'onmessage', {
      get: function () {
        return _onmessage;
      },
      set: function (fn) {
        _onmessage = function (event) {
          if (typeof event.data !== 'string') {
            console.warn('[WS-Recv Binary via onmessage]', {
              type: Object.prototype.toString.call(event.data),
              length: event.data.byteLength || event.data.size,
              raw: event.data
            });
            // 过滤短小的常规心跳帧后停下执行上下文
            if ((event.data.byteLength || event.data.size) > 32) {
              debugger;
            }
          }
          return fn.apply(this, arguments);
        };
      }
    });

    return ws;
  };

  window.WebSocket.prototype = RealWebSocket.prototype;
  window.WebSocket.CONNECTING = RealWebSocket.CONNECTING;
  window.WebSocket.OPEN = RealWebSocket.OPEN;
  window.WebSocket.CLOSING = RealWebSocket.CLOSING;
  window.WebSocket.CLOSED = RealWebSocket.CLOSED;
})();
执行位置与时机要求

该脚本必须在目标网页建立 WebSocket 连接之前执行完毕。若网页在 HTML 加载早期即发起连接,建议使用浏览器扩展在页面顶部注入,或在 DevTools 的 Source 面板中勾选“Event Listener Breakpoints → WebSocket → Create”先行阻断。

JS逆向中WebSocket二进制数据加解密分析思路
图 1:WebSocket 二进制数据拦截与调用栈逆向定位步骤

二、调用栈溯源:穿透底层驱动找到业务解码入口

当断点在 Hook 函数内部触发时,JavaScript 引擎会停在最原始的事件接收处。此时 event.data 依然是原生的 ArrayBuffer 或 Blob,业务逻辑尚未对它进行任何加工。接下来需要利用 Chrome DevTools 的 Call Stack(调用堆栈) 单步步过(Step Over, F10)或单步步入(Step Into, F11)向外追踪。

在这个过程中,调用栈通常会经历三个阶段:

  1. 网络底层事件触发层:这是浏览器的原生驱动或第三方基础通信库(例如 sockjs-client、engine.io-client、event-target-shim)。这部分代码由通用开源库编译而来,不需要在此浪费时间,直接观察函数返回。
  2. 二进制载体适配层:如果服务端的 binaryType 是 blob,前端通常需要调用 FileReader.readAsArrayBuffer() 或 blob.arrayBuffer() 将其转换为内存字节数组。在这一层,数据类型完成了从文件对象到内存缓冲区的跨越。
  3. 业务消息分发与解码层:这是逆向分析的真正重点。代码中通常会出现类似 dispatch(type, buffer)、handleMessage(data)、decodePacket(bytes) 的函数名。在混淆代码中,也可以通过局部变量中出现 new Uint8Array(...) 或 new DataView(...) 识别出进入了业务解包模块。

三、二进制载荷形态识别:四种典型模式与辨识决策

许多初学者看到二进制数组便直觉认定是强加密算法,随后费尽周折寻找密钥,最终却发现那只是常规的压缩或序列化。在溯源到具体的解码函数后,应先提取原始二进制数据的前 16 个字节及整体字节分布,对照以下四种形态进行分类研判:

JS逆向中WebSocket二进制数据加解密分析思路
图 2:WebSocket 二进制载荷形态识别与研判决策树

1. 形态 A:纯数据压缩(Gzip / Zlib / Brotli)

为节省高频行情的带宽,很多金融、社交及弹幕类网站直接将明文 JSON 进行压缩后下发。只需检查数据前两字节的十六进制魔数即可辨别:

  • Gzip 魔数:0x1F 0x8B。
  • Zlib(Deflate)魔数:0x78 0x01(无压缩/弱压缩)、0x78 0x9C(默认压缩等级)、0x78 0xDA(最高压缩等级)。

在前端代码中搜索 pako、inflate、fflate 等关键字。如果在控制台拿到 uint8Array,可直接调用引入的 pako.inflate(uint8Array, { to: 'string' }) 验证;若立即打印出可读的 JSON 文本,则说明该接口并未对数据加解密,仅做了传输压缩。

2. 形态 B:标准化二进制序列化(Protobuf / MessagePack)

Protocol Buffers(Protobuf)是现代 WebSocket 架构中使用最广泛的序列化方案。Protobuf 编码后的数据密度极高,且没有统一固定的文件头魔数,但在客户端代码中有非常显著的静态与动态特征:

  • 静态代码特征:混淆文件中包含 protobuf.Reader.create、.decode(reader)、finish(),或在循环结构中执行 tag >>> 3(提取字段序号)与 tag & 7(提取传输类型 WireType)。
  • 运行时特征:断点停在解码函数内部时,入参是一个包含 pos、len、buf 属性的 Reader 实例,代码分支充斥着大量的 switch (tag >>> 3) 语句。

若使用的是 MessagePack,其首字节通常落在特定范围(例如 0x80~0x8F 表示元素较少的 Map,0x90~0x9F 表示数组),在 JS 依赖中多见 msgpack-lite 或 @msgpack/msgpack。

3. 形态 C:对称/非对称加解密(AES / ChaCha20 / 国密 SM4 / 异或)

真正的加密载荷具有典型的高信息熵特征:Shannon 熵值通常接近理论极限 8.0,字节取值在 0~255 之间均匀分布,没有任何可识别的文本片段或固定魔数。常见的分析路径如下:

  • Web Crypto API 原生调用:检索 crypto.subtle.decrypt,断点查看传入的 CryptoKey、iv 及算法名称(如 AES-GCM、AES-CBC)。
  • 第三方加密库:检索 CryptoJS.AES.decrypt、forge.cipher 或自定义的位运算还原函数。
  • 密钥协商与分发:解密算法本身的逻辑通常是固定的标准库,重点在于寻找密钥(Key)的来源。密钥可能是连接握手(HTTP Upgrade 请求)时下发的 Token 计算得出,也可能是客户端在建立 WS 连接前通过 RSA/ECDH 与服务端协商所得。

4. 形态 D:自定义私有二进制封包(TLV / Header + Payload)

大型系统或重度依赖私有通信协议的应用,往往会设计专用的二进制包头(Frame Header),用来承载内部路由、消息编号以及压缩/加密标志。这是逆向分析中必须攻克的结构层。

四、核心实战:自定义私有协议与 DataView 内存映射

在面对自定义二进制封包时,最常调用的 JavaScript 内置 API 是 DataView。与直接访问索引的 Uint8Array 不同,DataView 提供了精确控制字节偏移量和字节序(Endianness)的读写能力。

JS逆向中WebSocket二进制数据加解密分析思路
图 3:典型自定义二进制协议封包结构与 DataView 内存偏移映射

以下是一段在客户端逆向中提取出来的典型解包逻辑实现:

// 客户端解包函数逆向还原示例
function unpackWebSocketFrame(arrayBuffer) {
  // 1. 基于原始 Buffer 建立视图
  const view = new DataView(arrayBuffer);
  const totalLength = arrayBuffer.byteLength;

  // 包头定长校验 (例如本协议包头占 13 字节)
  if (totalLength < 13) {
    throw new Error('Frame length is shorter than header size');
  }

  // 2. 依次按字节偏移读取 Header 字段
  // 第二个参数 false 表示采用大端序 (Big-Endian / 网络字节序)
  const magic = view.getUint16(0, false);
  if (magic !== 0xAA55) {
    throw new Error('Invalid Protocol Magic: 0x' + magic.toString(16));
  }

  const cmdId = view.getUint16(2, false);
  const seqId = view.getUint32(4, false);
  const bodyLen = view.getUint32(8, false);
  const flags = view.getUint8(12);

  // 3. 校验变长包体长度
  if (13 + bodyLen > totalLength) {
    throw new Error('Incomplete body payload: packet fragmented');
  }

  // 4. 提取纯净 Payload 字节切片
  const rawPayload = arrayBuffer.slice(13, 13 + bodyLen);

  // 5. 根据 Flags 标志位处理解密与解压缩
  let payloadBuffer = rawPayload;
  const isEncrypted = (flags & 0x01) === 0x01;
  const isCompressed = (flags & 0x02) === 0x02;

  if (isEncrypted) {
    // 传入密钥进行 AES 解密
    payloadBuffer = customAesDecrypt(payloadBuffer);
  }

  if (isCompressed) {
    // 调用 pako 进行解压缩
    payloadBuffer = window.pako.inflate(new Uint8Array(payloadBuffer)).buffer;
  }

  return {
    header: { magic, cmdId, seqId, bodyLen, flags },
    payload: payloadBuffer
  };
}
必须警惕的端序陷阱(Endianness)

DataView.prototype.getUint32(byteOffset, littleEndian) 的第二个参数决定了多字节整数的解析方式。若省略或传 false,则默认为大端序(Big-Endian);传 true 则为小端序(Little-Endian)。如果端序识别错误,一个原本为 100(0x00000064)的包体长度会被误读为 1677721600(0x64000000),直接导致内存切片异常崩溃。

五、Protobuf 协议还原:从前端代码反推数据模型

当判定二进制载荷为 Protobuf 编码时,核心目标不是强行写底层位运算解码器,而是获取或推导对应的 .proto 数据契约定义:

  • 方法一:在控制台中直接导出打包器中的 Proto 模块
    大多数使用 Webpack 打包的项目会将 Protobuf 编译生成的 JavaScript 类统一收归在某个模块中。在断点停留在解码函数处时,可以在控制台打印当前作用域,通常能直接发现包含 Root、lookupType 或 Type.decode 的命名空间。通过 window.__proto_root = rootModule; 将其暴露到全局,后续在控制台即可直接调用 __proto_root.lookupType('TradeDetail').decode(buf) 输出可读对象。
  • 方法二:根据解码 Switch 分支与 Tag 反向拼写 .proto 文件
    如果前端对代码进行了深度混淆压制,剥离了反射元数据,可以观察反序列化函数中的循环与条件分支。例如:
    // 混淆代码片段
    while (reader.pos < end) {
      var tag = reader.uint32();
      switch (tag >>> 3) {
        case 1: message.symbol = reader.string(); break;
        case 2: message.price = reader.double(); break;
        case 3: message.volume = reader.int64(); break;
        default: reader.skipType(tag & 7); break;
      }
    }
    根据字段编号(Field Number)与读取方法(WireType 映射),即可直接还原出纯净的标准 proto 声明:
    syntax = "proto3";
    
    message TradeDetail {
      string symbol = 1;
      double price = 2;
      int64 volume = 3;
    }
    有了这个 .proto 文件,在脱机脚本中便可以使用 Python 的 google.protobuf 或 Node.js 的 protobufjs 直接编解码。

六、脱机独立重放与工程化落地

在浏览器内搞清解密与解码逻辑后,最终需要脱离浏览器,在独立脚本(如 Python 或 Node.js)中完整跑通数据拉取。此时需要满足长连接建立与维持的三大基本条件:

关键环节 实现要点与常见问题 工程处理手段
1. 握手鉴权 (Handshake) HTTP 101 Upgrade 请求中附带鉴权 Cookie、Token、以及根据时间戳计算的签名参数。 抓取初次发起握手请求的完整 Headers,复现 HTTP 握手阶段的加密签名生成逻辑。
2. 协议初始化与订阅 连接建立后必须在 3~5 秒内发送指定格式的 Auth/Login 二进制包或 Topic 订阅包。 按照逆向出的私有 Header 格式或 Protobuf 编码结构构造登录二进制帧并首批发送。
3. 心跳守护机制 (Heartbeat) 服务端通常设置有空闲超时(通常为 30 秒至 60 秒),若无定周期上行数据则强制断开(Code 1006)。 在客户端维护一个后台定时器(如每隔 15 秒),组装心跳包(如固定 CmdID 为 0 的二进制帧)发往服务端。
JS逆向中WebSocket二进制数据加解密分析思路
图 4:WebSocket 二进制逆向分析与工程化还原全景方法论

七、常见分析雷区与应对总结

在实际逆向分析过程中,还有若干容易导致卡顿的非典型场景,需要特别注意:

  • Worker 线程中的 WebSocket 连接:部分对性能要求较高的站点将 WebSocket 通信完全移入 Web Worker 或 SharedWorker 中执行。此时在主线程 Hook window.WebSocket 无法捕获任何连接。应对方法是 Hook 全局 window.Worker 构造函数,或在 Chrome DevTools 的 Threads 窗口中切换到对应的 Dedicated Worker 上下文再打断点。
  • 动态会话密钥混淆:若发现每次刷新页面后解密算法相同但密钥完全改变,应重点排查 WS 建立连接后的第一帧二进制数据。服务端经常通过首帧(如 Init Handshake Frame)将当前连接的临时公钥或 AES IV 下发给客户端。
  • 黏包与分片(Framing):在极少数吞吐量巨大的数据流中,一个 WebSocket 数据帧内可能包含多个连续的业务数据包。若仅按照第一包解析剩余字节会全部报错,必须在解包函数中根据 Header 中的 bodyLen 进行循环切片读取,直到耗尽当前 ArrayBuffer。

掌握 WebSocket 二进制加解密的核心,在于清晰地分离网络传输层、私有封包层、安全加密层与数据序列化层。面对杂乱的十六进制数据,始终遵循自底向上的调用栈排查法则,先判定数据形态,再针对性攻克解密与反序列化逻辑,即可高效还原出清晰完整的结构化业务数据。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章