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种,但咱们做模糊测试重点关注这几个:
| 类型 | 值 | 用途 | 测试价值 |
|---|---|---|---|
| DATA | 0x00 | 传输数据 | 高 |
| HEADERS | 0x01 | 发送头部 | 极高 |
| SETTINGS | 0x04 | 设置参数 | 高 |
| GOAWAY | 0x07 | 优雅关闭 | 中 |
| CONTINUATION | 0x09 | 继续头部 | 高 |
我个人习惯,做模糊测试时优先攻击HEADERS帧和CONTINUATION帧。为什么?因为头部压缩算法HPACK就在这里面,处理逻辑复杂,容易出问题。
4.2 HPACK头部压缩算法
HPACK,全称是HTTP/2头部压缩。它的设计初衷是减少头部开销,但实现起来相当复杂。
HPACK的核心机制有两个:
- 静态表:预定义了61个常用头部字段,比如:method、:path、content-type等。
- 动态表:连接双方动态维护的头部字段表,可以新增和淘汰。
编码方式有三种:
- 索引表示:直接引用静态表或动态表中的条目,一个字节搞定。
- 字面量表示:直接传输头部名称和值,可以带索引也可以不带。
- 霍夫曼编码:对字符串进行霍夫曼压缩,减少传输量。
嗯,这里要注意。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协议的模糊测试,那个也很有意思。