HTTP/2 Frame 解析:如何从原始二进制流中提取 HEADERS, DATA, SETTINGS 等帧,并对其进行篡改?

HTTP/2 帧解析与篡改:一场二进制世界的探险

大家好,我是你们今天的导游,带大家深入HTTP/2的二进制丛林,一起探险帧(Frame)的秘密,并学习如何成为一位“帧”的艺术家,创造性地修改它们。

首先,我们得明确一点:HTTP/2 帧是HTTP/2通信的基石。所有的数据,包括请求头、响应体,甚至连接控制信息,都被封装在帧中进行传输。理解帧的结构,就等于掌握了HTTP/2的命脉。

HTTP/2 帧结构:拆开“乐高玩具”

HTTP/2 帧由以下几个关键部分组成,可以想象成一个精心设计的“乐高玩具”:

字段名称 长度 (bytes) 描述
Length 3 帧负载的长度,不包括帧头(Length 和 Type,Flags,R)。最大值为 2^24 – 1 (16,777,215)。
Type 1 帧的类型,决定了帧的含义。例如,HEADERS、DATA、SETTINGS 等。
Flags 1 帧的标志位,用于指示帧的特定属性。不同的帧类型有不同的标志位定义。
R 1 保留位,必须设置为 0。 (虽然这个字段没什么实际用处,但也不能忽略它,毕竟它也是帧结构的一部分。)
Stream Identifier 4 流ID,用于标识帧所属的流。流是HTTP/2中并发传输的基础。流ID为0的帧用于连接级别的控制,其他帧必须具有非0的流ID。
Frame Payload Length 字节 帧负载,包含了帧的具体数据。帧负载的结构和内容取决于帧的类型。

长度 (Length):
这个字段告诉我们帧负载有多长。需要注意的是,这个长度不包括帧头本身的长度(Length、Type、Flags和R)。这就像告诉你一个包裹的尺寸,但没告诉你包裹本身的包装盒有多大。

类型 (Type):
这个字段定义了帧的类型。HTTP/2 定义了多种帧类型,每种类型都有不同的用途。一些常见的帧类型包括:

  • DATA (0x00): 包含HTTP请求或响应的数据。
  • HEADERS (0x01): 包含HTTP头信息。
  • PRIORITY (0x02): 用于指定流的优先级。
  • RST_STREAM (0x03): 用于重置流。
  • SETTINGS (0x04): 用于配置HTTP/2连接参数。
  • PUSH_PROMISE (0x05): 用于服务器推送。
  • PING (0x06): 用于测量往返时间。
  • GOAWAY (0x07): 用于关闭连接。
  • WINDOW_UPDATE (0x08): 用于流量控制。
  • CONTINUATION (0x09): 用于继续发送过大的HEADERS帧。

标志 (Flags):
Flags 字段用于指示帧的特定属性。不同的帧类型有不同的标志位定义。例如,DATA 帧的 END_STREAM 标志位表示这是流的最后一个数据帧。HEADERS帧的END_HEADERS标志位表示头信息结束。

流ID (Stream Identifier):
流ID用于标识帧所属的流。HTTP/2 允许在单个连接上并发传输多个流。流ID为0的帧用于连接级别的控制,比如SETTINGS帧。其他的帧必须具有非0的流ID。

帧负载 (Frame Payload):
帧负载是帧的实际数据。帧负载的结构和内容取决于帧的类型。例如,DATA 帧的负载包含HTTP请求或响应的数据,HEADERS 帧的负载包含HTTP头信息。

解析帧:用 Python 打开二进制“黑盒子”

现在,让我们用 Python 来解析 HTTP/2 帧。我们将编写一个简单的函数,它可以从原始二进制数据中提取帧的各个字段。

import struct

def parse_frame(data):
    """
    解析 HTTP/2 帧。

    Args:
        data: 包含帧数据的字节串。

    Returns:
        一个字典,包含帧的各个字段。
    """
    if len(data) < 9:
        raise ValueError("帧数据太短,无法解析")

    length = struct.unpack("!I", b'x00' + data[0:3])[0] # Length 字段是 3 字节,需要填充一个字节才能使用 struct.unpack("!I")
    frame_type = data[3]
    flags = data[4]
    R = data[5]
    stream_id = struct.unpack("!I", data[5:9])[0] & 0x7FFFFFFF # Stream Identifier 最高位是保留位,需要清零

    payload = data[9: 9 + length]

    return {
        "length": length,
        "type": frame_type,
        "flags": flags,
        "R": R,
        "stream_id": stream_id,
        "payload": payload
    }

