各位老铁,早上好!今天咱就来聊聊 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 原理
- 生成 Masking-key:客户端会随机生成一个 32 位的 Masking-key。
-
异或操作: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. 数据包篡改
知道了怎么解密,篡改数据就简单多了。流程如下:
- 捕获 WebSocket 数据包:同上。
- 解密 Payload 数据:同上。
- 修改 Payload 数据:把你想改的内容替换进去。
- 重新掩码 Payload 数据 (如果需要):如果原始数据是掩码的,修改后也要重新掩码。
- 重新计算 Payload 长度:因为 Payload 数据长度可能变了。
- 发送篡改后的数据包:把修改后的数据包发出去。
代码演示 (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 的工作原理,甚至可以进行一些“小破坏”(当然,仅限于学习研究)。希望今天的分享对你有帮助!下次再见!