WebSocket 协议的帧结构和 Masking (掩码) 机制是什么?如何对其进行流量解密和数据包篡改?

各位老铁,早上好!今天咱就来聊聊 WebSocket 协议里那些弯弯绕绕的东西,尤其是它的帧结构和 Masking (掩码) 机制。我会像唠嗑一样,把这个听起来高大上的东西给你们扒个精光,让你们不仅能看懂,还能动手玩起来,甚至能搞点“小破坏”(当然,仅限于学习研究啊!)。

WebSocket 帧结构:拆解“快递包裹”

WebSocket 协议就像一个高效的快递系统,它把数据分成一个个“包裹”(帧)来传输。每个“包裹”都有自己的格式,我们得先了解这些格式,才能知道里面装的是啥。

WebSocket 帧的基本结构如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Masking-key (if MASK set)                         |
+-------------------------------------------------------+
|    Payload Data                                           |
+-------------------------------------------------------+

别慌,一个个来解释:

  • FIN (1 bit)
    • 1:这是消息的最后一帧。
    • 0:这不是消息的最后一帧。
  • RSV1, RSV2, RSV3 (各 1 bit):保留位,通常为 0。除非你用了 WebSocket 扩展。
  • Opcode (4 bits):数据类型。
    • 0x0:延续帧
    • 0x1:文本帧 (UTF-8 编码)
    • 0x2:二进制帧
    • 0x8:连接关闭
    • 0x9:Ping
    • 0xA:Pong
  • MASK (1 bit)
    • 1:Payload 数据被 Masking-key 掩码了。
    • 0:Payload 数据没有被掩码。客户端发送给服务端的数据必须掩码。
  • Payload len (7 bits):Payload 数据的长度。
    • 0-125:Payload 数据的实际长度。
    • 126:Payload 数据的实际长度用后面的 16 bits 表示。
    • 127:Payload 数据的实际长度用后面的 64 bits 表示。
  • Extended payload length (16/64 bits):如果 Payload len 是 126 或 127,这里表示实际的 Payload 长度。
  • Masking-key (32 bits):掩码密钥,用于解码 Payload 数据。只有当 MASK 位为 1 时才存在。
  • Payload data:实际的数据。

用个表格更清晰:

字段 大小 (bits) 描述
FIN 1 标志是否是消息的最后一帧
RSV1, RSV2, RSV3 各 1 保留位
Opcode 4 数据类型 (文本、二进制、关闭连接等)
MASK 1 指示Payload数据是否被掩码
Payload len 7 Payload 数据的长度 (或指示使用扩展长度)
Extended payload length 16/64 当 Payload len 为 126 或 127 时,实际的 Payload 长度
Masking-key 32 掩码密钥,用于解码 Payload 数据
Payload data 变长 实际的 Payload 数据

Masking (掩码) 机制:给数据穿上“隐身衣”

Masking 机制是 WebSocket 协议为了安全而设计的一个重要特性。它主要用于客户端发送给服务端的数据。简单来说,就是把 Payload 数据用一个密钥进行异或操作,让数据看起来像乱码,防止中间人直接读取。

Masking 原理

  1. 生成 Masking-key:客户端会随机生成一个 32 位的 Masking-key。
  2. 异或操作:Payload 数据中的每个字节都与 Masking-key 进行异或操作。公式如下:

    masked_data[i] = original_data[i] XOR masking_key[i % 4]

    • masked_data[i]:掩码后的数据。
    • original_data[i]:原始数据。
    • masking_key[i % 4]:Masking-key 中的第 i % 4 个字节。因为 Masking-key 是 4 字节,所以要循环使用。

代码演示 (Python)

import os

def mask_data(data, masking_key):
  """
  使用 Masking-key 对数据进行掩码。

  Args:
    data: 要掩码的数据 (bytes)。
    masking_key: 4 字节的 Masking-key (bytes)。

  Returns:
    掩码后的数据 (bytes)。
  """
  masked_data = bytearray()
  for i, byte in enumerate(data):
    masked_byte = byte ^ masking_key[i % 4]
    masked_data.append(masked_byte)
  return bytes(masked_data)

def unmask_data(masked_data, masking_key):
    """
    使用 Masking-key 对已掩码的数据进行解掩码。

    Args:
        masked_data: 已掩码的数据 (bytes)。
        masking_key: 4 字节的 Masking-key (bytes)。

    Returns:
        解掩码后的数据 (bytes)。
    """
    return mask_data(masked_data, masking_key) # 解掩码和掩码操作相同,因为异或的特性

# 示例
original_data = b"Hello, WebSocket!"
masking_key = os.urandom(4)  # 生成随机的 Masking-key

masked_data = mask_data(original_data, masking_key)
print(f"Original Data: {original_data}")
print(f"Masking Key: {masking_key.hex()}")
print(f"Masked Data: {masked_data.hex()}")

unmasked_data = unmask_data(masked_data, masking_key)
print(f"Unmasked Data: {unmasked_data}")

assert original_data == unmasked_data

流量解密和数据包篡改:搞事情的正确姿势

现在我们知道了 WebSocket 帧的结构和 Masking 机制,就可以开始“搞事情”了。当然,这里的“搞事情”指的是学习研究,千万不要用于非法用途!

1. 流量解密

要解密 WebSocket 流量,我们需要:

  • 捕获 WebSocket 数据包:可以用 Wireshark、tcpdump 等工具。
  • 提取帧头信息:分析帧头,获取 Masking 位和 Masking-key。
  • 解掩码 Payload 数据:如果 Masking 位为 1,就用 Masking-key 对 Payload 数据进行异或操作。

