协议概述
本章简单介绍一下HTTP协议的发展与各自特征,后文会在介绍各协议指纹前更详细的说明各协议细节,这里先放一张小香蕉生成的图:

HTTP/0.9 - HTTP/1.1:文本协议时代
这是我们最熟悉的版本,其中0.9早已消失,1.0拥有最成熟的HTTP语意,所以即使抓到2.0的包分析工具(如burpsuite)也会以1.0的方式显示,当然了现在最常见是1.1的版本,下面简单说明:
1.HTTP/0.9(1991):最早的版本,极其简单,仅支持GET方法,无状态,无协议版本声明,是HTTP演化的起点。
2.HTTP/1.0(1996):确立了协议的基本形态,引入了版本号、状态码和头部字段。但致命伤是连接无法复用,每次请求都要重新建立 TCP 连接,效率极低。
3.HTTP/1.1(1999年发布,2022最后更新):解决了 1.0 的性能痛点,引入持久连接(Keep-Alive)复用 TCP;支持分块传输(Chunked)实现流式发送;尝试了管道化(Pipelining)但因队头阻塞(HOLB)未能普及。
HTTP/2:二进制分帧与效率革命
为了解决 HTTP/1.1 的效率瓶颈(特别是队头阻塞和并发连接开销),Google 推出了 SPDY 协议,这后来成为了 HTTP/2 的基础。其核心特点如下:
1.二进制协议:摒弃文本格式,引入二进制分帧层,使得解析更高效、更精确。
2.多路复用(Multiplexing):在单个TCP连接上支持多个并发的请求/响应流,并通过流(Stream)概念进行管理,解决了应用层队头阻塞。
3.头部压缩(HPACK):采用专门算法压缩冗余的HTTP头部,显著减少开销。
4.服务端推送(Server Push):允许服务器预测客户端需求,主动推送资源。
5.流优先级:允许为不同的请求流分配优先级,以优化资源加载顺序。
虽然 HTTP/2 解决了应用层的队头阻塞,但其底层仍依赖 TCP 传输。一旦发生TCP 丢包或重排序,TCP 的重传机制会导致所有正在传输的流都被阻塞。
gQUIC:Google的实验性解决方案
为了彻底解决延迟问题,Google 提出了一个实验性的独立协议。需要注意的是,它与最终标准化的 QUIC+HTTP/3 存在显著差异。虽然 gQUIC 并非 IETF 标准,但它为后续的标准化奠定了坚实基础。其核心特点如下:
1.传输与加密深度耦合:将QUIC传输、基于QUIC Crypto的加密握手(而非TLS)和HTTP/2语义直接捆绑,旨在减少往返延迟。
2.基于UDP:绕过TCP的限制,从根本上避免了TCP的队头阻塞。
3.快速迭代:有众多版本(如Q043, Q046),每个版本在数据包格式、连接ID、握手流程(如CHLO, REJ, SHLO消息)上可能存在差异。
4.快速连接建立:支持0-RTT数据发送。

