4、HTTP/2协议模糊测试:从帧结构到SBI接口变异

各位同学,今天我们来聊聊HTTP/2的模糊测试。说实话,HTTP/2这玩意儿刚出来的时候,我第一反应是“又来了个新协议,又要重新学”。但真正上手之后发现,它比HTTP/1.1有意思多了——二进制帧、多路复用、HPACK压缩,每一个特性都藏着不少攻击面。

我在做5G核心网测试时,SBI接口用的就是HTTP/2。那时候踩了不少坑,今天把这些经验分享给你们。

4.1 HTTP/2帧结构解析

HTTP/2和HTTP/1.1最大的区别是什么?说白了,就是从文本协议变成了二进制协议。你想想看,以前抓个HTTP包还能直接看明文,现在全是二进制帧,不解析根本看不懂。

一个HTTP/2帧长这样:

+-----------------------------------------------+
|                 Length (24 bits)                |
+---------------+---------------+---------------+
|   Type (8 bits) |  Flags (8 bits) |
+----------------+-------------------------------+
|R|                 Stream Identifier (31 bits)   |
+================================================+
|               Frame Payload (0...)              |
+-----------------------------------------------+

每个字段我都解释一下:

  • Length:24位,表示帧负载的长度。注意,这个长度不包括9字节的帧头。
  • Type:8位,定义帧类型。常见的有DATA、HEADERS、SETTINGS、GOAWAY等。
  • Flags:8位,不同帧类型有不同的标志位。比如HEADERS帧的END_HEADERS标志。
  • Stream Identifier:31位,流ID。客户端发起的流ID是奇数,服务端是偶数。

关键点:帧头只有9字节,但Payload可以很大。我在测试时发现,有些实现对Length字段的校验不够严格,你给它填个0xFFFFFF,它可能就直接崩了。

帧类型有10种,但咱们做模糊测试重点关注这几个:

类型用途测试价值
DATA0x00传输数据
HEADERS0x01发送头部极高
SETTINGS0x04设置参数
GOAWAY0x07优雅关闭
CONTINUATION0x09继续头部

我个人习惯,做模糊测试时优先攻击HEADERS帧和CONTINUATION帧。为什么?因为头部压缩算法HPACK就在这里面,处理逻辑复杂,容易出问题。

4.2 HPACK头部压缩算法

HPACK,全称是HTTP/2头部压缩。它的设计初衷是减少头部开销,但实现起来相当复杂。

HPACK的核心机制有两个:

  1. 静态表:预定义了61个常用头部字段,比如:method、:path、content-type等。
  2. 动态表:连接双方动态维护的头部字段表,可以新增和淘汰。

编码方式有三种:

  • 索引表示:直接引用静态表或动态表中的条目,一个字节搞定。
  • 字面量表示:直接传输头部名称和值,可以带索引也可以不带。
  • 霍夫曼编码:对字符串进行霍夫曼压缩,减少传输量。

嗯,这里要注意。HPACK最容易被攻击的地方就是动态表的管理。我曾经遇到过一个问题:某个设备在处理动态表大小变更时,没有做边界检查,结果一个恶意SETTINGS帧把动态表大小设成0,然后后续的HEADERS帧引用了一个不存在的索引——直接段错误。

避坑指南:测试HPACK时,重点关注索引越界、动态表大小溢出、霍夫曼编码解码异常。这三个点我几乎每次测试都能挖到东西。

4.3 基于Boofuzz的HTTP/2 Fuzzer

Boofuzz是Sulley的继承者,用Python写的。我个人觉得它比Sulley好用,文档也清晰一些。

下面是我写的一个HTTP/2 Fuzzer骨架:

from boofuzz import *

def http2_session_definition(session):
    # 先发一个PRI * HTTP/2.0 升级请求
    session.connect(s_get("preface"))
    
    # 发送SETTINGS帧
    session.connect(s_get("settings"))
    
    # 发送HEADERS帧
    session.connect(s_get("headers"))
    
    # 发送DATA帧
    session.connect(s_get("data"))

