Skip to content

缓冲区溢出与防御

基于通用操作系统概念 · 核于 2026-08

速查

  • 缓冲区溢出(Buffer Overflow):向定长缓冲区写入超过其容量的数据,多出的字节覆盖相邻内存——若覆盖到栈上的返回地址,攻击者就能让程序跳转到自己植入的 shellcode 执行,从而劫持控制流。
  • 栈溢出攻击原理:函数返回时 CPU 从栈上取返回地址(旧 PC)跳回调用者;攻击者用精心构造的超长输入把返回地址改写为 shellcode 的地址,函数一 ret 就跳去执行攻击代码——这是经典的 stack smashing
  • Shellcode:攻击者植入的一段精简机器码(通常用来 exec("/bin/sh") 开 shell),通过覆盖返回地址跳转过去执行。
  • ASLR(地址空间布局随机化):每次运行随机化栈、堆、共享库的加载基址——攻击者猜不准 shellcode 地址,盲跳大概率崩溃。是"提高攻击成本"的缓解,非根治。
  • DEP / NX(数据执行保护 / No-eXecute):利用 CPU 页表的 NX 位,把数据段(栈/堆)标记为不可执行——即便 shellcode 注入成功,CPU 执行到那里也触发异常。逼迫攻击者改用 ROP/JOP。
  • Stack Canary(栈金丝雀):函数序言在返回地址旁插一个随机值(canary),返回前检查它是否被改动——若溢出覆盖了返回地址,必先踩过 canary,检测到不匹配就 __stack_chk_fail 终止进程。名字源于矿工带金丝雀下井测毒气。
  • CFI(控制流完整性):限制间接跳转/调用的目标只能在合法集合内——即便攻击者改了函数指针或返回地址,也只能跳到"预计算的合法目标",无法跳到任意 shellcode/ROP gadget。
  • ROP / Return-Oriented Programming:DEP 让注入代码不可执行后,攻击者转而复用程序已有的代码片段(gadget),用一连串 gadget 地址串在栈上,靠 ret 依次弹出执行——绕过 DEP。CFI 专门针对它。
  • 最小权限:即便缓冲区溢出被利用,若服务以普通用户而非 root 运行,攻击者拿到的也只是普通权限——纵深防御的最后兜底。
  • Meltdown / Spectre(2018)推测执行(speculative execution)的侧信道漏洞。CPU 为提速会推测执行未授权分支(如用户态读内核内存),虽架构上结果被丢弃,但推测时改动了缓存,攻击者用计时探测缓存即可侧信道泄露数据。Meltdown 破内核/用户隔离(缓解:KPTI),Spectre 破进程间隔离(缓解难,性能损失大)。
  • 缓解叠加深意:单层皆可被绕过(ASLR 可被信息泄露破、DEP 可被 ROP 绕、Canary 可被爆破/泄露)——纵深防御叠加后才可靠,配合最小权限兜底。
  • 进阶:理解攻击与防御后,看参考(防御机制对比表、易错点)。

一、缓冲区溢出:最经典的内存攻击

C/C 程序常见这样的漏洞代码:

c
void vuln(char *input) {
    char buf[64];
    strcpy(buf, input);   // 不检查长度!input 超 64 字节就溢出
}

strcpy 不检查源串长度,若 input 来自不可信输入(网络/用户),攻击者塞入 64 字节以上的数据,多出的字节就溢出 buf 覆盖相邻内存。栈上 buf 旁边恰好是保存的返回地址(旧 PC),于是攻击者可以:

栈布局(从高地址到低地址):
┌──────────────┐
│ 返回地址(旧PC) │ ← 攻击目标:改写为 shellcode 地址
├──────────────┤
│ 保存的寄存器  │
├──────────────┤
│  buf[64]     │ ← 溢出从这里开始,向后(高地址)覆盖
└──────────────┘
  • 攻击流程:①攻击者构造超长输入,前 64 字节填垃圾,接下来的若干字节精确填入 shellcode 地址;②strcpy 把返回地址覆盖成 shellcode 地址;③函数 ret 时 CPU 取栈上的"返回地址"(已是 shellcode 地址)跳过去执行攻击代码。
  • 后果:被攻击进程的权限决定了危害上限——若进程以 root 运行,攻击者拿到 root;以普通用户运行,只能拿到普通权限(最小权限兜底)。
  • 历史:1988 年 Morris 蠕虫用 fingerd 缓冲区溢出感染了当时 10% 联网机器,是溢出攻击的"成名战"。此后这类漏洞(如微软 MS03-026 RPC 漏洞催生冲击波蠕虫)反复肆虐。

二、四大缓解机制:纵深防御

现代 OS 用多层缓解对抗缓冲区溢出,单层可被绕过,叠加后才可靠:

ASLR(地址空间布局随机化)

每次程序运行,OS 随机化栈、堆、共享库、可执行文件的加载基址——攻击者无法预知 shellcode 或 gadget 的确切地址

  • 作用:让攻击者盲跳大概率失败(跳到无效地址触发段错误,进程崩溃而非被劫持)。
  • 局限:①非位置无关代码(未开 PIE 的可执行文件)基址固定;②信息泄露(如格式化字符串漏洞泄露栈地址)会绕过 ASLR——一旦泄露一个地址,就能推算其他。
  • 代表:Linux 默认开(/proc/sys/kernel/randomize_va_space=2)。

