网络与系统攻防技术_实验一
2025-2026-1《网络与系统攻防技术》实验一实验报告
姓名:陈治武 学号:20242411
1. 基础知识
1.1 汇编指令及其机器码
汇编语言是机器指令的可读表示形式,程序最终需要转换为CPU能够执行的机器码。本实验需要分析程序控制流、修改机器指令并利用缓冲区溢出漏洞,因此必须掌握常见指令及其机器码。
NOP:机器码为0x90,表示空操作,不执行任何有效工作,常用于构造NOP雪橇。
CMP:常见机器码包括0x39、0x3B,本质是做减法比较,只修改标志位,不直接修改参与比较的寄存器内容。
JE:机器码通常为0x74,当ZF标志位为1时跳转,即比较结果相等时跳转。
JNE:机器码通常为0x75,当ZF标志位为0时跳转,即比较结果不相等时跳转。
JMP:无条件跳转,短跳转机器码一般为0xEB,长跳转根据寻址方式不同而变化。实验中虽然主要修改的是call指令,但本质上仍然是在改变程序原有执行流程。
1.2 32位Linux程序与ELF可执行文件
ELF(Executable and Linkable Format)是Linux系统中常见的可执行文件格式。ELF文件中包含程序代码、数据、符号表等内容,其中.text段主要存放机器指令。程序运行时,操作系统会将这些内容加载到虚拟内存中,CPU按照入口地址开始执行。
objdump -d命令用于将二进制机器码反汇编为AT&T格式汇编代码,可以查看函数地址、call/jmp指令以及程序整体执行流程。nm命令可以查看符号表并快速定位函数地址。
32位x86架构中,call指令常见格式为e8+相对偏移。CPU执行call时,会先把下一条指令地址压栈保存,再根据补码形式的偏移量计算目标函数地址。因此在修改指令时,不仅要知道目标函数地址,还要理解相对偏移的计算原理。
1.3 栈帧、缓冲区溢出BOF原理
函数调用发生时,会在栈上建立栈帧。典型结构从低地址到高地址依次为:局部缓冲区、保存的EBP、返回地址RET。函数返回时执行ret指令,其本质可以理解为从栈中取出返回地址并赋给EIP,因此如果返回地址被覆盖,程序执行流就会被劫持。
BOF(Buffer Overflow)缓冲区溢出漏洞通常由不安全输入函数引起,例如gets()不检查输入长度。当输入超出缓冲区大小时,会依次覆盖局部变量、保存的EBP,最终覆盖返回地址。攻击者构造合适的payload后,程序在执行ret时就会跳转到攻击者指定地址。
x86 32位系统采用小端字节序,即低字节存放在低地址中。例如地址0x0804847d在payload中需要写成\x7d\x84\x04\x08。这一点在构造BOF攻击载荷时非常关键。
1.4 Shellcode与Payload
Shellcode是可以直接执行的原始机器码,通常作为漏洞利用的最终执行代码。本实验中的Shellcode目标是通过execve("/bin/sh")启动shell。
Payload是完整攻击载荷,通常由填充字节、返回地址、NOP雪橇和Shellcode组成。任务二中的payload主要由填充字节和getShell地址构成;任务三中的payload则包含Shellcode以及Shellcode所在地址。
1.5 Linux缓冲区溢出保护机制
NX/DEP:用于禁止栈等内存区域执行代码,防止攻击者直接运行注入的Shellcode。本实验中通过readelf检查到GNU_STACK为RWE,说明实验文件的栈可执行,因此没有再额外使用execstack -s修改权限。
ASLR:地址空间布局随机化机制。程序每次运行时,栈、动态库等地址都会变化,使攻击者更难预测Shellcode位置。实验中通过查看/proc/sys/kernel/randomize_va_space并将其设置为0,关闭了地址随机化影响。
Stack Canary:即金丝雀保护机制,在返回地址前插入随机值。一旦缓冲区溢出破坏该值,程序就会立即终止。本实验环境为了便于教学演示,目标程序没有受到该保护的明显影响。
2. 实验内容
本实验以Linux下的pwn1程序为对象,围绕“修改程序执行流程—利用BOF漏洞—注入Shellcode”三个层次展开,具体内容如下:
(1)利用反汇编和十六进制编辑器分析并修改目标程序,使main函数不再调用foo,而是直接调用getShell。
(2)在不修改程序的前提下,利用foo函数中的缓冲区溢出漏洞构造payload,覆盖返回地址为getShell函数地址,从而在程序返回时获得shell。
(3)自行编写execve("/bin/sh")的Shellcode,将其注入到栈中,并将返回地址覆盖为Shellcode所在地址,观察程序执行注入代码的过程。
3. 实验过程
3.1 实验环境准备与文件传输
实验开始前,首先需要满足实验截图要求,即终端主机名显示为本人姓名拼音。进入Kali系统后,使用hostname命令验证主机名已经修改为chenzhiwu。

