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

《网络与系统攻防技术》实验一 实验报告

学号:20242415
学期:2026-2027-1

1. 基础知识

本次实验的目标对象 pwn1 是一个 Linux 可执行文件,正常流程是 main 调用 foo,foo 回显用户输入;文件中另有一段正常情况下不会执行的 getShell 代码,我们的任务就是想方设法运行它。

实验用到的基础知识:

  • 函数调用与栈帧:call 指令会把下一条指令的地址(返回地址)压入栈,ret 指令再从栈顶弹出返回地址送到 EIP。因此,只要能篡改栈中被保存的返回地址,ret 时程序就会跳转到攻击者指定的位置。foo 函数入口先是 push %ebp; mov %esp,%ebp; sub $0x38,%esp,局部缓冲区位于 -0x1c(%ebp),即从栈顶往下 28 字节处,返回地址保存在 %ebp+4。
  • 缓冲区溢出(BOF):gets 函数不检查输入长度,会把超长输入一直写入栈上的缓冲区。当写入超过 28 字节缓冲区并继续覆盖掉保存的 ebp 和返回地址后,foo 返回执行 ret 时 EIP 就被劫持为攻击者输入的数据,从而改变程序执行流程。
  • 相关汇编指令及机器码(实验要求掌握):
    • NOP(空操作):90,常用来填充 NOP 滑梯(sled),让跳转落到滑梯内任意位置都能滑到真正的 shellcode。
    • CMP(比较):比较两个操作数并设置标志位,机器码如 39(寄存器/寄存器)、80/81(与立即数比较)。
    • JE(相等则跳转):74,条件跳转;JNE(不等则跳转):75。
    • JMP(无条件跳转):短跳转 EB(8 位偏移),近跳转 E9(32 位相对偏移)。本实验用到的 call 近调用是 E8,后跟 32 位相对偏移,偏移 = 目标地址 − 下一条指令地址。
  • 反汇编与十六进制编程器:用 objdump -d 反汇编查看机器码与符号地址;用十六进制编辑器(如 ghex)直接修改可执行文件中的机器指令字节;用 gdb 动态调试观察寄存器与栈内容。
  • Shellcode 注入:构造一段能执行 execve("/bin/sh") 的机器码,利用栈溢出把返回地址改到栈中 shellcode 的起始位置;配合 NOP 滑梯提高命中率,并在程序前通过 env -i 清空环境变量、ulimit -c unlimited 保留 core 文件来获取稳定的栈地址。

2. 实验内容

围绕“运行 getShell 代码片段”,完成三个实践:

  1. 手工修改可执行文件:用反汇编定位 getShell 与 main 中 call foo 的位置,通过十六进制编辑器改写机器码,把调用目标改为 getShell,直接改变程序执行流程。
  2. BOF 攻击:利用 foo 中 gets 的缓冲区溢出漏洞,构造一段攻击输入字符串覆盖返回地址,触发 getShell。
  3. 注入 Shellcode:制作一段自己的 shellcode(执行 /bin/sh),通过溢出将其注入栈并跳转执行。

3. 实验过程

任务一:手工修改可执行文件,改变执行流程跳转到 getShell

  1. 反汇编定位函数地址。用 objdump -d pwn20242415 | grep -E "(main|foo|getShell)" 查看三个关键符号的地址:getShell 在 0804847d,foo 在 0804847f,main 在 080484af。

    ![任务1-1 反汇编定位函数地址]alt text

  2. 反汇编 main 函数,找到调用 foo 的指令。objdump -d pwn20242415 | grep -A 25 "<main>:" 显示 main 在地址 80484b5 处执行 e8 d7 ff ff ff call 8048491 <foo>。可见调用 foo 用的是 call 指令,机器码 e8 后跟 32 位相对偏移 d7 ff ff ff。

    ![任务1-2 反汇编 main 函数]alt text

  3. 用十六进制编程器修改机器码。目标是把这条 call foo 改为 call getShell。相对偏移的计算方法:目标地址 0x0804847d 减去下一条指令地址 0x080484ba,得到 -0x3d,即 c3 ff ff ff。因此把 e8 d7 ff ff ff 改为 e8 c3 ff ff ff。用十六进制编辑器打开二进制文件,定位到偏移 000004b0 处找到 e8 d7 ff ff ff 并修改。

    ![任务1-3 十六进制编辑器修改机器码]alt text

  4. 保存为带学号的新文件并运行验证。将修改后的文件另存为 pwn20242415_task1,执行 ./pwn20242415_task1,程序不再回显而是直接进入 shell,执行 whoami 返回 kali,说明执行流程已被成功改到 getShell。

    ![任务1-4 运行修改后程序成功获取 shell]alt text

  5. 反汇编 foo 分析漏洞成因。objdump -d pwn20242415 | grep -A 20 "<foo>:" 看到 foo 用 sub $0x38,%esp 分配栈空间,缓冲区地址为 -0x1c(%ebp)(28 字节),随后调用 gets@plt 读入、puts@plt 回显,gets 不检查长度即为溢出点。

    ![任务1-5 反汇编 foo 函数]alt text

  6. 用 gdb 动态验证溢出可行性。gdb ./pwn20242415 后 run,输入一串超长字符,程序发生 Segmentation fault,EIP 被覆盖为 0x35353535(即 ASCII “55555555”),证明返回地址确实可以被输入覆盖。

    ![任务1-6 gdb 验证返回地址被覆盖]alt text