def define_protocol():
    session = Session(
        target=Target(
            connection=TCPSocketConnection("192.168.1.100", 443)
        )
    )
    
    # 定义Preface
    s_initialize("preface")
    s_static("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n")
    
    # 定义SETTINGS帧
    s_initialize("settings")
    s_byte(0x00, name="Length_high")
    s_byte(0x00, name="Length_mid")
    s_byte(0x06, name="Length_low")  # SETTINGS帧长度
    s_byte(0x04, name="Type")        # SETTINGS类型
    s_byte(0x00, name="Flags")
    s_byte(0x00, name="Stream_High")
    s_byte(0x00, name="Stream_Mid")
    s_byte(0x00, name="Stream_Low")
    s_byte(0x00, name="Stream_Reserved")
    
    # 定义HEADERS帧
    s_initialize("headers")
    s_byte(0x00, name="Length_high")
    s_byte(0x00, name="Length_mid")
    s_byte(0x0F, name="Length_low")
    s_byte(0x01, name="Type")        # HEADERS类型
    s_byte(0x04, name="Flags")       # END_HEADERS
    s_byte(0x00, name="Stream_High")
    s_byte(0x00, name="Stream_Mid")
    s_byte(0x01, name="Stream_Low")  # 流ID=1
    s_byte(0x00, name="Stream_Reserved")
    
    # HPACK编码的头部块
    s_byte(0x82, name="HPACK_Method")  # GET
    s_byte(0x86, name="HPACK_Path")    # /
    s_byte(0x84, name="HPACK_Scheme")  # https
    
    session.connect(s_get("preface"))
    session.connect(s_get("preface"), s_get("settings"))
    session.connect(s_get("settings"), s_get("headers"))
    
    return session

if __name__ == "__main__":
    session = define_protocol()
    session.fuzz()

这段代码看起来简单,但实际跑起来效果不错。我建议你们先跑SETTINGS帧的变异,因为很多实现处理SETTINGS时不够严谨。

小技巧:Boofuzz的s_byte、s_word这些原语,默认会生成边界值、0、最大值等。但HTTP/2的帧头只有9字节,你最好手动控制变异范围,不然效率太低。

4.4 SBI接口消息变异

SBI是5G核心网的服务化接口,底层跑的就是HTTP/2。说白了,NSSF、NRF、UDM这些网元之间通信,用的都是HTTP/2 + JSON。

SBI接口的消息格式一般是这样的:

POST /nudm-sdm/v1/imsi-310150123456789/am-data HTTP/1.1
Host: udm.5gc.mnc150.mcc310.3gppnetwork.org
Content-Type: application/json
Accept: application/json

{
    "supportedFeatures": "00000001",
    "plmnId": {
        "mcc": "310",
        "mnc": "150"
    }
}

注意,虽然SBI用的是HTTP/2,但应用层还是JSON。所以做模糊测试时,要同时考虑两个层面:

  • HTTP/2层:帧结构变异、HPACK变异、流控制变异
  • JSON层:字段缺失、类型错误、嵌套深度、超大数值

我给你们总结一下SBI接口的测试重点:

测试维度变异手法预期效果
帧头变异修改Length、Type、Flags协议栈解析异常
HPACK变异索引越界、动态表溢出头部解压崩溃
流控制变异窗口大小异常、优先级循环资源耗尽
JSON变异字段缺失、类型错误业务逻辑异常
组合变异帧头+HPACK+JSON同时变异深层逻辑漏洞

我曾经在测试NRF时,发现一个很有意思的bug。那个设备在处理GOAWAY帧时,没有检查流ID的有效性。我发了一个GOAWAY帧,把最后一个流ID设成0x7FFFFFFF,结果设备直接进入死循环,CPU跑到100%。

核心思路:SBI接口的模糊测试,不能只盯着HTTP/2协议本身,还要结合5G核心网的业务逻辑。比如你变异一个supi字段,看看UDM会不会返回异常信息,说不定就能泄露用户数据。

好了,这一章的内容就到这里。HTTP/2的帧结构、HPACK压缩、Boofuzz实现、SBI接口变异,这四个点你们回去好好消化一下。下一章我们讲WebSocket协议的模糊测试,那个也很有意思。


专注资料整理