DEP / NX(数据执行保护)

利用 CPU 页表的 NX(No-eXecute)位,把数据所在的页(栈/堆)标记为不可执行

  • 作用:即便 shellcode 注入到栈/堆成功,CPU 执行到那里立即触发异常(数据区不能当代码跑)。
  • 局限:逼迫攻击者放弃"注入新代码",转而复用程序已有的代码片段(gadget)——即 ROP(Return-Oriented Programming),靠一连串以 ret 结尾的 gadget 拼出任意逻辑,绕过 DEP。
  • 代表:Windows 的 DEP、Linux 的 NX(gcc -z noexecstack)。

Stack Canary(栈金丝雀)

编译器在函数序言把一个随机值(canary)压在返回地址旁,函数返回前检查它是否被改动——若溢出要覆盖返回地址,必先踩过 canary,检测到不匹配就调用 __stack_chk_fail 终止进程。

c
void vuln(char *input) {
    char buf[64];
    uintptr_t canary = __stack_chk_guard;  // 序言:插入金丝雀
    strcpy(buf, input);
    if (canary != __stack_chk_guard)       // 返回前:检查金丝雀
        __stack_chk_fail();                // 被改动 → 终止
}
  • 作用检测栈溢出而非阻止——溢出一旦发生就主动终止进程(崩溃好过被劫持)。
  • 局限:①canary 可被信息泄露泄露出来(如格式化字符串漏洞);②针对 canary 的爆破(子进程 fork 后与父进程同 canary,可逐字节爆破);③堆溢出不踩栈 canary
  • 代表:GCC -fstack-protector、Clang -fstack-protector-strong

CFI(控制流完整性)

编译器为每个间接跳转/调用计算"合法目标集合",运行时校验跳转目标在集合内——即便攻击者改了返回地址或函数指针,也只能跳到预计算的合法目标,无法跳到任意 shellcode 或任意 ROP gadget

  • 作用:从根上限制控制流,让 ROP/JOP 失去"任意跳"的能力——这是最接近"根治"的缓解。
  • 局限:①性能开销(每个间接跳转都要校验);②部分实现(如粗粒度 CFI)仍可被绕过;③部署不普遍。
  • 代表:LLVM CFI、Microsoft CFG/XFG、Linux 的 kCFI。

四机制协同

缓解防什么被什么绕过
ASLR猜不准地址信息泄露地址
DEP/NX注入代码不可执行ROP 复用已有代码
Stack Canary检测栈返回地址被覆盖泄露/爆破 canary、堆溢出
CFI限制间接跳转目标粗粒度实现的盲区

纵深防御的意义正在于此:四层相互补位,攻击者需同时绕过全部才能成功,难度指数级上升。

三、最小权限:最后的兜底

即便上述缓解全部被攻破,最小权限原则仍能限制危害范围:

  • 一个 Web 服务器若以 root 运行,缓冲区溢出被利用 → 攻击者拿到整个系统最高权限
  • 同一服务器若降权以专用普通用户(如 www-data)运行 + 用 chroot/容器/沙箱隔离,溢出被利用 → 攻击者只拿到 www-data 那点受限权限,不能读 /etc/shadow、不能装后门、不能动其他服务
  • 这就是为什么生产环境绝不以 root 跑服务——它是纵深防御里"权限控制"那一层的兜底。

四、Meltdown 与 Spectre:推测执行侧信道

2018 年爆出的两个硬件级安全漏洞,重新定义了"隔离"的边界——它们攻击的不是软件 bug,而是 CPU 为性能做的推测执行

  • 推测执行(Speculative Execution):CPU 为填补流水线空泡,会提前猜测分支走向并执行那条路径。若猜错,架构上结果被丢弃——但缓存已被推测执行改动了
  • Meltdown(破内核/用户隔离):用户态进程通过精心构造的推测执行路径,推测读取内核内存的数据,再用缓存侧信道(用计时探测哪个缓存行被加载)把数据"侧漏"出来——直接打破"用户态不能读内核内存"的铁律。缓解:KPTI(内核页表隔离)——内核态与用户态用不同页表,用户态页表里干脆不映射内核地址(代价:syscall 多切一次页表,开销涨 5-30%)。
  • Spectre(破进程间隔离):诱骗别的进程/虚拟机的 CPU 推测执行越界读取,再用缓存侧信道泄露数据——比 Meltdown 更通用更难修,无法纯靠软件根治,需微码更新 + 重新编译(retpoline 等缓解),性能损失大。
  • 教训:安全机制(页表隔离)可能被底层硬件的优化(推测执行 + 缓存)侧信道绕过——性能优化的副作用有时会侵蚀安全边界。这催生了"安全不仅是软件问题,更是硬件问题"的认知。

下一步

掌握了攻击与防御机制后,看参考——汇总 ACL vs capability、四大防御机制、Unix 权限速查与易错点,作为复习与速查手册。