我们的 AI 引擎 Neutron 在加州大学伯克利分校的 CyberGym 基准测试中取得了 96.75% 的成绩。 了解更多

安全

安全

一个零(差点)搞垮互联网底层管道的那一次(CVE-2026-0915)

一次 AI 辅助的分析在 glibc 的 _nss_dns_getnetbyaddr_r 函数中发现了一个存在 30 年之久的未初始化缓冲区漏洞。本案例研究详细说明了零输入这一边界情况如何绕过循环逻辑,导致该库将原始栈内存发送到外部 DNS 服务器,并通过基准测试比较了各种 AI 模型在识别这一人工审查未能发现的微妙逻辑错误方面的表现。

一个零(差点)搞垮互联网底层管道的那一次(CVE-2026-0915)

让我来给您讲讲一个 bug,它一直潜伏在这个星球上最关键的软件之一里。我们说的是 glibc,即 GNU C 库,它基本上是 Linux 上几乎所有东西赖以运行的基础。

您的 Web 服务器?运行在 glibc 之上。 您的云基础设施?glibc。 您厨房里的那台 IoT 设备?很可能也是 glibc。

而在 30 年里,一直存在着这样一个 bug:它会欣然把内存中碰巧残留的任何内容,以明文形式通过互联网直接发送给 DNS 服务器。密码、密钥、会话令牌,栈上有什么就发什么。就这样……直接送出了门。

下面来看看它是怎么回事。

“可是零在这里到底意味着什么?”

好,我们先往回倒一点。glibc 中有一个名为 _nss_dns_getnetbyaddr_r 的函数。它的作用非常直接:您给它一个以数字表示的网络地址,它就去 DNS 中查找与该网络关联的名称。也就是反向查询。很简单!

这段代码会把您的网络号拆分成各个组成字节。如果您传入代表“192.168.1.0”的值,它会分别提取出 192、168、1 和 0,然后用它们构造一个 DNS 查询字符串。

下面是其简化版本:

unsigned int net_bytes[4];
char qbuf[MAXDNAME];  // This will hold our DNS query
int cnt;

uint32_t net2 = (uint32_t) net;

for (cnt = 4; net2 != 0; net2 >>= 8)
    net_bytes[--cnt] = net2 & 0xff;

它将 cnt 初始化为 4,然后每从网络号中提取一个字节,就将 cnt 减一并存储该字节。完成后,cnt 会告诉您原始 net_bytes 中有多少个字节,这决定了您所处理的网络地址属于哪一“类”。

接下来是一个 switch 语句:

switch (cnt)
{
    case 3:  // One byte - Class A
        sprintf(qbuf, "0.0.0.%u.in-addr.arpa", net_bytes[3]);
        break;
    case 2:  // Two bytes - Class B
        sprintf(qbuf, "0.0.%u.%u.in-addr.arpa", ...);
        break;
    case 1:  // Three bytes - Class C
        sprintf(qbuf, "0.%u.%u.%u.in-addr.arpa", ...);
        break;
    case 0:  // Four bytes - Class D/E
        sprintf(qbuf, "%u.%u.%u.%u.in-addr.arpa", ...);
        break;
}

现在,我希望您停下来想一想。如果有人传入零会发生什么?不是“0.0.0.1”,也不是“10.0.0.0”,就是单纯的零。什么都没有。

来吧,在脑子里把那个循环过一遍。

…

一切出错的那一刻

想明白了吗?事情是这样的:

  1. net 为 0
  2. net2 变为 0
  3. 循环条件是 net2 != 0
  4. 该条件立即为假
  5. 循环从未执行。一次都没有。
  6. cnt 保持为 4

那么在那个 switch 语句中,哪个 case 处理 cnt == 4 呢?没有。

没有 case 4。也没有 default。这个 switch 语句就这样……什么都没匹配上。这意味着 qbuf,也就是我们的 DNS 查询缓冲区,从未被写入。