图1 Kali虚拟机启动后的桌面与终端环境
接着通过VMware共享文件夹将实验文件传入虚拟机。最开始直接访问/mnt/hgfs失败,因此先使用vmware-hgfsclient确认共享目录名,再手动创建挂载点并挂载共享文件夹,之后成功进入/mnt/hgfs/kali_share。

图2 通过vmware-hgfs挂载共享文件夹并进入实验目录
将实验文件复制到用户主目录后,为满足实验要求,把文件命名为含学号形式的20242411_pwn1。然后使用file命令查看文件类型,可以确认该程序为32位ELF可执行文件。

图3 复制实验程序并使用file命令确认32位ELF可执行文件
3.2 任务一:分析程序结构并修改执行流程
任务一要求通过修改机器指令改变程序执行流程,因此先使用nm命令查看getShell函数地址。结果显示getShell地址为0x0804847d。随后使用objdump -d导出反汇编结果,结合grep定位foo和main函数位置。

图4 使用nm和objdump分析程序函数地址及反汇编信息
进一步查看main函数附近的汇编可以发现,原程序在0x080484b5处通过call 8048491 <foo>调用foo函数。由于call指令使用相对偏移编码,因此只要修改对应偏移,即可让main直接调用getShell。

图5 查看main函数汇编代码,分析原始调用foo函数流程
为了修改机器码,先安装十六进制编辑器hexedit。随后打开20242411_pwn1文件,定位到对应偏移位置并进行修改。修改完成后,再次使用objdump验证,可以看到main函数中的调用目标已经从foo变为getShell。

图6 使用hexedit查看目标程序二进制内容

图7 安装hexedit工具,为后续修改二进制文件做准备
运行修改后的程序并输入hello,可以看到程序已经不再进入foo,而是直接触发shell环境。虽然输入hello会提示“hello: not found”,但这正说明程序已经成功进入由getShell拉起的shell。任务一完成。

图8 使用十六进制方式查看程序机器码内容
3.3 任务二:利用BOF漏洞覆盖返回地址
任务二要求在不依赖直接修改main调用目标的情况下,利用foo函数本身的缓冲区溢出漏洞控制程序流程。因此需要先在gdb中分析foo函数的栈结构,找出从缓冲区起始位置到返回地址之间的准确偏移。
根据反汇编可以知道,foo函数栈上为局部缓冲区预留了0x38字节空间,但真正被gets写入的字符串起始位置并不在栈顶,而是通过lea -0x1c(%ebp), %eax得到输入缓冲区地址。因此实际利用时必须围绕ebp-0x1c进行分析。

图9 修改后通过objdump验证main函数调用目标变化
继续使用gdb查看$ebp-0x20和$esp附近的栈内容,可以看到输入的'A'字节已经覆盖到栈中。进一步分析可知,从缓冲区起点到保存的返回地址之间共需要32字节填充,因此任务二的payload可以构造为“A”*32 + getShell地址。