# 示例:解析一个简单的 DATA 帧
frame_data = b'x00x00x0ax00x00x00x00x00x01hellohttp2' #一个长度为10字节的DATA帧,Stream ID是1,数据是"hellohttp2"
parsed_frame = parse_frame(frame_data)
print(parsed_frame)

这个函数首先检查数据长度是否足够,然后使用 struct.unpack 函数从字节串中提取字段。注意 struct.unpack 函数需要指定字节序(这里使用大端字节序 !)和数据类型(这里使用无符号整数 I 和字节串 s)。 对于Length字段,因为只有三个字节,需要填充一个0字节才能使用struct.unpack("!I")进行解析。 对于Stream ID字段,由于最高位是保留位,需要使用& 0x7FFFFFFF将其清零。

这个函数返回一个字典,包含帧的各个字段。你可以使用这个字典来访问帧的各个部分。

注意事项:

  • HTTP/2 使用大端字节序。
  • Stream Identifier 的最高位是保留位,必须设置为 0。
  • Length 字段是 3 字节,需要填充一个字节才能使用 struct.unpack("!I")

篡改帧:成为“帧”的艺术家

现在我们已经可以解析 HTTP/2 帧了,接下来我们将学习如何篡改它们。篡改帧可以用于很多目的,例如:

  • 测试 HTTP/2 实现的安全性。
  • 模拟网络错误。
  • 实现自定义的 HTTP/2 扩展。

让我们从一个简单的例子开始:修改 DATA 帧中的数据。

def modify_data_frame(frame_data, new_data):
    """
    修改 DATA 帧中的数据。

    Args:
        frame_data: 原始的 DATA 帧数据。
        new_data: 要替换的新数据。

    Returns:
        修改后的 DATA 帧数据。
    """
    parsed_frame = parse_frame(frame_data)

    if parsed_frame["type"] != 0x00:
        raise ValueError("不是 DATA 帧")

    new_length = len(new_data)
    # Length 字段是 3 字节,需要使用 struct.pack("!I") 截断
    new_length_bytes = struct.pack("!I", new_length)[1:]
    new_payload = new_data.encode('utf-8')  # 确保 new_data 是字节串

    # 重新组装帧
    modified_frame = new_length_bytes + bytes([parsed_frame["type"]]) + bytes([parsed_frame["flags"]]) + struct.pack("!I", parsed_frame["stream_id"])[:1] + struct.pack("!I", parsed_frame["stream_id"])[1:] + new_payload

    return modified_frame

# 示例:修改 DATA 帧中的数据
frame_data = b'x00x00x0ax00x00x00x00x00x01hellohttp2'
new_data = "modified data"
modified_frame = modify_data_frame(frame_data, new_data)
print(modified_frame)

parsed_modified_frame = parse_frame(modified_frame)
print(parsed_modified_frame)

这个函数首先解析原始的 DATA 帧,然后检查帧类型是否为 DATA。接着,它计算新数据的长度,并使用 struct.pack 函数将其转换为字节串。最后,它重新组装帧,并返回修改后的帧数据。

更复杂的篡改:修改 HEADERS 帧

修改 HEADERS 帧稍微复杂一些,因为 HEADERS 帧的负载包含了压缩后的头信息。我们需要使用 HPACK 算法来解压缩和压缩头信息。

HPACK 算法比较复杂,这里我们使用一个简化的例子,假设头信息没有被压缩。

def modify_headers_frame(frame_data, new_headers):
    """
    修改 HEADERS 帧中的头信息(假设头信息没有被压缩)。

    Args:
        frame_data: 原始的 HEADERS 帧数据。
        new_headers: 一个字典,包含新的头信息。

    Returns:
        修改后的 HEADERS 帧数据。
    """
    parsed_frame = parse_frame(frame_data)

    if parsed_frame["type"] != 0x01:
        raise ValueError("不是 HEADERS 帧")

    # 将新的头信息转换为字节串
    header_block_fragment = b""
    for name, value in new_headers.items():
        header_block_fragment += f"{name}:{value}rn".encode('utf-8')
    header_block_fragment += b"rn"  # End of headers

    new_length = len(header_block_fragment)
    new_length_bytes = struct.pack("!I", new_length)[1:]

    # 重新组装帧
    modified_frame = new_length_bytes + bytes([parsed_frame["type"]]) + bytes([parsed_frame["flags"]]) + struct.pack("!I", parsed_frame["stream_id"])[:1] + struct.pack("!I", parsed_frame["stream_id"])[1:] + header_block_fragment

    return modified_frame

