🛡️ 第四章 系统漏洞的防范
📄 一句话概括

缓冲区溢出式攻击的原理是:程序给一块内存(缓冲区)留了固定大小的位置,但写入时没有检查"来的数据有多长",于是多出来的部分盖住了相邻的内存——如果被盖住的正好是"函数接下来该跳回哪里"这个地址,程序的执行流就被改写了。它是本章里唯一一个"从根上属于编程语言选择问题"的漏洞,也是少数被当成"整类问题"来消除的对象。

📝 本节和前面几节的关系

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)

读这个输出:

四、⚠️ 2026 现状

30 秒实验

今天做两个动作,各 15 秒:

  1. 打开任务管理器(或活动监视器),看一眼有没有哪个进程占用内存异常、或者经常崩溃后自动重启。频繁崩溃的程序值得留意——虽然绝大多数崩溃只是普通 bug,但"反复异常退出"也是内存类问题的常见外部表现。这一步的意义是建立一种直觉: 这类漏洞在"被利用"之前,往往已经先在"不稳定"上露出过痕迹。
  2. 想一件"你身边长期不更新、也不能更新的联网设备"(老路由器、旧摄像头、老 NAS、工业设备)。对这类设备,今天只做一件事:确认它有没有必要直接暴露在公网。如果有必要——那说明它必须升级或换掉;如果没必要——那就把它收进内网。理由很简单: 这类设备往往是"老代码 + 没开缓解措施"的组合,恰恰是缓冲区溢出最容易见效的地方(4-9 讲的暴露面盘点就包含这一步)。

这一节要带走的一句是:其他漏洞是"写错了",这一类是"用的语言不保证这件事"——所以它的根治办法不是"更小心",而是"换掉那个前提"。

常见误区

  1. 错误:程序开了"编译加固",缓冲区溢出就不会被利用了。——纠正:加固是"抬高利用成本",不是"消除漏洞"。本节 Demo 的第二张表里,每一条缓解手段后面都跟着一句"可以绕过":栈金丝雀可以被信息泄露绕过、不可执行栈可以被返回导向编程绕过、地址随机化可以先泄露一个地址再绕过。它们的作用是"让一个简单的溢出利用变成一串复杂的多步骤攻击"——这很有价值(很多攻击者会因此转向更容易的目标),但不能被理解成"修好了"。 真正的"修好"只有一个方向:让代码不再有"写出边界"的可能性(换语言,或严格使用带边界检查的库)。
  2. 错误:缓冲区溢出是上世纪的古董漏洞,现在很少见了。——纠正:它的"老写法"确实少了,但"这一类问题"仍然活跃。2026 年的证据有两个方向:一是 CISA 前 8 个月仍向 KEV 新增了 7 条缓冲区溢出条目(都是被真实利用的);二是各类漏洞统计里,内存安全类(比如"释放后重用")在近年一直占据显著位置——2026 年上半年,"释放后重用"(CWE-416)以 858 个排在单一类型第二, 而且内存安全类弱点占了前 20 名的 7 席(4-1)。所以准确的判断是:它从"入门级攻击技术"变成了"高级攻击仍在使用的基础设施"。
  3. 错误:我不写 C/C++,所以这类漏洞跟我无关。——纠正:你可能不写,但你一定在用别人用 C/C++ 写的东西。操作系统内核、数据库、Web 服务器、加密库、浏览器引擎、各类设备的固件——这些最底层的组件大多还是 C/C++。所以即使你用 Python 或 Java,你的程序也是搭在这些组件之上的——底层组件出的问题,上层一样会受影响。 对使用者的实际意义是: 这类漏洞的补丁往往出现在"你以为不用管的地方"(内核、容器运行时、VPN 网关、固件)——这也是为什么补丁管理不能只盯自己写的那部分(4-2)。
  4. 错误:堆溢出和栈溢出是一回事,搞清楚一个就够了。——纠正:两者的目标区域和利用难度不同,攻击者的偏好也不同。栈上的溢出(本节的 Demo)是经典写法:目标是盖住返回地址,直接改写"接下来跳到哪"。堆上的溢出发生在动态申请的内存里,比较复杂——它不能直接改写返回地址,通常要配合"覆盖某个函数指针""破坏数据结构""先泄露一个地址"等步骤。 本节 Demo 的结论里专门写了这一点: "教科书式的溢出基本失效,攻击转向堆破坏 + 信息泄露 + 返回导向编程"。了解这个区分不是为了去写攻击,而是为了理解为什么"加固"能挡住简单攻击、却挡不住有备而来的攻击。
自测三问(点开对答案) 1. 缓冲区溢出为什么能从"一个数据问题"变成"执行任意代码"? —— **关键在"溢出盖住了什么"**。**如果只是盖住了普通数据,程序顶多崩溃;但如果盖住了"函数返回时要跳去的地址"(返回地址),那就相当于改写了"程序接下来要执行的指令在哪里"。** **攻击者的做法就是:把那段超长的数据组织成"先填满缓冲区、再把返回地址改成一个自己安排好的位置"**——**等函数执行完毕要返回时,它就跳到了攻击者指定的地方。** **这就是从"数据"到"代码"的跳跃,也是"溢出"这个漏洞为什么严重程度那么高:** **它不需要程序本身做任何错事,只要"没检查长度"就够了。** 2. 为什么说"编译加固"和"换内存安全语言"解决的不是同一个层面的问题? —— **"编译加固"是在保留语言的前提下,给可能出问题的环节各加一道锁**——**栈金丝雀(检查返回地址有没有被改)、不可执行栈(不让栈上的数据被当代码跑)、地址随机化(让攻击者猜不到地址)、控制流完整性(检查跳转是否合法)**。**这些问题都有一个共同点:它们只能"让绕过变难",因为语言仍然允许写出边界。** **而"换内存安全语言"是把这个可能性本身去掉**——**在这类语言里,"写出边界"这件事根本不会被表达出来(编译器在编译期就挡住了)**。**所以两者的关系是:加固是"围着漏洞加护栏",换语言是"让漏洞不存在"。** **现实中的取舍也很清楚:** **加固不需要改写代码(成本低、立刻可用),换语言需要重写(成本高、但一劳永逸)**——**这就是为什么现状是"两条腿走"。** 3. "内存安全类弱点占了前 20 名的 7 席"这句话说明了什么? —— **说明内存安全问题不是"一类漏洞",而是一个占据了很大比重的"漏洞家族"**。**在 4-1 讲过的统计里,2026 年上半年一个对 18,464 个高危/极危漏洞的分析中,内存安全类弱点在前 20 名里占了 7 个位置**(**其中包括排第二的"释放后重用",858 个**)。**这个比例的含义是:** **当今被报出来的严重漏洞里,有相当大的一块都源于同一个根本原因(用不检查边界的语言写代码)**——**这正是"消除整类漏洞"这个思路最典型的应用对象**:**不一个一个修,而是换掉产生这类问题的前提**(这也是 4-11 讲的七个目标之一)。

相关笔记