任务二:利用 BOF 漏洞构造 payload,触发 getShell

  1. 构造 payload。缓冲区 28 字节 + 保存的 ebp 4 字节 = 32 字节填充,之后放置 getShell 的地址 0x0804847d(小端序为 \x7d\x84\x04\x08)。用 perl 生成:

    perl -e 'print "111111122222222333333344444444\x7d\x84\x04\x08"' > input20242415_bof
    

    文件名包含学号 20242415。

    ![任务2-1 构造攻击输入文件]alt text

  2. 注入并执行。gets 之后 foo 还会调用 puts 继续从标准输入读数据,因此用 (cat input20242415_bof; cat) | ./pwn20242415 把输入文件喂入并保持管道打开,程序返回地址被覆盖为 getShell,成功进入 shell。

    ![任务2-2 注入 payload 后获取 shell]alt text

任务三:注入自制 Shellcode 并执行

  1. 构造 shellcode 输入文件。准备一段执行 execve("/bin/sh") 的 shellcode,前面用若干 \x90(NOP)作为滑梯。先 ulimit -c unlimited 允许生成 core 文件,然后生成输入并运行程序使其崩溃,以便后续用 core 文件定位栈地址。

    ![任务3-1 生成 shellcode 输入并触发 core]alt text

  2. 用 gdb 分析 core 定位栈地址。gdb ./pwn20242415 $(ls -t core.* | head -1) -batch -ex "info registers esp" -ex "x/40bx $esp-64",从 core 中看到程序崩溃在 0x41414141,并观察到 esp 附近的栈上有一大片 0x90,即 NOP 滑梯所在位置,据此确定 shellcode 的跳转目标地址。

    ![任务3-2 分析 core 定位栈中 NOP 滑梯]alt text

  3. 把返回地址改为栈中 NOP 滑梯地址并重新生成输入。将 payload 中的返回地址改为上一步得到的栈地址(小端 \x20\xde\xff\xff),重新用 perl 生成,并用 xxd 查看十六进制确认 NOP 滑梯与 shellcode 布局正确。

    ![任务3-3 重新生成并核对输入]alt text

  4. 执行验证。用 (cat input; cat) | env -i ./pwn20242415 注入运行(env -i 清空环境变量,使栈地址稳定、减少对溢出的干扰),执行 whoami 返回 kali,说明注入的 shellcode 成功运行,获得了 shell。

    ![任务3-4 注入成功执行 whoami]alt text

4. 问题及解决方案

  • 问题1:任务一中直接把 call foo 的机器码改成 call getShell 时,第一次改了地址但程序仍执行原来的 foo,跳转没有生效。
    • 问题1解决方案:认识到 call 的 e8 后跟的是相对偏移而非绝对地址。重新按公式“偏移 = 目标地址 − 下一条指令地址”计算,0x0804847d − 0x080484ba = -0x3d,把 d7 ff ff ff 改为 c3 ff ff ff 后保存,程序即正确跳转到 getShell。
  • 问题2:任务二中执行 ./pwn20242415 < input20242415_bof 后 shell 一闪而过,无法交互。
    • 问题2解决方案:因为 gets 之后 foo 还会调用 puts 继续读输入,输入重定向的管道会在读完文件后关闭。改用 (cat input20242415_bof; cat) | ./pwn20242415,用后面的 cat 一直占用标准输入保持管道打开,从而维持 shell 交互。
  • 问题3:任务三直接按之前猜的栈地址跳转,程序反复崩溃,shellcode 没有被执行。
    • 问题3解决方案:栈地址受环境变量等因素影响并不固定。先在程序前执行 ulimit -c unlimited 生成 core 文件,再用 gdb 从 core 中读出程序崩溃时的真实 esp 和栈中 NOP 滑梯的位置,把返回地址精确指向 NOP 滑梯,并在运行时用 env -i 清空环境变量保证栈布局稳定,最终成功执行 shellcode。

5. 学习感悟、思考等

这次实验让我第一次把“会读代码”和“能攻破代码”打通了。任务一让我真正理解了 call/jmp 的相对寻址:改动机器码时不是抄个地址就完事,而是要看指令之间的相对偏移,这比背机器码表深刻得多。任务二的 BOF 攻击把课本上的栈帧模型变成了亲眼可见的事实——gets 不检查长度,输入一旦越过 28 字节缓冲区,就把返回地址盖掉了,ret 一出,程序就“身不由己”。任务三的 shellcode 注入则综合考验了栈布局、NOP 滑梯和地址随机化的应对,稍有偏差就段错误,反复调试才成功。

整个过程也提醒我:漏洞的根子在于编写代码时对输入长度的疏忽。写代码时随手用 fgets 替换 gets、开启栈保护、NX 和 ASLR,就能让大部分此类攻击失效。攻防是一体两面,站在攻击者角度做一遍,反过来更能体会防御设计每一条规则背后的意义。

参考资料

posted @ 2026-10-11 12:05  20242415李怡涵  阅读(9)  评论(0)    收藏  举报