3.21 缓冲区溢出

缓冲区溢出是程序向固定大小的缓冲区写入超过其容量的数据,导致相邻内存被覆盖的漏洞。这是 C 语言中最经典、最危险的安全漏洞类型。

原理

栈上缓冲区布局

void vulnerable() {
    char buf[8];     // 局部缓冲区在栈上
    gets(buf);       // 没有边界检查!输入可超过 8 字节
}
栈帧布局(调用 vulnerable 后):
高地址
       ┌────────────────────────┐
       │  调用者的栈帧           │
       ├────────────────────────┤
       │  返回地址 (8字节)       │ ← 攻击目标:覆盖此区域
       ├────────────────────────┤
       │  保存的 %rbp (8字节)    │ ← 也可覆盖
       ├────────────────────────┤
       │  buf[7]    buf[4]      │
       │  buf[3]    buf[0]      │ ← 8 字节缓冲区
       └────────────────────────┘ ← %rsp (栈顶)
低地址

溢出过程

正常输入 "ABCD"(4 字节,安全):
buf: [A][B][C][D][ ][ ][ ][ ] [保存 %rbp] [返回地址]
      ↑ buf 起始               ↑ 溢出 8 字节后到达 √

危险输入 16 个 'A':
buf: [A][A][A][A][A][A][A][A] [AAAAAAAA] [AAAAAAAA]
      ↑ buf 起始               ↑ %rbp 被覆盖 ↑ 返回地址被覆盖!

当函数执行 ret 时:
→ ret 读取被覆盖的返回地址 → 跳转到攻击者控制的位置!

攻击利用

经典攻击方式

  1. 注入 shellcode:在溢出数据中包含恶意机器代码
  2. 覆盖返回地址:使函数返回到 shellcode
  3. 获得控制权:执行攻击者的代码
// 攻击者构造的输入结构
// [NOP sled][shellcode][padding][覆盖的返回地址 → shellcode]
输入缓冲区布局:
┌──────────────────────────────────────────────────────┐
│ NOP sled (0x90) | shellcode | 填充 | 伪造返回地址     │
└──────────────────────────────────────────────────────┘
                              ↑ 指向 NOP sled 区域

NOP sled(NOP 滑梯):一串 0x90(x86 NOP 指令),增加攻击成功率。只要跳转到 NOP sled 中的任意位置,最终都会滑入 shellcode。

蠕虫与病毒的利用方式

漏洞类型典型目标后果
栈缓冲区溢出local buffer覆盖返回地址 → 任意代码执行
堆缓冲区溢出malloc 分配的区域覆盖堆管理结构 → 任意写
格式化字符串printf 未指定格式读取/写入任意内存
整数溢出size 计算错误导致堆溢出

著名的蠕虫 Morris Worm(1988) 就是利用 gets() 缓冲区溢出传播。

防御机制

1. 栈随机化(ASLR)

原理:每次程序运行时栈的起始地址随机变化。

# Linux: 查看 ASLR 级别
cat /proc/sys/kernel/randomize_va_space
# 2 = 完全随机化(默认)

效果:攻击者无法预知栈的地址,使得构造精确的返回地址更加困难。

局限性

  • 攻击者可以通过暴力搜索(尝试大量地址)绕过
  • 32 位系统地址空间有限,更容易猜测
  • 通过信息泄露(如格式化字符串漏洞)可以获取随机化的地址

2. 栈保护(Canary / Stack Guard)

原理:在返回地址之前放置一个金丝雀值(canary),函数返回前检查其完整性。

void protected_func() {
    // 编译器插入的代码(用户不可见)
    // 从 fs:0x28 读取 canary 值到栈上
    char buf[8];
    gets(buf);
    // 函数返回前检查 canary
    // 如果 canary 被修改 → 调用 __stack_chk_fail()
}
栈帧(启用栈保护后):
       ┌────────────────────────┐
       │  返回地址               │
       ├────────────────────────┤
       │  canary(金丝雀值)      │ ← 检查此区域是否被修改
       ├────────────────────────┤
       │  保存的 %rbp            │
       ├────────────────────────┤
       │  buf                   │
       └────────────────────────┘ ← %rsp
# 启用了栈保护的函数汇编
protected_func:
    subq    $24, %rsp
    # 设置 canary
    movq    %fs:40, %rax        # 从线程本地存储读取随机 canary
    movq    %rax, 8(%rsp)       # 将 canary 放在 rsp+8
 
    # ... 函数体 ...
 
    # 返回前检查 canary
    movq    8(%rsp), %rax
    subq    %fs:40, %rax        # 与原始值比较
    jne     .Lfail              # 不相等 → 检测到溢出
 
    addq    $24, %rsp
    ret
 
.Lfail:
    call    __stack_chk_fail    # 终止程序

编译控制

gcc -fno-stack-protector   # 禁用栈保护
gcc -fstack-protector      # 仅对含大缓冲区的函数启用
gcc -fstack-protector-all  # 对所有函数启用
gcc -fstack-protector-strong  # 对含局部数组的函数启用(默认)

局限性

  • Canary 值也是从栈上读取,如果攻击者可以读取栈内容(如通过格式化字符串漏洞),可以计算出 canary 值并包含在攻击载荷中
  • 某些攻击不覆盖 canary(如溢出到相邻变量而非返回地址)

3. 不可执行内存(NX 位 / DEP)

原理:将栈和堆标记为不可执行,即使注入 shellcode 也无法执行。

# Linux: 检查 NX 支持
grep nx /proc/cpuinfo

硬件支持

  • x86 页表条目的第 63 位(NX bit)
  • Windows 上称为 DEP(Data Execution Prevention)
  • 通常默认启用
# 查看段权限(Linux)
cat /proc/self/maps
# 输出示例:
# 08048000-08049000 r-xp  ...  ← .text: 读+执行,不可写
# 08049000-0804a000 rw-p  ...  ← .data: 读+写,不可执行
# b7e21000-b7e22000 rw-p  ...  ← 栈: 读+写,不可执行

绕过技术Return-Oriented Programming(ROP) — 攻击者不注入新代码,而是拼接已有代码段中的指令片段(gadgets)构成攻击链。

4. 安全编码实践

// ❌ 危险函数(无边界检查)
gets(buf);
strcpy(dst, src);
strcat(dst, src);
sprintf(buf, "%s", data);
scanf("%s", buf);
 
// ✅ 安全替代
fgets(buf, BUF_SIZE, stdin);       // 指定最大读取长度
strncpy(dst, src, DST_SIZE);       // 指定最大复制长度
strncat(dst, src, REMAIN_SIZE);
snprintf(buf, BUF_SIZE, "%s", data);// 指定缓冲区大小
scanf("%" STR(MAX_LEN) "s", buf);  // 指定最大输入长度
// 安全字符串复制的正确模式
void safe_copy(char *dst, const char *src, size_t dst_size) {
    size_t src_len = strlen(src);
    size_t copy_len = (src_len < dst_size - 1) ? src_len : (dst_size - 1);
    memcpy(dst, src, copy_len);
    dst[copy_len] = '\0';           // 确保以空字符结尾
}

攻击与防御演进

时代攻击技术防御技术
1990s简单 shellcode 注入(固定地址)无(或手动保护)
2000sNOP sled + 精确地址计算栈保护(canary)
2005+ROP / Return-to-libcASLR + NX 位
2010+JIT-ROP / Heap sprayCFI(Control Flow Integrity)

相关笔记

    • 栈帧结构(理解溢出目标的基础)
    • 栈段在进程内存中的位置
    • 更多内存安全内容

链接到