跳转到内容

内存安全与常见漏洞

约 70% 的严重漏洞源于内存安全问题(Microsoft、Google Chrome 的多年统计一致)。本章讲栈溢出、释放后使用、整数溢出的原理,缓解措施与绕过(ROP),以及内存安全语言作为根本解法(→ 类型系统、流水线、冒险与乱序执行)。

栈帧(→ 指令集架构)里返回地址与缓冲区相邻——经典的缓冲区溢出:

void vuln(const char *input) {
char buf[64];
strcpy(buf, input); /* 不检查长度:input > 64 字节时
覆盖栈上相邻的返回地址 */
}

攻击:输入里塞入恶意机器码(shellcode)+ 恰好的填充 + 覆盖返回地址指向 shellcode——函数返回时跳去执行攻击者代码。**"控制指令指针"**是一切控制流劫持的原型:覆盖返回地址、函数指针、虚表指针(C++ 的 vtable 劫持)。

释放后使用(use-after-free):释放后的内存被再次分配给新对象,旧指针还在用——类型混淆:把新对象当旧对象读写。

  • UAF 是浏览器漏洞的主力(复杂对象图 + 大量回调);同类:双重释放(double free)、未初始化使用;
  • 堆漏洞的通用形态:堆元数据(chunk 头)被破坏 → 后续分配被操纵 → 任意写。

整数溢出(→ 信息的表示)为内存漏洞铺路:

if (len < sizeof(buf)) { ... } /* len 是 int:负数绕过检查 */
malloc(len); /* len = -1 → 巨大的无符号数 */

先溢出(检查被绕过)再溢出(分配过小)——组合拳:整数溢出通常不直接执行代码,而是让后续的长度检查失效。

每层缓解提高攻击成本(不消除漏洞):

缓解做法绕过
栈保护(canary)栈帧放金丝雀值,返回前校验泄漏/重写 canary;不保护函数指针
DEP/NX数据页不可执行ROP(见下)
ASLR栈/堆/库地址随机化信息泄漏(读指针)+ 喷射
CFI间接跳转校验合法目标粒度内的跳转

**ROP(return-oriented programming)**是 DEP 时代的标准绕过:不注入代码,重用已有代码——把程序里以 ret 结尾的指令片段(gadget)串成链条:控制栈上的一串"返回地址",每个 gadget 做一小步(设置寄存器、调系统调用)。DEP 防注入防不了重用——缓解措施的军备竞赛由此展开(CFI、CET 收紧重用)。

缓解措施提高攻击成本但不消除漏洞:每层缓解把"一次攻击"变成"泄漏 + 绕过 + 重用"的多步链条——成本提高,可行仍在(见侧信道,见下)。

**侧信道(side channel)**绕过所有传统隔离:不攻破边界,测量时间/缓存/功耗的副效应:

  • Spectre:推测执行的缓存副作用(→ 流水线、冒险与乱序执行)——诱导推测执行越界读,用缓存命中与否编码秘密;
  • Meltdown:乱序执行的越界读取在异常前留下缓存痕迹;
  • Rowhammer:反复刷新相邻内存行导致位翻转。

侧信道(Spectre)绕过了所有传统隔离:同核的进程、容器、甚至同超线程的进程都可能被测量——缓解(边界检查屏障、禁用推测)付性能税;根本解法在硬件与新语言。

根本解法是消除漏洞的类别:

  • Rust:所有权 + 借用检查在编译期消除 UAF、越界、数据竞争(→ 类型系统)——Rust 的所有权在编译期消除整类漏洞;
  • 托管语言(Java/Go/Python):GC + 边界检查——运行时保证内存安全,代价是 GC 停顿与性能;
  • C/C++ 的现实:存量代码巨大,迁移是长期工程——渐进策略:新代码用安全语言(Android/Chrome 的 Rust 渗入数据:内存安全漏洞占比显著下降),旧代码上 sanitizers(ASan/UAF、UBSan)+ fuzzing(→ 测试)。

约 70% 的严重漏洞源于内存安全——这条曲线唯一显著的下降来自"新代码不再用 C/C++",不是来自更多缓解。

  • 栈溢出的原型是控制指令指针(覆盖返回地址/函数指针/虚表)
  • UAF 是浏览器漏洞的主力:释放后复用造成类型混淆;整数溢出让长度检查失效
  • 缓解措施提高攻击成本但不消除漏洞:canary/DEP/ASLR/CFI 各有绕过;ROP 重用已有代码绕过 DEP
  • 侧信道(Spectre)绕过了所有传统隔离:测量推测执行的缓存副效应
  • Rust 的所有权在编译期消除整类漏洞;根本解法是内存安全语言,缓解是军备竞赛