📄 一句话概括
缓冲区溢出式攻击的原理是:程序给一块内存(缓冲区)留了固定大小的位置,但写入时没有检查"来的数据有多长",于是多出来的部分盖住了相邻的内存——如果被盖住的正好是"函数接下来该跳回哪里"这个地址,程序的执行流就被改写了。它是本章里唯一一个"从根上属于编程语言选择问题"的漏洞,也是少数被当成"整类问题"来消除的对象。
📝 本节和前面几节的关系
4-4 和 4-5 讲的都是"数据被当成了代码",但它们的修复位置在应用代码(参数化、输出转义)。这一节的修复位置要低得多——在内存布局和编程语言这一层。
它和 4-1 讲的"漏洞的根因分类"是呼应的:前面几节的漏洞是"写法不对",这一节的漏洞是"这个写法所在的语言本身不保证安全"——所以它没法靠"小心一点"解决,只能靠换语言或加缓解措施。
一、原理:写多了会盖到别人的地方
先理解"缓冲区"是什么:程序在内存里向系统申请一块空间,用来临时放数据(比如读进来的一行输入、收到的一个网络包)。关键是:这块空间的大小是"申请时就定好"的。
如果程序在写入前不检查"数据有多长",就会发生两件事:
- 数据比缓冲区小 → 正常,多出来的空间没用上;
- 数据比缓冲区大 → 超出的部分继续往后写,盖住了紧邻的内存。
"盖住相邻内存"本身还不算攻击,真正的关键在"被盖住的是什么":
- 如果盖住的是另一块普通数据 → 程序行为异常或崩溃(这也是为什么这类漏洞经常表现为"程序莫名退出");
- 如果盖住的正好是"函数该返回哪里"这个地址(返回地址)→ 执行流就被导向了攻击者指定的位置。
这就完成了从"数据"到"代码执行"的跳跃——而且是权限上的跳跃:如果被溢出的这个程序有更高权限(比如以系统服务身份运行),那么拿到的就是那个权限。
二、为什么它被单独列出来讲
教材把"缓冲区溢出"从一串 Web 攻击里单独拎出来,原因有三个:
- 它是"内存安全"问题的代表:这类问题不来自某一个功能写错了,而来自"用不检查边界的语言写代码"这个前提;
- 它的后果是最严重的一档:其他漏洞多是"泄露信息""越权操作",这一类可以直接变成"在目标机器上执行任意代码";
- 它有一个明确的、工程化的解法:不是"每次写代码时小心一点",而是"换成不提供这种可能的语言"(下一节要讲的"内存安全语言")。
三、动手 Demo:用一段被模拟的内存看溢出
下面这段脚本用一段被模拟的内存来演示"溢出之后,返回地址被盖住"这件事:
# 函数里开了一个 8 字节的缓冲区
buf = ["."] * 8
print("申请了 8 字节的缓冲区:" + "".join(buf))
# 输入 8 个 A(填满)再接一段地址(越过边界)
data = "A" * 8 + "【返回地址】"
print("输入填满并越界后变成:" + data)
# → 超出缓冲区的那部分,落在了【函数返回地址】的位置上
实跑输出:
一、栈溢出的原理(用一段被模拟的内存看)
函数里开了一个 8 字节的缓冲区:........
输入 8 个 A 再接一段地址 → 溢出后变成:AAAAAAAA【返回地址】
→ 超出缓冲区的部分会盖住【函数返回地址】,执行流就被改了。
二、现代缓解手段各挡哪一步
手段 挡住的环节 单用够不够
----------------------------------------------------------------------------------------------
栈金丝雀 Canary 连续覆盖返回地址 可被信息泄露绕过
不可执行栈 NX/DEP 在栈上直接执行代码 转向返回导向编程,仍可绕过
地址随机化 ASLR 精确定位跳转地址 需要先泄露一个地址
控制流完整性 CFI 跳到非法函数入口 x86-64 需开 fcf-protection
编译加固组合 上述多个环节同时 提高成本,不消除漏洞
内存安全语言 整类问题从源头消失 新代码可行,老代码难改
→ 2026 年的结论:教科书式的【覆盖返回地址就跳】基本失效,
攻击转向【堆破坏 + 信息泄露 + 返回导向编程】,
以及【根本没开这些缓解措施的嵌入式设备】。
三、2026 年的两个事实
① CISA 在 2026 年前 8 个月,向 KEV 新增了 7 条缓冲区溢出条目
② 美国把内存安全提上了采购门槛:2026-01-01 起,
仍用 C/C++ 的厂商要交【内存安全路线图】,否则改用内存安全语言
(名单:Rust / Go / C# / Java / Swift / Python / JavaScript)
读这个输出:
- 第一部分只用了 8 个点和一个地址,就把原理讲完了:溢出的后果取决于"盖住了什么",而最危险的情况就是盖住了返回地址。这也是为什么早期这类漏洞的利用非常"直接"——盖住返回地址,让它跳到自己写好的代码上就行。
- 第二部分是这一节最需要读懂的一张表:五种缓解手段各自只挡住"一个环节",而且几乎每一条后面都跟着一句"可以绕过"。 这不是说这些手段没用,而是说明了一个很重要的判断:缓解手段是"抬高成本",不是"消除漏洞"。 最后一行才是根本解法(换语言),但它的限制也很实在——"新代码可行,老代码难改"。
- 第二部分的结论值得单独记住:"教科书式的溢出利用基本失效"和"这类漏洞仍然在被利用"这两句话不矛盾——失效的是"老写法",攻击转向了"堆破坏 + 信息泄露 + 返回导向编程"这些更复杂的技术,而且还有一个庞大的去向:根本没开任何缓解措施的嵌入式设备。这类设备恰恰是 3-10 讲过的物联网世界。
四、⚠️ 2026 现状
- 【"内存安全"从技术话题变成了采购门槛】。 美国从 2026 年 1 月 1 日起,要求仍使用 C/C++ 的软件厂商提交"内存安全路线图",要么给出"什么时候迁移到内存安全语言"的时间表,要么改用内存安全语言。被点名的内存安全语言名单包括 Rust、Go、C#、Java、Swift、Python、JavaScript。这个变化的性质是: 一个过去只在程序员之间讨论的语言选择问题,现在有了合规和采购的后果——这也是"用制度推动整类漏洞消失"的典型例子。
- 【这类漏洞在"已被利用"的清单里仍然有新成员】。 CISA 在 2026 年前 8 个月向 KEV 目录新增了 7 条缓冲区溢出类条目。数量不算多,但"进 KEV"的含义是"确认真实攻击中在被利用"——这说明它不是一个"已经过去的历史问题",而是仍然能被实际打穿的一类。
- 【Rust 在系统软件里的落地已经越过了"试验阶段"】。 Linux 内核在 2025 年 12 月把 Rust 支持从试验状态转正;各大操作系统和云厂商都在把关键组件逐步改写。与之配套的一组数字经常被引用:微软和谷歌都曾公开表示,其产品中被修的高危漏洞里,约七成、约六成来自内存安全问题。这两个数字是整场"换语言"运动的论据基础: 不是"用 C/C++ 的人不小心",而是"这类语言的功能太强、默认太不设防"。
- 【老代码和嵌入式设备仍然是主战场】。 "新代码可行、老代码难改"这句限制在现实中非常硬:一个跑了十年的系统、一个成本压到极限的固件、一台不允许停机的工业设备——这些地方既换不了语言,也常常没开缓解措施。 所以在很长一段时间里,实际的防御姿势是"两条腿走":新代码用内存安全语言(这是根治),老代码靠"编译加固 + 分层隔离 + 限制它被谁访问"来降低风险(这是缓解)。这两条腿都要走,缺一条都会留下很长的尾巴。
30 秒实验
今天做两个动作,各 15 秒:
- 打开任务管理器(或活动监视器),看一眼有没有哪个进程占用内存异常、或者经常崩溃后自动重启。频繁崩溃的程序值得留意——虽然绝大多数崩溃只是普通 bug,但"反复异常退出"也是内存类问题的常见外部表现。这一步的意义是建立一种直觉: 这类漏洞在"被利用"之前,往往已经先在"不稳定"上露出过痕迹。
- 想一件"你身边长期不更新、也不能更新的联网设备"(老路由器、旧摄像头、老 NAS、工业设备)。对这类设备,今天只做一件事:确认它有没有必要直接暴露在公网。如果有必要——那说明它必须升级或换掉;如果没必要——那就把它收进内网。理由很简单: 这类设备往往是"老代码 + 没开缓解措施"的组合,恰恰是缓冲区溢出最容易见效的地方(4-9 讲的暴露面盘点就包含这一步)。
这一节要带走的一句是:其他漏洞是"写错了",这一类是"用的语言不保证这件事"——所以它的根治办法不是"更小心",而是"换掉那个前提"。
常见误区
- 错误:程序开了"编译加固",缓冲区溢出就不会被利用了。——纠正:加固是"抬高利用成本",不是"消除漏洞"。本节 Demo 的第二张表里,每一条缓解手段后面都跟着一句"可以绕过":栈金丝雀可以被信息泄露绕过、不可执行栈可以被返回导向编程绕过、地址随机化可以先泄露一个地址再绕过。它们的作用是"让一个简单的溢出利用变成一串复杂的多步骤攻击"——这很有价值(很多攻击者会因此转向更容易的目标),但不能被理解成"修好了"。 真正的"修好"只有一个方向:让代码不再有"写出边界"的可能性(换语言,或严格使用带边界检查的库)。
- 错误:缓冲区溢出是上世纪的古董漏洞,现在很少见了。——纠正:它的"老写法"确实少了,但"这一类问题"仍然活跃。2026 年的证据有两个方向:一是 CISA 前 8 个月仍向 KEV 新增了 7 条缓冲区溢出条目(都是被真实利用的);二是各类漏洞统计里,内存安全类(比如"释放后重用")在近年一直占据显著位置——2026 年上半年,"释放后重用"(CWE-416)以 858 个排在单一类型第二, 而且内存安全类弱点占了前 20 名的 7 席(4-1)。所以准确的判断是:它从"入门级攻击技术"变成了"高级攻击仍在使用的基础设施"。
- 错误:我不写 C/C++,所以这类漏洞跟我无关。——纠正:你可能不写,但你一定在用别人用 C/C++ 写的东西。操作系统内核、数据库、Web 服务器、加密库、浏览器引擎、各类设备的固件——这些最底层的组件大多还是 C/C++。所以即使你用 Python 或 Java,你的程序也是搭在这些组件之上的——底层组件出的问题,上层一样会受影响。 对使用者的实际意义是: 这类漏洞的补丁往往出现在"你以为不用管的地方"(内核、容器运行时、VPN 网关、固件)——这也是为什么补丁管理不能只盯自己写的那部分(4-2)。
- 错误:堆溢出和栈溢出是一回事,搞清楚一个就够了。——纠正:两者的目标区域和利用难度不同,攻击者的偏好也不同。栈上的溢出(本节的 Demo)是经典写法:目标是盖住返回地址,直接改写"接下来跳到哪"。堆上的溢出发生在动态申请的内存里,比较复杂——它不能直接改写返回地址,通常要配合"覆盖某个函数指针""破坏数据结构""先泄露一个地址"等步骤。 本节 Demo 的结论里专门写了这一点: "教科书式的溢出基本失效,攻击转向堆破坏 + 信息泄露 + 返回导向编程"。了解这个区分不是为了去写攻击,而是为了理解为什么"加固"能挡住简单攻击、却挡不住有备而来的攻击。
自测三问(点开对答案)
1. 缓冲区溢出为什么能从"一个数据问题"变成"执行任意代码"? —— **关键在"溢出盖住了什么"**。**如果只是盖住了普通数据,程序顶多崩溃;但如果盖住了"函数返回时要跳去的地址"(返回地址),那就相当于改写了"程序接下来要执行的指令在哪里"。** **攻击者的做法就是:把那段超长的数据组织成"先填满缓冲区、再把返回地址改成一个自己安排好的位置"**——**等函数执行完毕要返回时,它就跳到了攻击者指定的地方。** **这就是从"数据"到"代码"的跳跃,也是"溢出"这个漏洞为什么严重程度那么高:** **它不需要程序本身做任何错事,只要"没检查长度"就够了。** 2. 为什么说"编译加固"和"换内存安全语言"解决的不是同一个层面的问题? —— **"编译加固"是在保留语言的前提下,给可能出问题的环节各加一道锁**——**栈金丝雀(检查返回地址有没有被改)、不可执行栈(不让栈上的数据被当代码跑)、地址随机化(让攻击者猜不到地址)、控制流完整性(检查跳转是否合法)**。**这些问题都有一个共同点:它们只能"让绕过变难",因为语言仍然允许写出边界。** **而"换内存安全语言"是把这个可能性本身去掉**——**在这类语言里,"写出边界"这件事根本不会被表达出来(编译器在编译期就挡住了)**。**所以两者的关系是:加固是"围着漏洞加护栏",换语言是"让漏洞不存在"。** **现实中的取舍也很清楚:** **加固不需要改写代码(成本低、立刻可用),换语言需要重写(成本高、但一劳永逸)**——**这就是为什么现状是"两条腿走"。** 3. "内存安全类弱点占了前 20 名的 7 席"这句话说明了什么? —— **说明内存安全问题不是"一类漏洞",而是一个占据了很大比重的"漏洞家族"**。**在 4-1 讲过的统计里,2026 年上半年一个对 18,464 个高危/极危漏洞的分析中,内存安全类弱点在前 20 名里占了 7 个位置**(**其中包括排第二的"释放后重用",858 个**)。**这个比例的含义是:** **当今被报出来的严重漏洞里,有相当大的一块都源于同一个根本原因(用不检查边界的语言写代码)**——**这正是"消除整类漏洞"这个思路最典型的应用对象**:**不一个一个修,而是换掉产生这类问题的前提**(这也是 4-11 讲的七个目标之一)。- ☐ 下一节(4-9 检查系统是否存在漏洞)
相关笔记
- 用户登录状态的劫持 —— 上一篇:令牌被拿走之后
- 检查系统是否存在漏洞 —— 下一篇:怎么知道自己有没有漏洞
- 软件漏洞的分类 —— 内存安全类占前 20 的 7 席
- 计算机病毒感染的不仅仅是计算机 —— 没有加固措施的嵌入式设备
- 🛡️ 网络与信息安全 · 索引 —— 全课路线图