20242403 2026-2027-1 《网络与系统攻防技术》实验一实验报告
1.基础知识
关于ELF
ELF是Linux系统常见的可执行文件格式。程序在文件中以机器码保存,CPU运行时依据指令地址、寄存器和内存内容执行。如果我们想看懂指令,则需要借助objdump -d将机器码解释为汇编指令。
关于指令
NOP, JNE, JE, JMP, CMP等汇编指令的常用机器码作用如下表格所示:
|
汇编指令 |
机器码 |
作用 |
|---|---|---|
|
NOP |
0x90 |
空操作,不改变程序运行状态,仅占用一个指令周期,常被用于指令对齐或漏洞利用中的占位填充 |
|
CMP |
XX |
比较两个操作数,仅修改标志寄存器的状态位,不保存运算结果,常用于条件跳转的判定依据 |
|
JMP |
XX |
无条件直接跳转,跳转到指定地址后继续执行程序,可实现程序执行流程的直接改变 |
|
JE |
XX |
件跳转,当相等标志位ZF为1时跳转到指定地址,通常配合CMP指令使用,判定两个操作数相等时跳转 |
|
JNE |
XX |
条件跳转,当相等标志位ZF为0时跳转到指定地址,通常配合CMP指令使用,判定两个操作数不相等时跳转 |
关于BOF
BOF(Buffer Overflow)攻击,也就是缓冲区溢出攻击,是一种利用程序向缓冲区写入超出其容量的数据、从而覆盖相邻内存内容来控制程序执行流程的攻击方式。它的本质是程序对输入数据缺乏边界检查(如gets、strcpy等),最常见的目标是覆盖函数栈上的返回地址,让程序跳转到攻击者指定的位置执行恶意代码。本实验中我们就要利用这个漏洞构造payload进行BOF攻击,黑进Shell。
2.实验内容
本次实验以pwn1程序为研究对象,展开三种不同方式的攻击:
- 直接修改程序指令:创建原始文件副本,修改main()内的CALL指令,使程序直接执行 getShell()。
- 缓冲区溢出利用:通过缓冲区溢出覆盖函数栈的返回地址,将程序执行流程跳转到程序中已存在的getShell()函数,不修改原有程序文件即可拿到目标Shell权限。
- 自制 Shellcode 注入:将提前编写好的、可直接调用系统Shell的二进制Shellcode注入目标程序的缓冲区,通过溢出覆盖返回地址跳转到注入的Shellcode,执行恶意代码拿到Shell权限。
本实验的核心目标是通过完成三种不同难度的攻击,掌握二进制ELF文件结构、x86汇编基础以及缓冲区溢出攻击的基本原理与实现方法,建立对二进制漏洞利用的初步认知。
3.实验过程
准备工作
在准备开始实验之前,我们要确定Kali系统能否解析并运行这个文件,并做好备份。

可以看到,出现了ELF 32-bit LSB executable的文件类型,且可以直接运行,其内部的foo函数会回显任何用户输入的字符串(如hello),说明实验环境完整,可以开始。
第一种:直接修改程序指令

如上图所示,首先用objdump反汇编,能找到main、foo、getShell:

还可以找到80484b5这一行:

这里的e8是call指令,后面4个字节(d7 ff ff ff)是跳转偏移。然后,在80484b6的位置(就是b5往后一位),写入一个字节0xc3覆盖0xd7,改完后再反汇编确认。
为什么是在80484b6的位置上把d7改成c3呢?因为getShell对应的是7d,foo对应的是91,做十六进制减法可得91-7d=14,所以做十六进制减法得d7-14=c3。
修改后,就变成了这样:

此时再运行程序,看上去似乎没什么太大变化,但是注意:前面出现了一个$,这说明我们已经跑进了shell!比如我们输入一个whoami,它可以回答xiayuanpu,说明攻击成功。
此外,这一步做完后,pwn1文件就会被修改,不能再次使用,需要备份好的原文件才能再做另外两种攻击方式。
第二种:构造溢出payload
这里不改文件,靠foo的缓冲区溢出覆盖返回地址。我们先用GDB测试下偏移量:

返回的是0x49494949,而0x49在ASCII码里对应的是I,第一个I前面有A~H共32位,即说明返回地址从第32个字节开始。因此我们输入32个A试一试:

