缓冲区溢出概述
缓冲区溢出(Buffer Overflow)是一种经典的安全漏洞,当程序未对输入长度进行检查时,多余的数据会覆盖相邻内存区域,进而篡改程序控制流,达到执行任意代码的目的。
1. 栈(Stack)与堆(Heap)
- 栈 (Stack):后进先出(LIFO)结构,用于管理函数调用。每次调用都会创建独立栈帧,包含函数参数、返回地址、保存的寄存器(如 EBP)和局部变量。
- 堆 (Heap):用于动态分配内存,地址从低向高增长,需手动分配和释放。堆与栈分区相互独立。
2. 字节序(Endianness)
在多字节系统中:
- 大端序 (Big-Endian):高位字节存储在低地址,常见于网络协议(如 IP 头)。
- 小端序 (Little-Endian):低位字节存储在低地址,x86、ARM 默认采用此模式。
示例 :32 位地址 0xbffffb80
在小端系统中的存储顺序:
内存地址增长 → 低地址: 0x80 0xfb 0xff 0xbf
在利用漏洞时,必须将地址按小端序写入载荷(如 \x80\xfb\xff\xbf
),否则会跳转到错误地址。
3. x86 栈帧布局与函数调用
asm
高地址
+------------------+
| 参数 n | ← 调用者按右→左顺序压栈
| ... |
+------------------+
| 返回地址 (EIP) | ← `call` 指令自动压栈
+------------------+
| 旧 EBP | ← `push ebp`
+------------------+ ← 新 EBP 指向此处
| 局部变量 | ← `sub esp, N`
| ... |
低地址
调用流程:
- 调用者按反序压入参数。
call
将返回地址压栈,并跳转到函数入口。push ebp; mov ebp, esp; sub esp, N
设置新栈帧。leave; ret
恢复 EBP 并将返回地址弹栈到 EIP。
4. 缓冲区溢出攻击流程
以以下易受攻击函数为例:
c
void vulnerable(char *input) {
char buffer[256];
strcpy(buffer, input); // 未检查边界
}
- 当
input
超过 256 字节,多余数据向高地址写入,依次覆盖:旧 EBP → 返回地址 → 参数区。 - 覆盖返回地址后,执行
ret
时,EIP 跳转到攻击者指定地址。 - 若该地址指向包含 shellcode 的输入区,即可实现任意代码执行。
5. 构造利用载荷
bash
./vuln $(python -c 'print "A"*268 + "\x80\xfb\xff\xbf" + "\x90"*20 + SHELLCODE')
- 偏移量:通过 GDB 找到 EIP 覆盖点,此处为 268。
- 小端地址 :
\x80\xfb\xff\xbf
对应内存地址0xbffffb80
。 - NOP 雪橇 :
\x90
填充,覆盖返回地址与 shellcode 之间的区域,扩大跳板范围。
6. 获取 ESP 地址的方法
方法一:EIP 覆盖测试
$(python -c 'print "A"*268 + "BBBB"')
,观察 GDB 崩溃时:返回地址是否被0x42424242
(BBBB
)替换。- 若匹配,说明偏移 268 后正好覆盖返回地址。
方法二:ESP 跳板测试
-
在偏移基础上继续追加 NOP 和标记:
bash$(python -c 'print "A"*268 + "BBBB" + "C"*20')
-
当 EIP 被
BBBB
覆盖后,程序崩溃时返回地址已弹栈,GDB 显示 ESP 指向CCCC...
区域,即 shellcode 起始处。 -
该地址即为跳转目标,可按小端序写入返回地址。
注意:多次测试时,若输入长度变化(如从 272 → 292 字节),操作系统会为对齐在更低地址分配新的栈空间,导致 ESP 地址出现偏移差异。
7. NOP 雪橇与其作用范围
- 目的 :在返回地址到 shellcode 区间填充
\x90
,形成NOP
滑板,确保 EIP 落点可滑入真实 shellcode。 - 长度:通常几十到数百字节,根据偏移范围动态调整。
8. 坏字符测试(Bad Character Testing)
-
使用脚本生成连续字节串:
bashpython -c 'print "".join([chr(i) for i in range(1,256)])'
-
将该字符串传入程序,在 GDB 中查看内存(
x/256x $esp
或x/256b $esp
)。 -
若发现某字节丢失或被解释异常,则需将其从 payload 中排除,直到所有字节都能正确通过。
9. 常见调用约定对比
约定 | 参数顺序 | 清理者 | 示例 |
---|---|---|---|
cdecl | 右→左 | 调用者 | C 默认 |
stdcall | 右→左 | 被调用者 | Windows API |
fastcall | 前几个通过寄存器 | 被调用者 | 性能优化 |
10. x86 与 x64 区别
- 寄存器宽度:x86 为 32 位,x64 为 64 位。
- 新增寄存器:x64 增加 R8--R15。
- 参数传递:x64 多数通过寄存器,栈负载减少。
以上内容整合了栈与堆、字节序、栈帧结构、利用流程、偏移与跳板测试、NOP 雪橇、坏字符测试及调用约定差异,建议结合 GDB 工具逐步验证。