图10 运行修改后的程序,验证getShell执行效果
按照小端序,把getShell地址0x0804847d写为\x7d\x84\x04\x08,生成payload文件。直接重定向执行时程序会段错误,这是因为shell启动后输入流结束。为了维持交互,需要采用(cat payload; cat) | ./20242411_pwn1的方式。

图11 在gdb中查看栈空间,分析缓冲区覆盖关系
最终使用(cat payload; cat) | ./20242411_pwn1执行程序,然后输入whoami,成功得到输出kali。说明缓冲区溢出已经成功覆盖返回地址,并在函数ret时转入getShell。任务二完成。

图12 构造BOF攻击payload并进行测试

图13 使用payload覆盖返回地址并通过whoami验证获得shell
3.4 任务三:编写并注入Shellcode
任务三要求不再跳转到程序中已有的getShell,而是自己编写Shellcode并运行。首先编写32位Linux下execve("/bin/sh")对应的机器码,保存为shellcode.py。其核心思路是依次压入字符串“//sh”和“/bin”,再设置寄存器并触发int 0x80系统调用。
编写完成后运行Python脚本,可以看到Shellcode对应的字节串。同时使用readelf -l 20242411_pwn1 | grep STACK检查程序栈属性,结果显示GNU_STACK为RWE,说明栈可读、可写、可执行,因此注入的Shellcode理论上可以直接在栈中运行。

图14 编写execve("/bin/sh") Shellcode程序
为了让Shellcode地址稳定,需要关闭ASLR。之后在gdb中对foo函数中gets之后的位置设置断点,并使用run < sc_input导入仅包含Shellcode的输入,查看$ebp-0x1c附近的内存字节分布。结果表明Shellcode已经被写入栈中,且其起始地址能够通过调试观察到。

图15 使用readelf检查程序栈权限并准备Shellcode注入
进一步查看以word为单位的栈内存,可确认缓冲区起始处和保存的返回地址位置,为最终payload构造提供依据。此时的思路与任务二类似,只不过返回地址不再指向getShell,而是指向栈中的Shellcode起始地址。

图16 在gdb中观察Shellcode写入栈并分析执行地址
实验后续调试中,虽然没有获得稳定的交互式终端,但通过gdb已经观察到程序执行流进入/usr/bin/dash。这说明返回地址已经成功跳转到栈中的注入代码,Shellcode确实得到了执行。
4. 问题及解决方案
(1)共享文件夹无法直接访问。最开始/mnt/hgfs路径不存在,导致无法读取共享目录。解决方法是先使用vmware-hgfsclient确认共享名,再手动创建挂载点并执行挂载命令。
(2)任务二初次直接重定向执行payload时只出现段错误,没有立即看到shell交互。分析后发现,这是因为shell启动后标准输入已经结束,因此改用(cat payload; cat)保持管道输入,问题解决。
(3)任务三中Shellcode执行后没有出现稳定的可交互shell。通过多次调试确认并非Shellcode未执行,而是交互环境受管道与终端关系影响。最终以gdb观测到程序执行/usr/bin/dash作为成功依据。
5. 学习感悟、思考
通过本次实验,我对“程序执行流程并不是固定不变的,而是可以被机器指令和栈内容共同决定”这一点有了更直观的理解。任务一让我认识到,只要理解call指令的编码方式,就能直接修改二进制文件并改变程序逻辑;任务二则让我真正看到了缓冲区溢出如何一步步覆盖返回地址;任务三进一步说明,攻击者不仅可以跳转到现有代码,还可以把自己的代码注入并执行。
从安全角度看,这次实验也让我更清楚地认识到:不安全输入函数、缺少边界检查以及对内存布局缺少保护,都会给攻击者留下利用空间。今后在编写程序时,都必须更加重视输入校验。
参考文件
[1]wildlinux,NetSec项目:0x12_MAL_Exp1 逆向与Bof基础,https://gitee.com/wildlinux/NetSec/blob/master/ExpGuides/0x12_MAL_Exp1%E9%80%86%E5%90%91%E4%B8%8EBof%E5%9F%BA%E7%A1%80.md
浙公网安备 33010602011771号