似乎并没有出现我们想要的结果,返回的是全零。这是为什么?
我们尝试加入断点:

这说明程序确实跳到了getShell,返回地址覆盖得完全正确。前面输出的乱码是因为payload 里的x7d\x84\x04\x08被foo回显出来了。
为了进一步确认,我们试试用普通终端的管道命令:

这里有一点小瑕疵,第一次输入的时候还会出现异常,第二次就好了。我们加入一个换行符来解决这个瑕疵:

这样就完美了。乱码还是在的,不过被一个换行符跳过去了,不妨碍我们的输入。
第三种:自制Shellcode的注入与执行
在开始之前,我们需要做点小小的准备工作:关闭 ASLR(Address Space Layout Randomization),否则栈地址每次运行都变。输出0表示成功。

然后我们进行反汇编:

接着,我们要确定缓冲区的位置:

我们采用第二个结果,即0xffffce9c,作为缓冲区地址。根据本次任务三的Payload标准结构,我们这么布局:
- 偏移0-31:32 个 A(填充)
- 偏移32-35:返回地址(小端序)
- 偏移36-135:100 个 NOP(\x90)
- 偏移136-158:shellcode(23 字节)
返回地址不要指向shellcode的开头(太精确容易失败),而是指向NOP滑道中间,让CPU 顺着NOP滑到shellcode。因此我们选偏移36 + 60 = 96处,即0xffffcefc。
于是我们这么做:

然后我们进行验证,先进行GDB验证:

再进行普通终端验证:

可以看到能执行我们的指令。
因为现在的程序是通过管道 (cat payload; cat) | ./pwn1 运行的,shell 的标准输入不是终端(tty),而是管道,并不会显示$或者#。这种情况下,dash(Kali 默认的 /bin/sh)不会打印交互式提示符,但命令仍然会执行。这对于目前的实验来讲足够了。
4.问题及解决方案
Python3 -c里面的一大堆十六进制是什么?
其实就是机器码。为了方便,我们这么翻译过来会更舒服一些:
\x31\xc0 xor %eax, %eax ; eax = 0 \x50 push %eax ; 在栈上压入 0(字符串结尾 \0) \x68\x2f\x2f\x73\x68 push $0x68732f2f ; 压入 "//sh"(小端序) \x68\x2f\x62\x69\x6e push $0x6e69622f ; 压入 "/bin"(小端序) \x89\xe3 mov %esp, %ebx ; ebx 指向 "/bin//sh" 字符串 \x50 push %eax ; 压入 0(argv 结尾) \x53 push %ebx ; 压入字符串地址(argv[0]) \x89\xe1 mov %esp, %ecx ; ecx 指向 argv 数组 \xb0\x0b mov $0xb, %al ; 系统调用号 11 = execve \xcd\x80 int $0x80 ; 触发系统调用
为什么在确定缓冲区地址时不能输入一大坨东西?
AI说:这是因为我们需要得到的是程序在执行到 vulnerable 函数内部、还没开始读取用户输入时的栈地址,如果我们输入过多内容覆盖了原有栈帧,得到的地址已经是被污染后的结果,无法对应缓冲区的起始位置,会导致后续返回地址计算错误,最终攻击失败。
说了这一大堆,用人话说大概就是:你想用溢出的方式进行溢出攻击吗?不可能的。
5.学习感悟
本次完整完成三种缓冲区溢出攻击实验后,我对缓冲区溢出漏洞的原理和利用有了从书本理论到实践操作的具象理解:在此之前,我只在课堂上了解过缓冲区溢出的基本概念,知道它是因为缺乏边界检查导致的内存破坏漏洞,更多是明白水满则溢的抽象模型。不过通过一步步的亲手操作,我才发现不一定要暴力溢出;只要能精准覆盖返回地址,就能随意改变程序的执行流向,哪怕没有源码,也能利用漏洞拿到系统权限。
同时,这次实验也让我对漏洞防护的必要性有了更深的体会:ASLR作为基础的防护机制,就能大幅提升漏洞利用的难度;而gets这类本身存在安全风险的函数,也确实应该被更安全的接口替代。
作为网络空间安全方向的入门实验,本次BOF攻击实践帮我搭建了二进制漏洞利用的基础认知框架,也为后续学习更复杂的漏洞利用技术、防护机制打下了扎实的实践基础。

浙公网安备 33010602011771号