SST-Lab3:Buffer Overflow
软件安全测试第三次实验
姓名:secret! 班级:secret! 学号:secret!
1 实验概况
实验教材:Buffer_Overflow_Setuid.pdf (seedsecuritylabs.org)
实验环境:Ubuntu 20.04
2 Setup
关闭地址空间随机化(ASLR)这个针对缓冲区溢出攻击的防御措施。ASLR对程序内存中的一些关键数据区域进行随机化,包括栈的位置、堆和库的位置等,目的是让攻击者难以猜测到所注入的恶意代码在内存中的具体位置。
为了使实验成功,需要使用一个没有实现保护机制的shell,只要把/bin/sh指向这个叫做zsh的shell程序
3 Task1:熟悉shellcode
3.1 shellcode.c
通过execve()系统调用执行一个shell程序
不能编译c代码以及生成的二进制文件用于shellcode攻击,原因如下:
加载器问题:缓冲区溢出攻击中恶意代码不由操作系统加载,而是直接通过内存复制载入的,因此初始化步骤缺失导致无法运行这个shell程序
代码中的0:字符串复制例如strcpy()遇到\0停止,发现这个c代码编译为二进制代码后至少会出现三处0:
字符串“/bin/sh”末尾
程序有两个NULL
name[0]中的0是否转化为二进制中的0取决于编译环境
由于上述问题,不能使用由C语言程序生成的二进制代码作为恶意代码,需要直接用汇编语言来写,shellcode最核心的部分是使用execve()系统调用来执行“/bin/sh”,这个系统调用需要设置以下4个寄存器:
eax寄存器:保存11,这是execve()的系统调用号
ebx寄存器:保存命令字符串的地址(如“/bin/sh”)
ecx寄存器:保存参数数组的地址
edx寄存器:保存新程序的环境变量的地址,可设为0表示不传递任何环境变量
解读shellcode代码步骤(以32位为例):
第一步:找到“/bin/sh”字符串在内存中的地址并设置ebx。为了找到“/bin/sh”的地址,需要把这个字符串压入栈中。由于一次只能压入4字节,故把字符串分成3份一次压入。最后movl %esp %ebx将字符串地址保存到ebx寄存器中。后续实验中调试可以通过打印ebx的值获知shell关键恶意代码的位置
第二步:找到name数组的地址并设置ecx。Name数组由2个元素:第一个存放”/bin/sh”,第二个放空指针。已知ebx保存了字符串“b/bin/sh”的地址,也就是name数组首地址。最后指令movl %esp %ecx表示ecx寄存器保存着name数组的首地址
第三步:将edx设为0。用异或方法清空edx寄存器
第四步:调用execve()系统调用。需要两条指令:将系统调用到11保存到eax;中断将系统切换到内核
实验中先设置好环境,然后尝试编译运行观察可见顺利调用到/bin/sh
4 Task2:理解stack.c
4.1解释Stack.c
上述程序存在缓冲区溢出漏洞。它首先从一个名为badfile的文件中读取一个输入,然后将这个输入传递到函数bof()中的另一个缓冲区。原始输入的最大长度可以为517字节,但bof()中的缓冲区只有BUF SIZE字节长,小于517字节。strcpy()不检查边界,就会发生缓冲区溢出。由于这个程序是一个root拥有的Set-UID程序,如果一个普通用户可以利用这个缓冲区溢出漏洞,用户可能能够获得一个root shell。需要注意的是,该程序从一个名为badfile的文件中获取其输入。这个文件在用户的控制之下。现在,我们的目标是为badfile创建内容,这样当易受攻击的程序将内容复制到它的缓冲区时,就可以生成一个root shell。
4.2编译
编译。要编译上述易受攻击的程序,不要忘记使用-fno-stack-protector和“-z执行堆栈”选项关闭StackGuard和不可执行的堆栈保护。编译后,我们需要使程序成为root拥有的Set-UID程序。我们可以通过首先将程序的所有权更改为root,然后将权限更改为4755以启用Set-UID位来实现这一点。需要注意的是,更改所有权必须在打开Set-UID位之前完成,因为所有权更改会导致Set-UID位被关闭。
编译和设置命令已经包含在Makefile中,所以我们只需要键入make来执行这些命令。变量L1、…、L4在Makefile中设置;它们将在编译期间使用。
5 Task3:攻击32位程序(Level1)
5.1 gdb调试
要利用目标程序中的缓冲区溢出漏洞,最需要知道的是缓冲区的起始位置和存储返回地址的地方之间的距离。我们将使用一种调试方法来找出它。由于我们有目标程序的源代码,我们可以在调试标志打开的情况下编译它。那将使调试更加方便。我们将在gcc命令中添加-g标志,因此调试信息被添加到二进制文件中。如果运行make,则调试版本已经创建。我们将使用gdb来调试stack-L1-dbg。我们需要在运行程序之前创建一个名为badfile的文件
注1、当gdb在bof()函数内部停止时,它在ebp寄存器设置为指向当前堆栈帧之前就停止了,所以如果我们在这里打印出ebp的值,我们就会得到调用者的ebp值。我们需要使用next来执行几条指令,并在ebp寄存器修改为指向bof()函数的堆栈帧后停止。
注2、需要注意的是,从gdb获得的帧指针值与实际执行时(不使用gdb)不同。这是因为gdb在运行调试程序之前,已经将一些环境数据推送到堆栈中。当程序不使用gdb直接运行时,堆栈没有这些数据,所以实际的帧指针值会更大。
gdb调试代码解释:
b bof : 在bof()函数设置断点
run: 在gdb模式下run
next : 执行下一句(进行strcpy(buffer,str))
p $ebp: 获取调用函数栈帧基地址
p &buffer: 获取buffer地址
5.2 发起攻击 exploit.py
实验要求:要利用目标程序中的缓冲区溢出漏洞,我们需要准备一个有效负载,并将其保存在badfile中。我们将使用Python程序来做到这一点。我们提供了一个名为exploit.py的骨架程序,该程序包含在实验室设置文件中。代码不完整,学生需要替换代码中的一些基本值。
代码解释:
栈溢出漏洞导致数据溢出到相邻的内存区域。buffer是一个数组,用于存储恶意文件的内容。通过在payload中溢出buffer的边界,我们可以覆盖控制流中的返回地址。
shellcode存放恶意代码,32位的汇编版本
L20创建一个长度为517个字节的content数组,并用0x90(NOP)填充整个数组,
L25表示把恶意代码放在该数组的尾部
Ret=ebp+100返回地址
Offset=ebp+4-buffer偏移量
L等于4是32位程序
Ret:Ebp+4是返回地址地址;ebp+8是第一条NOP指令地址,NOP指令什么也不做,只告诉CPU往前走,因此只要猜中任意一个NOP指令地址就可以一直往前走,最终走到恶意代码的真正注入点(猜测注入代码的准确入口地址ret)又因为调试比运行栈帧更深,用ebp+100,写入返回地址字段中
Offset:返回地址字段在输入数据中处于那个位置?输入将被复制到buffer,为了让输入中的返回地址字段准确覆盖栈中的返回地址字段区域,需要知道栈中buffer和返回地址区域之间的距离,这个距离就是返回地址字段在输入数据中的位置,offset=返回地址-buffer=(ebp+4)-buffer=112
完成上述程序后,运行它。这将生成badfile的内容。然后运行易受攻击的程序堆栈。编译运行成功获得root
6 Task4:攻击时不知道缓冲区大小(Level2)
在这个任务下,我们不知道 buffer 的大小,只知道是 100 到 200。但是,我们还是可以使用 gdb 得到 &buffer 的地址,只是不能得到 $ebp 的地址罢了。我们先把 ret 的值修改为 &buffer,然后把函数每个可能的 ret 位置都修改为我们的 ret,并且将 start 设为517 - len(shellcode)。
用返回地址进行范围覆盖,注意ret要在覆盖范围之外,确保跳转到设置的返回地址。
Offset从112开始因为由上一题的offset计算出
完整代码解释:
这段代码是一个简单的漏洞利用脚本,用于生成一个恶意文件。
首先,脚本导入了sys模块,这是Python的标准库,用于与Python解释器进行交互。
接下来,定义了一个变量shellcode,它包含了一个恶意代码的字节表示。这段代码是一个shellcode,用于执行特定的操作,例如获取系统权限或执行恶意操作。在这个例子中,shellcode是一个简单的Linux/x86的反弹shell。
然后,定义了一个名为content的字节数组,用于存储恶意文件的内容。这个数组的长度是517字节,填充了NOP指令(0x90)。
在注释下方的代码块中,将shellcode插入到payload中的某个位置。首先,计算了shellcode应该插入的起始位置start,然后将shellcode的字节复制到content数组中的相应位置。
接下来,定义了一个返回地址ret,它是一个指向payload中某个位置的地址。这个地址将决定程序执行完shellcode后的下一条指令的位置。在这个例子中,返回地址是一个固定的值。
然后,使用一个循环将返回地址的字节复制到payload中的多个位置。这些位置是通过offset变量计算得出的,它的范围是从112到212,每次增加4个字节。这些位置将被用作程序执行完shellcode后的返回地址。
最后,将content数组的内容写入一个名为badfile的二进制文件中。
总结来说,这段代码的目的是生成一个恶意文件,其中包含了一个shellcode和一些返回地址。当这个文件被执行时,shellcode将被执行,并且程序将跳转到返回地址指定的位置。
7 Task5:攻击64位程序(Level3)
由于64位下地址最高位2个字节总是0,就导致strcpy的时候遇到0停止,复制到栈中的时候内容不全。
具体而言,一个64 位地址0x00007FFFFFFFFFFF 也就是地址中有 0,在执行 strcpy(buffer, str); 的时候遇到 0 就停止了,0后面的内容不会被拷贝。地址还是小端序,ret 的低位非 0 会先拷贝,高位 0 不拷贝。这也就意味着 shellcode 不能放在返回地址的后面,只能放在返回地址的前面,所以调整start和ret
offset同样方法计算=返回地址-buffer=(rbp+8)-buffer
start的值被设置为96,是为了将shellcode放置在payload的适当位置。在这个例子中,start的值是根据实际情况进行调整的,以确保shellcode被正确放置在payload中,并且不会覆盖其他重要的数据。
Ret:由于shellcode 不能放在返回地址的后面,只能放在返回地址的前面,所以是返回地址-64是减号
8 攻击64位程序(Level4)
不知道缓冲区大小,实际缓冲区大小很小,所以不能使用task5的方法,否则把返回地址也一并覆盖了。
可以修改返回地址,跳转到str上的shellcode而不是使用buffer中的。
在gdb调试中查看rbp、buffer和str的地址
Start的值使得shellcode放在后面容错率高一些,ret适当偏移量,offset是str到返回区域的距离
经过实验可见成功拿到root权限
9 Task7:击败dash的对策
在 ubuntu 中, dash shell 检测到有效的 UID 和真实 UID 不相等就会放弃特权。前面我们将 sh 链接到 zsh 来解决的这个问题,现在我们来尝试新的对策。首先链接回原来的
在调用 execve() 之前将真实 ID 修改为 0 即可,也就是调用 setuid(0)。我们把 call_shellcode.c 中注释掉的二进制代码加入到 shellcode 的开头就行了。
可见添加setuid(0)前uid是用户,但是添加后重新编译运行能够拿到root权限
重复level1,可见在sh链接到 dash 的情况下我们拿到了 root 权限:
10 Task8:击败地址随机化
输入命令使得栈堆地址随机化,攻击者无法猜测固定地址注入点,实验重新运行level1可见不成功
突破32位计算机的栈随机化,通过暴力的方式不断重复发起缓冲区溢出攻击,希望碰巧猜对内存地址
输入命令sh ./addr_random.sh,经实验用了57秒猜对地址,成功拿到root权限
11 Task9:尝试其他对策
Stackgurad:在返回地址和缓冲区之间设置一个哨兵,用这个哨兵来检测返回地址是否被修改。当缓冲区溢出攻击修改返回地址时,所有处于缓冲区和返回地址之间的内存值也会被修改。
重新编译去除-fno-stack-protector,取消关闭stackguard选项,将badfile作为恶意输入,gcc检查到缓冲区溢出输出stack smashing
去除 -z execstack(程序的栈可执行的),编译 call_shellcode.c 并运行,可以发现栈不再执行。
不可执行堆栈只是使得无法在堆栈上运行shellcode,但并不能防止缓冲区溢出攻击,因为在利用缓冲区溢出漏洞后,还有其他方法可以运行恶意代码