一个零(差点)搞垮互联网底层管道的那一次(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”,就是单纯的零。什么都没有。
来吧,在脑子里把那个循环过一遍。
…
一切出错的那一刻
想明白了吗?事情是这样的:
net为 0net2变为 0- 循环条件是
net2 != 0 - 该条件立即为假
- 循环从未执行。一次都没有。
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:
- 您需要追踪循环中的控制流
- 您需要意识到零是一个特殊情况,会导致循环不执行
- 您需要注意到 switch 语句没有处理
cnt由此得到的值 - 您需要理解这会导致缓冲区未被初始化
- 您需要将这一点与该缓冲区被发送到网络上联系起来
这涉及很多步骤。这恰恰是人工代码审查中容易遗漏的那种多跳推理,尤其是在像 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:
建议的流程图:“两条路径”
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