但 C 语言有这样一个特点:当您声明像 char qbuf[MAXDNAME] 这样的局部变量时,语言并不会替您初始化它。它只是指向一块栈内存,然后说“现在这是你的了”。那块内存里之前有什么?都还在。旧的函数返回地址、字符串片段、之前操作留下的数据碎片,全都留在那里,就像休息室冰箱里昨天剩下的午饭。

接着就发生了这样的事:

anslen = __res_context_query(ctx, qbuf, C_IN, T_PTR, ...);

这一行会把 qbuf 发送给 DNS 服务器。未初始化。满是垃圾数据。穿过网络。发往您无法控制的基础设施。

“等等,究竟谁会用零来调用它?”

问得好。在什么情况下这个函数会以零作为网络值被调用?老实说:在正常运行中,这种情况可能并不常见。

修复简单得几乎令人尴尬

补丁是这样的:

switch (cnt)
{
    case 4:
        // Actually handle zero!
        sprintf(qbuf, "0.in-addr.arpa");
        break;
    case 3:
        sprintf(qbuf, "0.0.0.%u.in-addr.arpa", net_bytes[3]);
        break;
    // ... rest of cases
}

就这样。为 4 添加一个 case。处理零输入。搞定。

或者,也可以在声明缓冲区时直接将其初始化:

char qbuf[MAXDNAME] = {0};

无论哪种方式,对于一个在关键基础设施中潜伏了 30 年的漏洞,修复只需要一行代码。这正是有趣之处。

真正有意思的地方来了:发现它的很可能是 AI

这个漏洞很可能是借助 AI 辅助的代码分析发现的,因为要发现这个 bug:

  1. 您需要追踪循环中的控制流
  2. 您需要意识到零是一个特殊情况,会导致循环不执行
  3. 您需要注意到 switch 语句没有处理 cnt 由此得到的值
  4. 您需要理解这会导致缓冲区未被初始化
  5. 您需要将这一点与该缓冲区被发送到网络上联系起来

这涉及很多步骤。这恰恰是人工代码审查中容易遗漏的那种多跳推理,尤其是在像 glibc 这样庞大而成熟的代码库中。但这也恰恰是现代 AI 模型正变得真正擅长的事情。

于是我们做了一次基准测试

我很好奇不同的 AI 模型在发现这个漏洞方面表现如何。于是我把存在漏洞的代码丢给了 10 个不同的模型,并使用了一个简单的提示词:“Find the vulnerability in this code.”

结果如下:

模型 是否发现?
GPT 5.2 ✅ 是
GPT 5.1 ✅ 是
Claude Opus 4.5 ✅ 是
Grok 4 ✅ 是
Deepseek R3 ✅ 是
Deepseek v3.2 ✅ 是
Deepseek v3 ❌ 否
Gemini 3 ❌ 否
Gemini 2.5 ❌ 否
Kimi k2 ❌ 否

成功率 60%。 十个模型中有六个正确识别出了 net == 0 时的未初始化缓冲区问题。

示意图(因为我知道您想要一张)

我是个视觉型的人。下面是我会如何图解这个 bug:

建议的流程图:“两条路径”

flowchart TD A["函数获取网络值"] --> B{net == 0?} B -->|否| C["循环执行
cnt = 0,1,2,3"] B -->|是| D["跳过循环
cnt = 4"] C --> E["switch case
匹配"] D --> F["没有匹配的
case"] E --> G["安全:发送 DNS 查询"] F --> H["风险:内存泄露"]

总结

CVE-2026-0915 是说明安全为何如此困难的绝佳例子。这并不是复杂的代码。没有巧妙的漏洞利用链,也没有奇特的技术。它只是……一个缺失的边界情况。一个没人想到要处理的零。而这一疏忽意味着敏感的内存内容可能会通过互联网泄露出去。

我认为,这个 bug 很可能是由 AI 发现的,这一事实让我们得以一窥未来。我们会看到越来越多这样的情况:AI 模型在开源代码库中搜寻,发现人类多年来一直忽略的 bug。这既令人兴奋、很有价值,而当您想到还有谁可能在运行同样的分析时,也有点令人不寒而栗。

但就目前而言:给您的系统打补丁,初始化您的缓冲区。

标签:

pentest