代码演示 (Python)

import struct

def parse_websocket_frame(data):
  """
  解析 WebSocket 帧。

  Args:
    data: WebSocket 帧数据 (bytes)。

  Returns:
    一个字典,包含帧头信息和 Payload 数据。
  """
  fin = (data[0] >> 7) & 0x01
  opcode = data[0] & 0x0F
  mask = (data[1] >> 7) & 0x01
  payload_len = data[1] & 0x7F

  offset = 2
  if payload_len == 126:
    payload_len = struct.unpack("!H", data[2:4])[0]
    offset = 4
  elif payload_len == 127:
    payload_len = struct.unpack("!Q", data[2:10])[0]
    offset = 10

  masking_key = None
  if mask:
    masking_key = data[offset:offset+4]
    offset += 4

  payload_data = data[offset:offset+payload_len]

  if mask:
    payload_data = unmask_data(payload_data, masking_key)

  return {
      "fin": fin,
      "opcode": opcode,
      "mask": mask,
      "payload_len": payload_len,
      "masking_key": masking_key,
      "payload_data": payload_data
  }

# 示例 (假设我们捕获到了一个 WebSocket 帧)
frame_data = bytes.fromhex("81b948b3647e68b233336131633039336161306237653862313566333136313236343635363136353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330") # 示例数据
parsed_frame = parse_websocket_frame(frame_data)

print(f"FIN: {parsed_frame['fin']}")
print(f"Opcode: {parsed_frame['opcode']}")
print(f"Mask: {parsed_frame['mask']}")
print(f"Payload Length: {parsed_frame['payload_len']}")
print(f"Masking Key: {parsed_frame['masking_key'].hex() if parsed_frame['masking_key'] else None}")
print(f"Payload Data: {parsed_frame['payload_data'].decode()}")

这个例子里,parse_websocket_frame 函数会解析帧头,判断是否需要解掩码,然后返回解密后的 Payload 数据。

2. 数据包篡改

知道了怎么解密,篡改数据就简单多了。流程如下:

  1. 捕获 WebSocket 数据包:同上。
  2. 解密 Payload 数据:同上。
  3. 修改 Payload 数据:把你想改的内容替换进去。
  4. 重新掩码 Payload 数据 (如果需要):如果原始数据是掩码的,修改后也要重新掩码。
  5. 重新计算 Payload 长度:因为 Payload 数据长度可能变了。
  6. 发送篡改后的数据包:把修改后的数据包发出去。

代码演示 (Python)

def modify_websocket_frame(frame_data, new_payload):
    """
    修改 WebSocket 帧的 Payload 数据。

    Args:
        frame_data: 原始 WebSocket 帧数据 (bytes)。
        new_payload: 要替换的新 Payload 数据 (bytes)。

    Returns:
        篡改后的 WebSocket 帧数据 (bytes)。
    """
    parsed_frame = parse_websocket_frame(frame_data)

    # 重新掩码 Payload 数据 (如果需要)
    if parsed_frame["mask"]:
        new_payload = mask_data(new_payload, parsed_frame["masking_key"])

    # 重新计算 Payload 长度
    new_payload_len = len(new_payload)

    # 构造新的帧头
    first_byte = (parsed_frame["fin"] << 7) | parsed_frame["opcode"]
    second_byte = (parsed_frame["mask"] << 7)

    if new_payload_len <= 125:
        second_byte |= new_payload_len
        header = bytes([first_byte, second_byte])
    elif new_payload_len <= 65535:
        second_byte |= 126
        header = bytes([first_byte, second_byte]) + struct.pack("!H", new_payload_len)
    else:
        second_byte |= 127
        header = bytes([first_byte, second_byte]) + struct.pack("!Q", new_payload_len)

    # 添加 Masking-key (如果需要)
    if parsed_frame["mask"]:
        header += parsed_frame["masking_key"]

    # 拼接新的帧数据
    modified_frame_data = header + new_payload

    return modified_frame_data

# 示例
original_frame_data = bytes.fromhex("81b948b3647e68b2333361316330393361613062376538623135663331363132363436353631363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330363536353631333136363635333033303635363536313331363636353330333036353635363133313636363533303330")

new_payload = b"Modified Payload!"
modified_frame_data = modify_websocket_frame(original_frame_data, new_payload)

print(f"Original Frame Data: {original_frame_data.hex()}")
print(f"Modified Frame Data: {modified_frame_data.hex()}")

# (发送 modified_frame_data 到服务器)

在这个例子中,modify_websocket_frame 函数会先解析原始帧,然后用新的 Payload 数据替换原来的,并重新计算帧头,最后返回修改后的帧数据。

一些注意事项

  • 长度限制:WebSocket 协议对消息长度有限制,不要发送太大的数据。
  • 编码问题:文本帧必须是 UTF-8 编码,否则可能会出错。
  • 扩展:WebSocket 协议支持扩展,有些扩展会修改帧结构,需要特殊处理。
  • 安全性:篡改数据包可能会导致安全问题,请务必在授权的情况下进行。

总结

WebSocket 的帧结构和 Masking 机制是协议的核心部分。理解了这些,你就可以像一个“黑客”一样,深入了解 WebSocket 的工作原理,甚至可以进行一些“小破坏”(当然,仅限于学习研究)。希望今天的分享对你有帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注