HTTP/3:标准化的未来
它汲取了gQUIC的经验,IETF将其标准化为QUIC传输协议和上层的HTTP/3协议,旨在替代HTTP/2成为下一代网络标准,其核心特点如下:
1.使用RFC 9000定义的QUIC作为底层传输,继承了其所有优点,包括基于 UDP、内置多路复用、安全的流传输、连接迁移等。值得注意的是,QUIC在这里被设计为独立的传输层,其上可支持多种应用层协议。
2.彻底解决了队头阻塞,由于每个QUIC流都由传输层独立处理,单个包的丢失只影响其所在的流,不会阻塞其他流。
3.集成的TLS 1.3使用RFC 9001定义的机制,将TLS 1.3握手直接集成到QUIC中,加密从第一个数据包开始(初始数据包除外)。
4.改良的头部压缩由于QUIC不保证跨流的顺序,HTTP/3采用了QPACK(而非HPACK)进行头部压缩,通过独立的单向流来管理压缩上下文。
5.帧结构简化,去除了HTTP/2帧中的Flags字段和流标识符(流由QUIC管理),类型和长度使用变长整数编码。
6.拥有明确标识,通过ALPN标识符“h3”在TLS握手中明确宣告。
附:> 虽然QUIC v2已经发布,但其功能与v1完全一致,推出v2的主要目的是为了对抗网络中间设备僵化(Ossification)。在 TCP 时代,许多网络设备会将未知的协议扩展视为畸形数据丢弃,导致协议难以升级。QUIC v2通过修改部分字段值和处理细节,迫使设备制造商适应协议的变更,从而保持协议的演进能力。
从它的进化路径可以看出,它一直奔着更高效、更安全的方向发展,其语意在1.0时基本定型。
协议指纹
HTTP1 (HTTP/0.9, HTTP/1.0, HTTP/1.1)
协议详解
HTTP/1 泛指以文本明文格式传输的早期HTTP协议,包括HTTP/0.9、HTTP/1.0和当前广泛部署的HTTP/1.1。整个演进体现了从极简到功能完备,但在性能、格式严格性上存在固有瓶颈,其协议格式的宽松性和多种实现变体是协议指纹识别的主要来源。
1.HTTP/0.9:它是极简的单行协议,由于仅支持GET方法,响应仅为原始数据,无状态码、无头部字段,在21世纪已经灭绝了,这里仅供了解:
/* HTTP/0.9 请求示例 */
GET /page.html\r\n
/* HTTP/0.9 响应示例(仅为数据,无头部) */
<html>...\r\n
2.HTTP/1.0:它引入了我们现在熟悉的版本声明、状态码、头部字段等核心概念,默认每个请求/响应占用一个TCP连接(无持久连接),如:
/* HTTP/1.0 请求示例 */
GET /index.html HTTP/1.0\r\n
User-Agent: NCSA_Mosaic/2.0\r\n
\r\n
/* 早期非标准持久连接尝试(非RFC,但被广泛实现) */
GET /index.html HTTP/1.0\r\n
Connection: Keep-Alive\r\n
Keep-Alive: timeout=30\r\n
\r\n
3.HTTP/1.1:它解决了HTTP/1.0的核心性能问题,即引入了四个关键改进:
- 持久连接:单个TCP连接可承载多个请求/响应,它通过
Connection: keep-alive来设置,是H1版中请求走私漏洞存在的一个重要条件。 - 强制Host头:支持虚拟主机,即通过Host头来决定目的主机,该头是HTTP/1.1请求的必备字段(RFC 9112 第3节)。
- 分块传输编码:即除了Content-Length外,新增了
Transfer-Encoding: chunked来支持流式传输,这是请求走私的另一个关键点。 - 管道化:它允许在持久连接上并发发送多个请求而不必等待响应,但因队头阻塞实践受限。 如:
GET /index.html HTTP/1.1\r\n
Host: www.example.com\r\n
User-Agent: Mozilla/5.0 (兼容指纹)\r\n
Accept: text/html,application/xhtml+xml\r\n
Connection: keep-alive\r\n
\r\n
协议指纹
HTTP/1协议的指纹来源于其文本格式的可变性、宽松的语法规则以及不同客户端/服务器实现的历史兼容行为。以下从请求与响应两个角度进行总结:
1. 起始行(Start-Line)指纹
- 版本声明:
HTTP/1.1是标准格式。- 变体:错误或旧实现可能发送
HTTP/1.0、HTTP/1.10或大小写错误http/1.1。 - RFC依据:
HTTP-version在RFC 9112第2.3节定义为大小写敏感。
- 方法(Method)大小写:RFC 9112第3.1节定义
method为token,是大小写敏感的。标准方法为大写(GET, POST),但某些脚本或错误实现可能用小写。 - 请求目标形式(RFC 9112第3.2节):
- origin-form (
/path?query):最常见。 - absolute-form (
http://host/path):通常用于代理请求。 - authority-form (
host:port):仅用于CONNECT方法。 - asterisk-form (
*):仅用于OPTIONS *。
客户端对不同场景使用的形式是区分客户端类型的线索。
- origin-form (
/* 起始行指纹示例 */
/* 规范示例 */
GET /api/data HTTP/1.1\r\n
/* 变体1 - 方法小写(非常规,可能来自特定脚本库)*/
get /api/data HTTP/1.1\r\n
/* 变体2 - 版本字段错误(兼容性错误或攻击尝试)*/
GET /api/data HTTP/1.0\r\n
/* 变体3 - 绝对路径形式(通常由代理客户端发出) */
GET http://example.com/api/data HTTP/1.1\r\n
2. 头字段(Header Fields)指纹
- 字段顺序与存在性:RFC未规定字段顺序。
User-Agent,Accept,Accept-Encoding,Accept-Language,Connection等字段的出现顺序、组合及是否存在,是识别客户端(浏览器、爬虫、脚本)和服务器的强特征。 - 字段值内容:
User-Agent:直接标识客户端和操作系统。Accept:MIME类型列表、*/*的使用、q值权重格式。Accept-Encoding:支持的压缩算法列表(gzip,br,deflate)。Connection:keep-alive,close或旧式Keep-Alive(HTTP/1.0兼容)。
- 字段大小写:RFC 9112第5节定义字段名是大小写不敏感的,但建议使用首字母大写的格式(如
Content-Length)。实现可能使用全小写、全大写或混合形式。 - 非标准/废弃字段:如
DNT(Do Not Track),P3P,其存在与否具有标识性。
/* 头字段指纹示例 - 不同客户端的典型特征 */
/* 现代浏览器示例 */
GET / HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: Mozilla/5.0 ...\r\n
Accept: text/html,application/xhtml+xml...\r\n
Accept-Encoding: gzip, deflate, br\r\n
Accept-Language: en-US,en;q=0.9\r\n
Connection: keep-alive\r\n
\r\n
/* Python `requests` 库示例 (字段顺序、大小写差异) */
GET / HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: python-requests/2.28.1\r\n
Accept-Encoding: gzip, deflate\r\n
Accept: */*\r\n
Connection: keep-alive\r\n
\r\n
3. 连接管理与消息体指纹
- 持久连接协商:
- HTTP/1.1:使用
Connection: keep-alive。 - HTTP/1.0兼容:可能使用
Connection: Keep-Alive或旧式Keep-Alive: timeout=5头(RFC 9112附录C.2.2提及此非标准实践)。 - 关键指纹:RFC 9112第9.3节明确规定,代理服务器不得与HTTP/1.0客户端维持持久连接。违反此行为的代理服务器可被识别。
- HTTP/1.1:使用
- 消息体长度界定(关键安全指纹):
- RFC 9112第6节详细描述了三种方式:
Content-Length、Transfer-Encoding: chunked、连接关闭。 Transfer-Encoding与Content-Length共存:RFC 9112第6.1节指出,早期实现可能同时发送两者(用于进度条),但Transfer-Encoding优先。服务器收到此冲突请求时,必须按Transfer-Encoding处理并在处理后关闭连接以防止走私攻击。服务器是否严格遵守此规则是重要指纹。Transfer-Encoding使用条件:客户端仅当确认服务器支持HTTP/1.1时才应发送Transfer-Encoding(RFC 9112第6.1节)。某些客户端可能过早或错误地发送,触发服务器不同响应。
- RFC 9112第6节详细描述了三种方式:
/* 消息体长度界定冲突 - 用于检测服务器安全策略 (RFC 9112 6.1) */
POST /upload HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 13\r\n
Transfer-Encoding: chunked\r\n
\r\n
2\r\n
ab\r\n
0\r\n
\r\n
/* 符合规范的服务器应:1) 按 chunked 解析正文;2) 返回响应;3) 立即关闭连接。 */
/* 不符合规范的服务器可能:1) 按 Content-Length 解析(导致请求走私风险);2) 返回 400 错误。 */
4. 错误处理与兼容性行为指纹
- 对无效输入的处理:
- 起始行前空行:RFC 9112第2.2节要求服务器“应(SHOULD)忽略至少一个空行”。服务器是否忽略、拒绝还是处理这些空行,存在差异。
- 废弃行折叠(Obs-fold):使用
\r\n和空格将头字段值跨多行是过时的(RFC 9112第5.2节)。服务器必须(MUST)拒绝(400)或将其替换为空格。选择哪种方式是指纹。
- 协议降级(Version Downgrade):RFC 9112第2.3节规定,当服务器确知或怀疑客户端对HTTP/1.1实现有误时,可以(MAY)对HTTP/1.1请求发送HTTP/1.0响应。这种降级决策往往基于
User-Agent等头字段的匹配,是主动指纹探测响应版本的依据。 - 对HTTP/1.0客户端的特殊处理:
- 缺失Host头:HTTP/1.1请求必须包含
Host头,否则返回400。服务器对不带Host头的请求的处理(返回400、降级为1.0、默认虚拟主机)是指纹。 - POST后的额外CRLF:早期HTTP/1.0客户端存在一种工作区,在POST请求后发送额外CRLF。RFC 9112第2.2节明确指出,HTTP/1.1用户代理禁止(MUST NOT) 这样做。是否容忍此行为是指纹。
- 缺失Host头:HTTP/1.1请求必须包含
/* 错误处理指纹示例 */
/* 变体1: 起始行前有空行 (RFC 9112 2.2) */
\r\n
GET / HTTP/1.1\r\n
Host: example.com\r\n
\r\n
/* 变体2: 使用废弃的行折叠 (RFC 9112 5.2) */
GET / HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: MyLong\r\n
UserAgentValue\r\n
\r\n
/* 服务器应返回400或将其替换为 `User-Agent: MyLong UserAgentValue` */
HTTP/1协议的指纹识别本质上是利用其规范定义的严格性与实际实现的宽松性之间的巨大差异。通过分析起始行、头字段、连接管理和错误处理中的大量变体行为,可以高精度地区分不同的客户端实现、服务器软件、中间件版本,甚至识别恶意的协议混淆攻击。由于HTTP/1是纯文本协议,它的指纹识别及其容易,只需进行文本对比即可!
HTTP/2
协议详解
HTTP/2是针对HTTP语义的优化表达,它旨在克服HTTP/1.x的性能瓶颈,核心目标是降低延迟和更有效地利用网络资源。HTTP/2是一个二进制协议,这从根本上改变了其调试和分析方式,并引入了新的、更具结构化的指纹特征。
先回忆H1上存在的性能问题:
1. 队头阻塞(Head-of-Line Blocking):HTTP/1.x在同一条TCP连接上,后续请求必须等待前面的请求响应完成(管道化在实践中几乎不可用)。
2. 冗余头部开销:每次请求都携带大量重复且未压缩的头部字段(如Cookie, User-Agent)。
3. 连接数过多:为了并发,浏览器会为同一域名打开多个TCP连接(通常6-8个),增加了连接建立与拥塞控制的负担。
为此H2提出多个特性: - 二进制分帧层:这是与HTTP/1.x最根本的区别,所有通信被分割为更小的消息和帧,并采用二进制格式编码,帧是通信的最小单位。
HTTP/2的常见帧类型如下表所示:
| 帧类型 | ID (十六进制) | 说明 | 相关标识位 |
|---|---|---|---|
| DATA | 0x00 | 传输HTTP请求或响应的主体数据 | PADDED (0x08), END_STREAM (0x01) |
| HEADERS | 0x01 | 打开流并携带HTTP头部字段块 | PRIORITY (0x20), PADDED (0x08), END_HEADERS (0x04), END_STREAM (0x01) |
| PRIORITY | 0x02 | 已弃用,用于设置流优先级 | 无 |
| RST_STREAM | 0x03 | 立即终止流 | 无 |
| SETTINGS | 0x04 | 交换连接配置参数 | ACK (0x01) |
| PUSH_PROMISE | 0x05 | 服务器推送资源的承诺 | PADDED (0x08), END_HEADERS (0x04) |
| PING | 0x06 | 测量往返时间(RTT)和连接保活 | ACK (0x01) |
| GOAWAY | 0x07 | 优雅关闭连接,指示最后处理的流 | 无 |
| WINDOW_UPDATE | 0x08 | 更新流或连接的流量控制窗口 | 无 |
| CONTINUATION | 0x09 | 继续传输HEADERS或PUSH_PROMISE的字段块 | END_HEADERS (0x04) |
当前已定义0x00-0x09,保留值0x0A-0xFF用于未来扩展,接收方必须忽略并丢弃未知类型的帧。而不同帧可使用的标志位定义如下:
| 标识位 | 值 | 适用帧类型 | 含义 |
|---|---|---|---|
| END_STREAM | 0x01 | DATA, HEADERS | 表示这是发送端在该流上发送的最后一帧 |
| END_HEADERS | 0x04 | HEADERS, PUSH_PROMISE, CONTINUATION | 表示此帧包含完整的字段块,后面没有CONTINUATION帧 |
| PADDED | 0x08 | DATA, HEADERS, PUSH_PROMISE | 表示帧包含填充字段 |
| PRIORITY | 0x20 | HEADERS | 已弃用,表示帧包含优先级信息 |
| ACK | 0x01 | SETTINGS, PING | 表示此帧是对先前帧的确认 |
流ID是严格单调递增的,0是特殊流用于连接控制帧,客户端发起的使用奇数,服务端发起(如Server Push)的使用偶数。
- 多路复用(Multiplexing):通过流的概念,一个HTTP/2连接内可以同时进行多个独立的、双向的请求/响应交换(流),帧可以交错发送,互不阻塞。这解决了HTTP/1.x的队头阻塞问题(但在传输层,TCP的队头阻塞依然存在)。
/* 流(Stream)是HTTP/2中的逻辑概念 */
Stream ID = 1: HEADERS帧 (GET /page1)
Stream ID = 3: HEADERS帧 (GET /page2) // 无需等待Stream 1完成
Stream ID = 1: DATA帧 (page1的内容)
Stream ID = 3: DATA帧 (page2的内容) // 交错发送
注意:这里的头帧可以使用符合规则的任意ID,而gQUIC是限制在Stream ID=3里,不要搞混了。
- 头部压缩(HPACK):使用专门的HPACK算法压缩头部字段,大幅减小开销。这里简单介绍下,它通过索引表来压缩头部,有一张静态表是协议标准的,在客户端和服务端都预先存在,存储了常见的请求头/起始行的字段信息,例如
:status 200这种信息,它还有张动态表存储静态表里不存在但渴望压缩的字段,该表在两端默认都为空并在连接建立时就协商了表大小,之后需顺序处理请求头来更新表,通过这可保证客户端和服务端表是一致的(一旦不一致整个连接都必须被重置),除了索引表它还支持哈夫曼编码来压缩字段。
注:H2只处理头部压缩,Body部分(Data帧)在H2协议里没有特别处理,它把这部分留给了上层引用,如使用gzip/broti协议去处理。
- 服务器推送(Server Push):服务器可以主动向客户端推送预计其需要的资源(如图片、CSS),无需客户端明确请求。推送通过一个虚构的请求(
PUSH_PROMISE帧)发起。 - 流优先级:客户端可以指定流的相对优先级,指导服务器分配资源(如带宽、CPU)。然而,RFC 9113中指出优先级方案是可选的,且在实践中实现不一,后来它还被标记为过期了,现在推荐的是在请求头里指定(
Priority: u=0,i)。 - 流量控制:类似于TCP,但作用在单独的流级别和整个连接级别,防止接收端被过快的数据淹没。
协议指纹
HTTP/2的指纹源于其严格的二进制帧协议、复杂的连接状态机以及各种可选的扩展行为。识别主要围绕连接建立、帧交换模式、特定字段值和错误处理等方面。
1. 连接建立与前言指纹
- 协议标识符:
h2:用于基于TLS的HTTP/2(主流)。通过TLS的ALPN扩展协商。h2c:用于明文TCP的HTTP/2。根据RFC 9113第3.1节,h2c令牌的使用已被弃用,但仍可能在一些实现或旧配置中遇到。
- 客户端连接前言:RFC 9113第3.4节规定,客户端必须在TCP/TLS连接上发送的第一个应用数据是24字节的固定魔数字节串,其后必须紧跟一个
SETTINGS帧。
/* 连接前言 (十六进制) */
505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
/* 对应ASCII字符串 */
PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
/* 这是一个精心设计的字符串,目的是让HTTP/1.x服务器认为这是一个错误请求而停止处理,从而“礼貌地”拒绝 */
这个精确的24字节序列是HTTP/2的决定性特征。任何实现都必须发送它。
- 服务器连接前言:服务器必须在连接建立后立即发送一个
SETTINGS帧(可以为空)作为其前言。 - 初始
SETTINGS帧参数:连接初始时交换的SETTINGS帧内容(见下方第3点)是重要的指纹。
2. 帧格式与基本行为指纹
- 帧类型与标志的使用:不同客户端或服务器可能倾向于使用或忽略某些帧类型或标志。
PRIORITY帧:用于设置流优先级。许多实现(尤其是服务器)可能忽略或不完全支持优先级,其发送与否及参数的设定模式是指纹。WINDOW_UPDATE帧:流量控制。更新窗口的时机和增量大小策略(积极或保守)可能因实现而异。PING帧:用于测量RTT和保活。发送PING的频率可作为指纹。DATA帧的PADDED标志:是否及何时使用填充来模糊负载大小(出于安全考虑)。
- 流ID分配模式:客户端发起的流使用奇数ID,服务器发起的流(包括推送)使用偶数ID。观察流ID的分配序列和并发流的数量可以作为行为特征。
GOAWAY帧行为:优雅关闭连接前,是否总是发送GOAWAY帧(RFC建议发送),以及其中声明的“最后处理的流ID”的准确性,可以作为实现质量或特定行为的指纹。
3. SETTINGS帧参数指纹
SETTINGS帧(类型0x04)是HTTP/2连接初始化时交换的关键协商帧,其参数值直接反映了端点的能力和配置,它们的值,出现与否及其顺序是强指纹:
下面是设置的详细说明:
| 设置标识符 | 十六进制值 | 名称 | 说明 | 默认值 | 典型指纹值 | 指纹意义 |
|---|---|---|---|---|---|---|
| SETTINGS_HEADER_TABLE_SIZE | 0x01 | 头部压缩表大小 | HPACK动态压缩表的最大大小(字节) | 4,096 | 65,536 / 4,096 | 较大值(如65,536)表明实现期望压缩更多头部以节省带宽;较小值(如4,096)可能表明资源受限环境 |
| SETTINGS_ENABLE_PUSH | 0x02 | 启用服务器推送 | 是否启用服务器推送功能 | 1 | 0 / 1 | 值0表示禁用,1表示启用。客户端显式禁用推送是常见行为指纹 |
| SETTINGS_MAX_CONCURRENT_STREAMS | 0x03 | 最大并发流数 | 发送方允许接收方创建的最大并发流数 | 无限制 | 100 / 1000 | 服务器通常设得较高(如100或1000),客户端可能较低(如100)。 |
| SETTINGS_INITIAL_WINDOW_SIZE | 0x04 | 初始流量控制窗口大小 | 流的初始流量控制窗口大小(字节) | 65,535 | 65,535 / 131,072 | 客户端或服务器可能调整此值以优化性能,不同的值是指纹 |
| SETTINGS_MAX_FRAME_SIZE | 0x05 | 最大帧大小 | 允许接收的最大帧负载大小(字节) | 16,384 | 16,384 / 16,777,215 | 设置为更大值(如16,777,215)可能表明期望处理大块数据 |
| SETTINGS_MAX_HEADER_LIST_SIZE | 0x06 | 最大头部列表大小 | 愿意接受的未压缩头部列表的最大大小(字节) | 无限制 | 16,384 / 65,536 | 这是一个重要的安全/策略限制,不同实现有不同策略 |
4. 头部处理与伪头部指纹
- 强制小写:RFC 9113第8.2节规定,在构建HTTP/2消息时,字段名必须转换为小写。这是与HTTP/1.x的重大区别。一个携带大写或混合大小写字段名的“HTTP/2消息”本身就是畸形的,但这种转换可能发生在不同层次(库、代理),观察原始流量中的特征可以识别中间件。
-
伪头部字段:HTTP/2使用以冒号
:开头的伪头部字段承载请求/响应的控制数据。它们在头部块中必须出现在所有常规头部字段之前。- 请求必须包含
:method,:scheme,:path(CONNECT除外),通常还有:authority。 - 响应必须包含
:status。 - 指纹点:伪头部字段的顺序、是否包含不必要的伪头部、
:authority与Host头部是否并存及其值是否一致,都可以反映生成该消息的软件。
- 请求必须包含
-
禁止的连接相关头部:HTTP/2明确禁止使用
Connection,Keep-Alive,Proxy-Connection,Transfer-Encoding,Upgrade等头部。任何包含这些头部的HTTP/2消息都是畸形的。检测到这些头部可以强烈暗示流量正从HTTP/1.x被不当转换,或者存在中间件干扰。
5. 错误处理与安全行为指纹
- 对畸形消息的反应:RFC 9113第8.1.1节定义了大量导致消息畸形的情况(如无效伪头部、字段名大写、流状态错误)。实现是选择发送
RST_STREAM(流错误,类型PROTOCOL_ERROR)还是直接关闭连接(连接错误),以及反应速度,可以作为指纹。 - 对特定攻击的缓解:
CONTENT-LENGTH与Transfer-Encoding:HTTP/2不使用分块传输编码。如果收到Transfer-Encoding: chunked头部,应视为畸形。处理方式可识别实现。- Cookie头部拆分:为提升压缩效率,允许将单个
Cookie: a=b; c=d头部拆分为多个Cookie头部(RFC 9113第8.2.3节)。实现是否进行这种优化是指纹。 - padding的使用:出于对抗BREACH等攻击的考虑,
DATA帧是否添加随机填充。
除了H1里提到的HTTP语意指纹,现在它多了更多看不见的东西,例如设置帧/头帧里的字段,以及对于其他特性做出的反应。
gQUIC (Google QUIC / 早期实验性QUIC)
协议详解
gQUIC是IETF将QUIC标准化为RFC 9000之前,由Google主导开发、演进和部署的一套实验性协议家族。gQUIC的设计旨在解决TCP+TLS+HTTP/2协议栈在互联网环境下的固有问题,QUIC的功能相当于 TCP+TLS+HTTP/2,但实现于 UDP 之上,下面详细说明它的机制:
1.连接标识与包处理:早期 gQUIC 版本使用64 位连接 ID,该 ID 由客户端随机生成,用于在网络地址(如 IP/端口)发生变化时(如 NAT 重绑定、Wi-Fi 切换至蜂窝网络)保持连接的连续性,实现连接迁移。QUIC 可以经受 IP 地址变化和 NAT 重绑定,因为连接 ID 在这些迁移过程中保持不变。服务器通过连接ID将数据包匹配到正确的连接上下文,当 4 元组(源/目的 IP 和端口)能唯一标识连接时(如服务器向客户端发送的短时端口数据),可选择省略连接 ID 以节省空间。
2.加密握手协议:它使用了自定义的加密协议(当时TLS1.3还没出来),为握手预留了专用的流 ID 1(后来规范为独立的CRYPTO 流,专用于传输握手消息,而非应用数据流),但其握手消息格式是自定义的、基于标签-值 (Tag-Value) 的消息结构,称为“QUIC Crypto”。QUIC Crypto详细描述了其密码学计算与密钥交换过程,并指出其设计目的之一是减少服务器端的计算开销其性能对比示例显示,QUIC Crypto通过算法选择(如使用 Curve25519 而非 P-256)和协议设计,实现了比传统 TLS 握手更低的服务器端计算延迟。为了将包含完整证书链的拒绝消息(REJ)装入有限的 UDP 数据包(如两个 1350 字节的包)以降低放大攻击风险,gQUIC 实现了书压缩。
3.数据包格式与分层: - 公有包头 (Public Header):每个 QUIC 数据包都以一个 1 到 51 字节的公有包头开始。它包含: - 公共标志 (Public Flags):版本协商位、连接 ID 存在位等。 - 连接 ID (Connection ID):可选的 64 位连接标识符。 - QUIC 版本 (QUIC Version):可选的 32 位版本号。gQUIC 通过快速迭代了数十个版本(如 Q043, Q046),每个版本都有独特的版本号,用于版本协商。 - - 帧 (Frames):数据包载荷由一系列帧组成。帧是控制信息和应用数据传输的基本单元。gQUIC 定义了丰富的帧类型,例如: - STREAM 帧:承载应用层(如 HTTP/2)的数据。 - ACK 帧:支持 SACK 风格的确认范围,并携带精确的接收时间戳,便于精确计算 RTT 和避免重传模糊性。 - WINDOW_UPDATE 帧:用于流级和连接级流控。 - RST_STREAM, GOAWAY 等控制帧。
4.流 (Stream) 与流控制:与 HTTP/2 类似,是连接内独立的、有序的单向或双向字节流。流 ID 具有奇偶性,用于区分客户端发起的流(奇数)和服务器发起的流(偶数)。它通过发送第一个该流 ID 的 STREAM 帧 来隐式创建流,无需显式的流打开信号。它实现了流级和连接级两层信用制流控: - 流级流控:接收方为每个流通告一个绝对字节偏移量上限,即此流上愿意接收的数据最大位置。 - 连接级流控:接收方限定了整个连接上所有流的总数据缓冲能力。 - 通过 WINDOW_UPDATE 帧动态更新这两个限额,并支持类似 TCP 的自动调优。
5.可靠性、拥塞控制与包编号:gQUIC早期版本在整个连接的生命周期中使用单一的、单调递增的包序列号空间。这不仅用于重传,也用于ACK确认。由于重传的包会携带新的包序列号,因此接收方的 ACK 可以明确区分原始传输和重传,彻底避免了 TCP 重传的二义性问题(Karn‘s algorithm 处理的场景),使得 RTT 估计更加准确。拥塞控制算法在用户空间实现,可以快速升级和实验。
协议指纹
gQUIC 的指纹高度依赖于其非标准的 QUIC Crypto 握手、独特的二进制格式以及已废弃的版本特性。识别主要围绕初始字节、握手消息结构、特定标签/帧以及版本行为。
1. 连接发起与初始包指纹
- 公共标志(Public Flags):gQUIC 长包头的第一个字节包含版本、连接ID长度等信息,其格式与 IETF QUIC 长包头(固定位、类型等)不同。
- 版本号:gQUIC 版本号是诸如
Q043、Q046等魔术数字,而非 IETF QUIC 的0x00000001。在QUIC Wire Layout Specification.md中提到的Q031、Q034、Q035、Q036等都是特定的 gQUIC 版本标识。 - 连接ID长度:早期版本(如Q033之前)有 2 字节和 4 字节连接ID长度的标志位,后来被移除。
- 无连接前言:gQUIC 没有 IETF QUIC 要求的那种固定的 24 字节客户端连接前言(
PRI * HTTP/2.0...)。
2. QUIC Crypto 握手消息指纹
- 消息格式:握手消息使用 标签-值(Tag-Length-Value) 的键值对格式。标签是 32 位的数字,通常用 4 字节 ASCII 助记符表示(如
CHLO,SHLO,REJ)。
/* QUIC Crypto 握手消息通用格式 (参考 QUIC Crypto.md) */
struct {
uint32 tag; // 例如 ‘C’ ‘H’ ‘L’ ‘O’ 对应 0x4f4c4843 (小端序)
uint32 length; // 值的长度
uint8 value[length];
} TagValuePair;
- 关键握手标签:
CHLO:Client Hello。包含客户端支持的标签列表(如SNI,VER,NONC)等。SHLO:Server Hello。REJ:Rejection。服务器在完整握手前发送,可能包含STK(源地址令牌)、SNO(服务器名称)、CERT(证书链)等。证书压缩是 gQUIC 的强指纹。SCFG:Server Config。包含服务器的长期公钥等信息。STK:Source-Address Token。源地址令牌,格式为服务器端可解的认证加密块。
- 证书压缩特征:
REJ消息中的证书链使用特定压缩格式,与 TLS 标准格式完全不同。
/* QUIC Crypto 证书压缩格式 (参考 QUIC Crypto.md) */
enum EntryType { end_of_list = 0, compressed = 1, cached = 2, common = 3 };
struct Entry {
EntryType type;
select(type) {
case cached:
uint8 hash[8]; // FNV-1a 64-bit hash
case common:
uint8 set_hash[8];
uint32 index;
}
};
struct Certs {
Entry entries[]; // 以 end_of_list 终止,而非长度前缀
uint32 uncompressed_length;
uint8 gzip_data[];
};
3. 帧类型与参数指纹
- 帧类型空间:HTTP/3(继承自QUIC)帧类型注册表是 62 位空间。gQUIC 的帧类型代码定义在其实验版本中,与 IETF 标准不同。例如,特定的 ACK 帧格式、流阻塞诱导帧(
FHOL)等。 - 已废弃或特有的帧/标签:
- 熵位(Entropy):
QUIC Wire Layout Specification.md指出在 Q034 版本中移除了熵位和私有标志。早期版本中包头包含熵位,这是特有指纹。 - ACK 帧格式:Q034 将 ACK 帧从 NACK 范围改为 ACK 范围,并移除了截断的 ACK。观察 ACK 帧的结构可以推断版本。
- FHOL 标签:Q036 引入的
FHOL标签,用于在握手阶段诱导流间的队头阻塞,是 gQUIC 特有的实验性特征。 - MIDS 标签:Q035 用
MIDS(Maximum Incoming Dynamic Streams)标签替代了旧的MSPC标签,用于独立设置最大入站流数。
- 熵位(Entropy):
4. 传输层行为指纹
- 包号处理:
QUIC Wire Layout Specification描述了gQUIC的包号推断规则:任何被截断的包号应被推断为与发送该包的端点已知的最大包号最接近的值。这与 IETF QUIC 的显示包号长度和推断逻辑可能存在差异。 - 连接迁移:gQUIC 依赖 64 位连接 ID 在 IP 地址变化时保持连接,其会话密钥在迁移中持续使用,这与 IETF QUIC 类似,但具体实现细节和加密上下文的处理可能不同。
- 服务器配置更新(SCUP):gQUIC 使用
SCUP消息在连接过程中刷新STK和服务器配置,以延长客户端可建立 0-RTT 连接的时间窗口。这是其特有的中期握手机制。
5. 安全与错误处理指纹
- 初始包保护:gQUIC 的初始包也使用加密,但密钥来源和保护机制基于其自定义的 QUIC Crypto,而非 IETF QUIC 的从目标连接ID导出的初始密钥(RFC 9001 第5.2节)。
- 错误码:gQUIC 定义了自有的一套错误码(如
QUIC_SEQUENCE_NUMBER_LIMIT_REACHED,当包号达到 2^64-1 时触发),与 IETF QUIC 的错误码(如PROTOCOL_VIOLATION)不同。 - 抗放大攻击机制:使用
STK进行地址验证,服务器在验证客户端地址前严格限制发送数据量(通常不超过接收数据量的3倍),这一策略与 IETF QUIC 类似,但实现载体(STKvsRetry包或NEW_TOKEN帧)不同。
QUIC v1 (IETF QUIC / RFC 9000)

协议详解
QUIC v1是IETF标准化的第一款QUIC传输协议版本,是HTTP/3的底层传输基础。它标志着 QUIC 协议从 Google 的实验阶段进入开放的互联网标准阶段,其最核心的变化是将加密握手协议替换为TLS 1.3,并规范了大量的传输语义和格式。先了解下它的关键概念:
-
Endpoint:Client 或 Server 实体。
-
Connection ID (CID):0-20字节(变长整数),用于标识连接,支持连接迁移和负载均衡。
-
Stream:有序字节序列的抽象,是多路复用的基本单位,它支持双向流(Bidirectional)和单向流(Unidirectional)。
-
Packet & Frame:Packet封装在UDP中,内部包含多个Frame(结构化信息单元),包的ID分为Initial、Handshake、Application Data 三个独立空间。
OK,开始逐个说:
1.先从包开始,它分为包头和类型相关的payload,包头分为用于连接建立阶段的长包头和数据传输阶段的短包头,下面是长包头的示意:
存在如下类型的长包头:
| 包类型 | 类型值 (二进制) | 类型值 (十六进制) | 名称 | 加密级别 | 包号空间 | 发送方 | 主要用途 | 关键字段 | 是否可合并 | 头部保护字段 |
|---|---|---|---|---|---|---|---|---|---|---|
| Version Negotiation | N/A(特殊) | N/A | 版本协商包 | 无加密 | 无包号 | 服务器 | 版本协商 | Supported Versions列表 | 否 | 无 |
| Initial | 00 |
0x00 |
初始包 | 初始密钥 | Initial空间 | 客户端 | 发起连接 | Token, Length, Packet Number | 是 | 保留位(2)+包号长度(2) |
| 0-RTT | 01 |
0x01 |
零往返时延包 | 0-RTT密钥 | 应用数据空间 | 客户端 | 发送早期应用数据 | Length, Packet Number | 是 | 保留位(2)+包号长度(2) |
| Handshake | 10 |
0x02 |
握手包 | 握手密钥 | Handshake空间 | 双方 | 交换TLS握手消息 | Length, Packet Number | 是 | 保留位(2)+包号长度(2) |
| Retry | 11 |
0x03 |
重试包 | 无加密 | 无包号 | 服务器 | 地址验证和连接ID更新 | Retry Token, Integrity Tag | 否 | 无(固定为0000) |
而短包头的包只有一种(1-RTT包):
然后是帧,它定义了 21 种核心帧类型:
| 帧类型值 | 帧名称 | 缩写 | 功能描述 |
|---|---|---|---|
| 0x00 | PADDING | PAD | 填充帧,增加包大小 |
| 0x01 | PING | PING | 心跳帧,保持连接活跃 |
| 0x02 | ACK | ACK | 确认帧(无ECN) |
| 0x03 | ACK | ACK | 确认帧(有ECN) |
| 0x04 | RESET_STREAM | RST | 流重置帧,异常终止流 |
| 0x05 | STOP_SENDING | STOP | 停止发送帧,请求停止发送 |
| 0x06 | CRYPTO | CRYPTO | 加密帧,传输TLS握手数据 |
| 0x07 | NEW_TOKEN | NEW_TOKEN | 新令牌帧,地址验证令牌 |
| 0x08-0x0f | STREAM | STREAM | 流数据帧(8种变体) |
| 0x10 | MAX_DATA | MAX_DATA | 最大数据帧,连接级流控窗口 |
| 0x11 | MAX_STREAM_DATA | MAX_STREAM_DATA | 最大流数据帧,流级流控窗口 |
| 0x12 | MAX_STREAMS | MAX_STREAMS | 最大流数帧(双向流) |
| 0x13 | MAX_STREAMS | MAX_STREAMS | 最大流数帧(单向流) |
| 0x14 | DATA_BLOCKED | BLOCKED | 数据阻塞帧,连接级流控阻塞 |
| 0x15 | STREAM_DATA_BLOCKED | STREAM_BLOCKED | 流数据阻塞帧,流级流控阻塞 |
| 0x16 | STREAMS_BLOCKED | STREAMS_BLOCKED | 流数阻塞帧(双向流) |
| 0x17 | STREAMS_BLOCKED | STREAMS_BLOCKED | 流数阻塞帧(单向流) |
| 0x18 | NEW_CONNECTION_ID | NEW_CID | 新连接ID帧,提供备用连接ID |
| 0x19 | RETIRE_CONNECTION_ID | RETIRE_CID | 退役连接ID帧,停用连接ID |
| 0x1a | PATH_CHALLENGE | PATH_CHALLENGE | 路径挑战帧,路径验证挑战 |
| 0x1b | PATH_RESPONSE | PATH_RESPONSE | 路径响应帧,路径验证响应 |
| 0x1c | CONNECTION_CLOSE | CONN_CLOSE | 连接关闭帧(传输错误) |
| 0x1d | CONNECTION_CLOSE | CONN_CLOSE | 连接关闭帧(应用错误) |
| 0x1e | HANDSHAKE_DONE | HANDSHAKE_DONE | 握手完成帧,服务器通知握手完成 |
| 0x1f*N+0x1b | Extension Frames | EXT | 扩展帧(保留用于扩展) |
协议指纹
QUIC v1 的指纹源于其标准化的 TLS 握手集成、严格的包格式定义以及丰富的可协商传输参数,指纹识别主要围绕连接初始化、包结构特征、传输参数值和特定行为模式:
1. 连接发起与初始包指纹
- 版本号:QUIC v1的版本号为固定的魔术数字
0x00000001。这是与所有gQUIC版本(如 Q043)和未来QUIC版本的根本区别。 - 长包头格式:初始包使用长包头,格式严格且固定。
- 初始密钥派生:初始包的加解密密钥基于一个固定的 初始盐值(Initial Salt)和客户端 Initial 包中的 目标连接ID 派生而来。这确保了只有能看到原始 Initial 包的实体才能正确加解密初始飞行数据。
/* RFC 9001 定义的初始密钥派生 (伪代码) */
initial_salt = 0x38762cf7f55934b34d179ae6a4c80cadccbb7f0a
initial_secret = HKDF-Extract(initial_salt, client_dst_conn_id)
client_initial_secret = HKDF-Expand-Label(initial_secret, "client in", "", Hash.length)
server_initial_secret = HKDF-Expand-Label(initial_secret, "server in", "", Hash.length)
- Token:用于地址验证。可以来自前序连接的
NEW_TOKEN帧,或来自本次连接的Retry包。其存在与否、长度及处理逻辑(如对Retrytoken必须回显)是指纹。
2. TLS 1.3 握手集成指纹
- QUIC 传输参数扩展:在TLS ClientHello和EncryptedExtensions消息中必须携带
quic_transport_parameters扩展,缺少此扩展会导致连接错误,而扩展内传输参数的编码格式和具体值集合是强指纹。 - ALPN 标识:HTTP/3使用ALPN标识符
"h3"。其他基于 QUIC 的应用层协议(如 DNS-over-QUIC)会使用自己的 ALPN。 - 握手消息承载:TLS握手消息通过
CRYPTO帧在QUIC流上传输,而非独立的记录层。CRYPTO帧可以分段、乱序到达(但按偏移重组成有序字节流),这与TCP上的TLS记录层不同。
3. 传输参数指纹
quic_transport_parameters 扩展中协商的参数是QUIC v1最核心、最丰富的指纹源之一,直接反映了端点的能力、策略和实现偏好。
- **关键传输参数示例:
initial_max_data(0x04):连接级别的初始流量控制窗口。值的大小(如 1,048,576 字节)反映了端点对吞吐量的初始期望。initial_max_stream_data_bidi_local(0x05) /initial_max_stream_data_bidi_remote(0x06):本地/对端初始的流级别流量控制窗口。initial_max_streams_bidi(0x08):初始允许的最大双向流数量。服务器通常将此值设得较高(如100),客户端可能设为较低值。max_idle_timeout(0x03):连接最大空闲超时时间(毫秒)。反映了端点对连接持久性的策略。stateless_reset_token(0x06):无状态重置令牌,用于生成 Stateless Reset 包。其存在与否和令牌值本身是重要指纹。disable_active_migration(0x0c):是否禁用主动连接迁移。设置为true(1)表明端点不支持或不允许主动迁移。max_udp_payload_size(0x04):端点愿意接收的最大 UDP 载荷大小。通常设为路径 MTU 相关值(如1452)。
- 0-RTT 传输参数:在 0-RTT 中,客户端使用之前会话中记住的服务端传输参数值。如果参数发生变化(如
initial_max_streams_bidi增大),可能影响 0-RTT 的接受和性能,这也是一个行为指纹点。
4. 帧与包格式行为指纹
- 短包头特征:
- 固定位:短包头第二个比特(Fixed Bit)必须为
1。接收到的短包头此位为0的包必须被丢弃。 - 密钥相位比特:用于1-RTT密钥更新(Key Update)。观察密钥相位切换的频率和模式(如是否积极使用 Key Update)可识别实现。
- 连接ID省略:在1-RTT包中,如果连接可由四元组唯一标识,源连接ID可被省略(长度为0)。
- 固定位:短包头第二个比特(Fixed Bit)必须为
- ACK 帧与 ECN:QUIC v1 的
ACK帧支持显式拥塞通告。是否启用和支持 ECN(通过ecn字段),以及 ACK 延迟的计算和报告策略,可作为实现质量指纹。 - Path Challenge/Response:用于路径验证的
PATH_CHALLENGE和PATH_RESPONSE帧。其使用的频繁程度(如在每次迁移时都使用)反映了实现的对迁移安全性的谨慎程度。
5. 连接生命周期与错误处理指纹
- Stateless Reset:当端点丢失连接状态时,可能会发送无状态重置包。其格式(最后16字节包含一个静态令牌)是标准化的,但触发条件(如多久未活动后发送)和令牌的生成/验证逻辑因实现而异。
- 连接关闭:使用
CONNECTION_CLOSE帧(有不同类型的错误码)或CONNECTION_CLOSE应用层错误。发送关闭帧前是否总是先发送GOAWAY(对于HTTP/3),以及错误码的选择(如PROTOCOL_VIOLATION,INTERNAL_ERROR),反映了实现的健壮性和调试信息丰富程度。 - 空闲超时处理:是安静地关闭连接,还是在超时前发送
PING帧保活,或使用其他机制,这些行为模式也是指纹。
QUIC v1的指纹特征相比于gQUIC更加结构化和标准化。其指纹核心围绕与TLS 1.3的深度集成(特别是 quic_transport_parameters 扩展)、严格定义的包与帧格式、以及丰富的可协商传输参数展开。这些特征不仅使QUIC v1更容易被中间设备识别和测量,也为精确识别客户端库、服务器软件版本以及特定的部署配置提供了可靠依据。同时,其标准化的错误处理和安全机制(如密钥更新、路径验证)也为行为分析提供了新的维度。
对抗
从上面的分析可见HTTP协议指纹极具隐蔽性,若做简单对抗,如通过参数定义请求头顺序与值、设置加密套件与签名参数、指定设置帧的值与顺序等只能骗过初级玩家,实际上不同实现很可能无法简单的通过设置来完美模拟,所以最好的方式是使用它的原版库发包,退而其次是使用同样的开源库,此时只需要使用同样的参数即可,而对于QUIC协议可能会遇到另一个问题,大多库原生都不支持代理该协议,此时需要自己去实现相关能力,在2025年这不是难事了。
参考文档
RFC 9000 – QUIC: A UDP-Based Multiplexed and Secure Transport
RFC 9001 – Using TLS to Secure QUIC
RFC 8446 – The Transport Layer Security (TLS) Protocol Version 1.3