# 示例:修改 HEADERS 帧中的头信息
frame_data = b'x00x00x1ax01x04x00x00x00x01:method:GETrn:path:/rn:authority:example.comrnrn' #一个示例的HEADERS帧,包含一些简单的头部
new_headers = {
    ":method": "POST",
    ":path": "/newpath",
    "Custom-Header": "new value"
}
modified_frame = modify_headers_frame(frame_data, new_headers)
print(modified_frame)

parsed_modified_frame = parse_frame(modified_frame)
print(parsed_modified_frame)

这个函数首先解析原始的 HEADERS 帧,然后将新的头信息转换为字节串。 这里我们简单地将头信息拼接成 name:valuern 的格式。 在实际的 HTTP/2 实现中,头信息需要使用 HPACK 算法进行压缩。最后,它重新组装帧,并返回修改后的帧数据。

更高级的篡改:修改 SETTINGS 帧

SETTINGS 帧用于配置 HTTP/2 连接参数。修改 SETTINGS 帧可以用于控制连接的行为。

SETTINGS 帧的负载包含一个或多个设置,每个设置由一个 ID 和一个值组成。

字段名称 长度 (bytes) 描述
ID 2 设置的 ID。
Value 4 设置的值。
def modify_settings_frame(frame_data, new_settings):
    """
    修改 SETTINGS 帧中的设置。

    Args:
        frame_data: 原始的 SETTINGS 帧数据。
        new_settings: 一个字典,包含新的设置(ID: Value)。

    Returns:
        修改后的 SETTINGS 帧数据。
    """
    parsed_frame = parse_frame(frame_data)

    if parsed_frame["type"] != 0x04:
        raise ValueError("不是 SETTINGS 帧")

    new_payload = b""
    for setting_id, setting_value in new_settings.items():
        new_payload += struct.pack("!H", setting_id)  # ID
        new_payload += struct.pack("!I", setting_value)  # Value

    new_length = len(new_payload)
    new_length_bytes = struct.pack("!I", new_length)[1:]

    # 重新组装帧
    modified_frame = new_length_bytes + bytes([parsed_frame["type"]]) + bytes([parsed_frame["flags"]]) + struct.pack("!I", parsed_frame["stream_id"])[:1] + struct.pack("!I", parsed_frame["stream_id"])[1:] + new_payload

    return modified_frame

# 示例:修改 SETTINGS 帧中的设置
frame_data = b'x00x00x06x04x00x00x00x00x00x00x01x00x00x40x00' # 原始SETTINGS帧,SETTINGS_MAX_FRAME_SIZE 设置为 16384
new_settings = {
    0x0001: 65535,  # SETTINGS_HEADER_TABLE_SIZE
    0x0002: 16777215
} #SETTINGS_MAX_CONCURRENT_STREAMS
modified_frame = modify_settings_frame(frame_data, new_settings)
print(modified_frame)

parsed_modified_frame = parse_frame(modified_frame)
print(parsed_modified_frame)

这个函数首先解析原始的 SETTINGS 帧,然后将新的设置转换为字节串。每个设置的 ID 和值都使用 struct.pack 函数转换为字节串。最后,它重新组装帧,并返回修改后的帧数据。

总结:二进制世界的无限可能

通过今天的探险,我们了解了 HTTP/2 帧的结构,并学习了如何使用 Python 来解析和篡改它们。掌握这些技能可以帮助我们更好地理解 HTTP/2 协议,并进行各种有趣和有用的实验。

当然,这只是一个开始。HTTP/2 还有很多其他的帧类型和特性等待我们去探索。希望今天的讲座能激发大家对 HTTP/2 的兴趣,并在二进制世界中发现更多的可能性。

请记住,就像任何强大的工具一样,帧篡改也可能被用于恶意目的。请务必遵守法律法规,并负责任地使用这些技术。祝大家在HTTP/2的世界里玩的愉快!

发表回复

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