揭秘cURL HTTP/2库中的堆内存越界读取漏洞

一份详细的安全报告揭示了cURL库lib/http2.c中一个关键的堆内存越界读取漏洞。该漏洞源于对第三方库API的误用,在处理畸形PUSH_PROMISE请求头时可能导致信息泄露或程序崩溃。

堆内存越界读取漏洞: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)。文档明确指出:

namevalue指针保证是以空字符结尾的。应用程序必须使用提供的长度参数。”

存在漏洞的实现

lib/http2.c中,on_header函数在为PUSH_PROMISE请求头格式化字符串时,忽略了长度参数,而依赖于curl_maprintf,该函数内部使用了strlen

1
2
/* lib/http2.c 第1642行附近 - 存在漏洞的代码 */
h = curl_maprintf("%s:%s", name, value);

执行流程

  1. curl_maprintf解析%s格式说明符。
  2. 它内部对namevalue指针调用strlen()
  3. 由于恶意服务器发送了一个不包含空字节的请求头,strlen()会读取超出已分配缓冲区边界的内存,直到在相邻的堆内存中碰巧遇到一个空字节。
  4. 这导致了越界读取。

复现步骤

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回调提供的显式namelenvaluelen参数来限制读取操作。

补丁(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回调的实际文档,该文档明确指出:“namevalue都保证是以空字符结尾的”。他据此认为,虽然代码风格可能不完美,但当前实现并未违反API,因此不构成安全漏洞。报告最终被标记为“不适用”并关闭。bagder还提到,一个几乎相同的问题之前已被错误地报告过(报告#3480078)。

在报告被披露前的最后交流中,darksql透露,他最初对API契约的理解可能源于使用AI工具翻译和总结技术文档时产生的误解(“AI幻觉”),这导致他错误地引用了文档。bagder对此表示,这种行为导致了darksql被禁止参与cURL的安全报告计划。报告最终按项目透明度政策被公开披露。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计