软件与系统安全大作业
Linux中缓冲区溢出漏洞实践实验:Shellcode与Return-to-libc
实验设计与背景实验背景
缓冲区Buffer指的是包含相同数据类型实例的一个连续的计算机显存块,是程序运行期间在显存中分配的一个连续的区域,用于保存包括字符链表在内的各类数据类型。溢出指的则是填充的数据超出了原有的缓冲区边界。缓冲区溢出指的是向固定宽度的缓冲区中写入超出其预先分配宽度的内容,导致缓冲区中数据的溢出,进而覆盖了缓冲区周围的显存空间,黑客可能会借助这个方式悉心构造填充数据,造成原有流程的改变,让程序执行特殊的代码从而进行各类非法操作,致使程序运行失败、系统宕机、重新启动等后果。
在各类安全报告中,缓冲区溢出漏洞仍然是其中很重要的一部份。这些漏洞很容易被功击者借助,由于C和C++等语言没有手动检查缓冲区溢出操作,并且开发人员很可能也没有注意到,可能给计算机系统带来巨大的恐吓。因而缓冲区溢出漏洞是一种十分普遍、非常危险的漏洞,在各类操作系统、应用软件中广泛存在。怎样避免和测量出借助缓冲区溢出漏洞进行的功击,就成为防御网路入侵以及入侵检查的重点之一。
本次实验是基于SeedLab实验进行设计的,分别进行了借助Shellcode的方式和Return-to-Libc的方式进行了缓冲区溢出漏洞功击。缓冲区溢出的常用方式是将储存在堆栈中的恶意代码Shellcode注入到程序中,并用其地址来覆盖程序本身函数调用的返回地址,致使返回时执行此恶意代码而不是原先本该执行的代码。
因此,操作系统作出了一系列的保护举措,例如地址空间随机化机制,非执行堆栈技术等。其中地址空间随机化机制在本实验中我们把其禁用,但是在功击成功后打开此机制,把功击结果做对比。对于非执行堆栈技术,它实际上是数据执行保护策略(DEP)的一种,这些策略可以控制程序对显存的访问方法即被保护的程序显存可以被约束为只能被写或则被执行,而不能同时执行,这些方案为程序运行时的显存访问提供了安全保护,当把堆栈设为不可执行,跳转到Shellcode都会造成程序失败。在Shellcode实验中,我们同样会关掉这些防护机制达到功击目的,而且这些保护方式不总是有效的,存在一种称为Return-to-libc的缓冲区溢出功击的变体,它不须要可执行堆栈,甚至不须要Shellcode,而是使易受功击的程序跳转到一些现有代码如Libc库中的system()函数,并通过这些方式获取Root权限。在前面的实验说明中linux telnetd密钥处理缓冲区溢出漏洞,还将开启保护机制,验证保护机制的疗效。
在下文1.3中会介绍本次实验的具体流程和难点。
实验环境
本实验在KaliLinuxx32位系统上进行,仿造SeedLab进行缓冲区溢出漏洞实验。
设计思路与难点
本实验将根据如下流程进行:
首先我们进行Shellcode实验,在关掉数据执行保护机制和地址空间随机化机制的条件下,在该平台中实现缓冲区溢出功击,并通过改变有漏洞程序的文件属主从而获取root权限。并在实验最后打开保护机制,运行一样的溢出功击代码,比较实验现象。
接出来进行Return-to-libc的功击实验,在不可执行堆栈的条件下借助漏洞程序发动功击并最终获取Root权限,并再深入学习Linux系统中的缓冲区溢出保护机制。
接出来的部份将这样安排:第2节对实验的细节进行详细的说明,第3节剖析实验结果以及一些实验细节的缘由,第4节介绍本实验可能用到的相关知识,第5节对实验做一个总结,并对缓冲区溢出漏洞防护方式做一个综述。
实验细节Shellcode缓冲区溢出功击实验
在实验开始前须要根据4.2中的形式关掉相应的安全机制。
首先我们须要构造一个有缓冲区溢出漏洞的程序。
从代码中可以看出程序从名为badfile的文件中读取输入,然后传到函数bof()中的另一个缓冲区,输入的最大宽度为517字节,并且bof()缓冲区只有12字节,因为strcpy()不检测边界,所以可能会发生缓冲区溢出。
在编译时,须要添加-fno-stack-protector和-zexecstack选项以关掉StackGuard和不可执行的堆栈保护,编译然后我们须要使之成为root拥有的set-uid程序才可以使普通用户获取root权限,
gcc-g-ostack-zexecstack-fno-stack-protectorstack.c
sudochownrootstack
sudochmod4755stack
这儿编译时添加-g主要是为了前面可以用gdb调试。
接出来我们须要构造badfile来使上述程序将内容复制到缓冲区来世成rootshell。
以下程序用于构造badfile内容
/*exploit.c*/
voidmain(intargc,char**argv)
charbuffer[517];
FILE*badfile;
//ox90代表NULL
memset(&buffer,0x90,517);
strcpy(buffer+100,shellcode);//将shellcode拷贝至buffer
strcpy(buffer+?,"");//在buffer特定偏斜处起始的四个字节覆盖shellcode地址
badfile=fopen("./badfile","w");
fwrite(buffer,517,1,badfile);
fclose(badfile);
这么这儿的问题就是怎样找到特定的偏斜地址构造四个字节?首先用gdbstack来调试一下,可以使用一下几个命令:
bmain//在main处打断点
r//run
p/x&str//查看str的地址
由上可知str的地址为0xbffff0d7,shellcode偏斜量为100即0x64,所以shellcode的地址为0xbffffxbffff1515b,即填充为x5bxf1xffxbf,接出来进行反汇编来查看bof函数:
这儿可以看见lea-0x14(%ebp),%edx,可知buffer储存在ebp-0x14的位置,即buffer举例ebp的距离为0x14linux操作系统下载,按照上述栈帧剖析可知returnaddress位置为0x14+4(十补码)=0x18。
所以我们可以把得到的值填充到源程序中,得到:
之后通过编译执行,我们可以获得root权限:
我们可以看见,在关掉所有防护对策以后linux 分区,功击可以成功。
接出来的Return-to-libc实验实际上是对上述实验的一个扩充,也就是在堆栈不可执行的情况下,借助系统函数进行缓冲区溢出漏洞功击,下边进行具体说明。
Return-to-libc实验
首先我们介绍一下Return-to-libc的功击原理,这些功击可以将漏洞函数返回到显存空间已有的动态库函数中。当我们晓得函数调用栈帧时的结构(4.1节有介绍),功击者可以借助栈中的内容施行功击,功击者才能通过缓冲区溢出改写返回地址为一个库函数的地址,但是此库函数执行时的参数也重新写入到栈中,这样函数调用时获取到的是功击者预设的参数值,并且结束后返回到库函数而不是main()。这样库函数就可以帮助功击者执行恶意行为,更复杂的功击可以通过调用链来完成。
在本实验中,首先我们关掉了地址空间随机化机制并建立了一个Makefile,为了便捷做接出来的实验。
其中retlib指的是编译retlib的过程并把它改为root持有的程序,其中编译时采用的是堆栈不可执行的方法,exploit指的是编译并执行exploit.c,shell指的是把/bin/sh添加到环境变量中以得到它的地址,mem0指的是关掉地址空间随机化,而mem2则是打开,prep和prep2分别对应把sh链接到zsh和dash上,clean则是重置所有设置。
接出来我们先建立一个易受功击的程序retlib.c和一个功击文件badfile。
我们可以很容易观察到上述程序存在缓冲区溢出漏洞,它首先从badfile文件中读取40字节的内容并输入到一个12字节的缓冲区buffer中,因为fread()函数没有检测边界,假如这个程序是一个root拥有的set-uid程序,这么普通用户就可以借助这个漏洞获取rootshell。所以接出来的任务就是怎样构造badfile文件,当漏洞程序复制文件内容到buffer中时,就可以获得rootshell。
在此功击中,我们须要跳转到一些现存的已被加载到显存的代码中,这儿使用Libc函数库中的system()函数和exit()函数,所以须要查找地址,这儿直接通过gdb调试技术,如图所示:
从上述命令中,我们可以见到在main函数处做了一个断点,之后运行r,程序运行到断点处,这样可以复印出system()和exit()的地址,可以得到:system()地址为0xbxbxb77ee11dd630630,exit()地址为0xbxb7310373103aa0。
本实验中我们须要跳转到system()函数并使用它执行/bin/sh程序,所以我们须要把/bin/sh装入到显存中并把它的地址传递给system()函数,这儿通过环境变量来实现,我们通过使用Makefile中的shell来定义MYSHELL这个环境变量,假如关掉了地址空间随机化机制,这么输出的地址是同一个。我们可以把输出地址的程序装入到retlib.c程序中进行输出:
这儿重新编译执行就可以得到/bin/sh置于显存中的地址:
可以看见地址为0xbffffxbffff590。
接出来我们须要创建badfile中的内容,我们这儿给出程序的截图并进行剖析:
这儿我们须要对上述截图中红框中的部份进行一个剖析,本实验的目的是使溢出的缓冲区覆盖bof函数的返回地址并使其指向system()函数,而且把/bin/sh作为system()的参数,最后执行exit()函数返回,所以我们可以通过gdb来剖析bof函数的返回值:
如图可以看见,buffer距离ebp的距离为0x14,因而依据栈帧结构,buffer距离ReturnAddress的距离为0x18,就是10补码中的24,所以我们须要把这个位置覆盖成system(),也就是上图中的buf[24]填充上为system()的地址,按照函数调用的过程,我们可以获知链表另外须要填充分别对应的值,对于函数栈帧结构在第4节进行了说明。
接出来,我们可以编译exploit.c程序生成badfile,并重新编译执行retlib.c,查看是否执行成功,
可以看见功击成功,早已获得了root权限。
实验剖析Shellcode实验剖析
对于Shellcode实验,假如我们重新打开防护举措。
首先,我使用严禁执行堆栈来编译stack.c,得到:
假如容许地址随机化:
也会提示段错误,所以可以见到Linux系统中的举措是可以防护缓冲区溢出漏洞功击的。
Return-to-libc实验剖析
假如我们容许地址随机化:
发觉形成了段错误,所以对于地址空间随机化机制,我们须要更多的时间和方式去猜想地址才可以功击成功。
我们注意到实验都是在/bin/sh指向zsh的情况下进行的,假如指向bash是不能获得root权限的,由于bash有权限增加的机制,尽管可以抵达shell,然而却不能获得root权限,所以我们可以在retlib.c的函数中添加setuid(0)一行来提高权限再调用system()函数。
另外,exit()函数在本实验中的作用是哪些,我们在exploit.c程序中注释掉这一行,
可以发觉,程序深陷了崩溃。
查阅资料剖析发觉,exit()在system(0执行后执行,它的地址是system()的返回地址,假如没有,这么哪个位置可能是任何值,自然没办法正常进行,只有正常退出,能够跳转到执行shell的位置。
相关知识C栈帧结构
在了解C栈帧结构之前,先来看一下计算机显存的基础结构。
其中,stack代表栈,储存代码中的局部变量,不包括static变量。heap代表堆,储存程序运行期间动态分配的空间,通常为malloc/realloc函数。BSSSegment代表BlockStartedbySymbol,储存未被初始化的全局变量或则static变量,有默认值0。DataSegment代表数据段储存已初始化的全局变量或则static变量。TextSegment代表代码段,用于储存指令、运行代码的一块显存空间,这其中通常会包含一些只读的常数变量例如字符串常量。
说到栈帧结构就必需要提到Function函数,例如这样一个函数。
voidfunc(inta,intb){
intx,y;
x=a+b;
y=a-b;
当调用func时,这种数据会被压入到stack中,HighAddress代表栈底,a和b是方式参数,先被压入栈中。数组根据从右到左的次序压栈。接出来压入ReturnAddress和PreviousFramePointer。ReturnAddress拿来储存调用后的下一条指令的地址,便于执行结束后返回到这个位置。从栈底到PreviousFramePointer这部份在函数调用中是固定的,挨在一起,并且与下边的局部变量之间可能会存在一个操作系统形成的gap,因而地址可能不连在一起。
对于这个程序,x=a+b;在程序中,想要把a、b相乘得到x,在操作系统中须要转换成汇编语言代码(最终为机器语言)来执行,必需要晓得a和b的地址。我们可以使用偏斜量offset来确定地址,假设一个基址base,并且在运行这个程序的时侯我们并不晓得stack从那里开始,因而不可以把stack开始的SP(StackPoint)作为base。这儿在运行的时侯规定了一个寄存器ebp,在运行过程中这个值可以确定,指向PreviousFramePointer。
假设运行环境为32bit,这么可以得到赋值地址:a=ebp+8,b=ebp+12,同理可以晓得x的地址,如果紧挨到ebp的话,x=ebp-4,而且这儿实际上存在一个gap,编译器晓得gap的值,因而也可以晓得x和y的值。
上述程序关键部份对应的汇编代码:
movl12(%ebp),%eax//把ebp+12复制到eax也就是b中
movl8(%ebp),%edx
addl%edx,%eax//相乘,值会存在eax中
movl%eax,-8(%ebp)//eax联通到ebp-12中
函数调用显存的三个区域,代码区、静态数据区、动态数据区(压栈和清栈就是在这个区域完成的)。
CPU中有三个寄存器,分别是eip、ebp和esp。eip永远指向代码区少将要执行的下一条指令,执行方法包括次序执行和跳转;ebp和esp用于管理栈空间,ebp指向栈底,esp指向栈顶,代码区中的函数调用、返回和执行都伴随着不断的压栈和清栈,在调用函数时,ebp会指向PreviousFramePointer以在执行函数然后返回到原先的地址。
在缓冲区溢出功击中,功击者可以通过缓冲区溢出改写返回地址为其他可以借助的函数地址,这样就可以帮助功击者实现恶意行为。
Linux系统针对缓冲区溢出功击的安全机制
为了简化功击,本实验少将先禁用它们,并在旁边一一启动观察功击能够成功。
1、地址空间随机化:大部份Linux系统都使用地址空间随机化来随机堆和栈的起始地址,这促使猜想准确的地址显得困难,而推测地址是缓冲区溢出功击的关键步骤之一。本实验中,可以通过如下命令禁用此功能:
$sudosysctl-wkernel.randomize_va_space=0
2、StackGuard保护方案:GCC编译器实现了一种称为StackGuard的安全机制,以避免缓冲区溢出。在这些保护的情况下,缓冲区溢出功击将不起作用。在编译期间可以使用-fno-stack-protector选项来禁用此保护,例如说在编译的时侯这样执行:
$gcc-fno-stack-protectorexample.c
3、不可执行的堆栈:程序和二补码映像必须申明它们是否须要可执行堆栈,即它们须要在程序焦段中标记一个数组。内核或动态链接器使用此标记来决定该程序是堆栈可执行还是不可执行的。默认情况下堆栈是不可执行的。可以通过编译时添加选项来修改:
对于堆栈可执行的程序:$gcc-zexecstack-otesttest.c
对于堆栈不可执行的程序:$gcc-znonexecstack-otesttest.c
对于Shellcode实验,我们将使用堆栈可执行形式进行编译,而且对于Return-to-libc实验则不须要,由于这些方法不须要执行堆栈即可完成功击,。
配置/bin/sh:Linux中的dash存在一个对策,可以避免自身在set-uid程序中运行,假如dash检查到时在set-uid执行都会把有效用户id转换成该进程的真实用户id,这样也就清除了特权,因而在本实验中通过把/bin/sh链接到另一个没有这些对策的shell程序中:
$sudorm/bin/sh
$sudoln-s/bin/zsh/bin/sh
本实验代码说明
本实验报告中大部份代码以截图方式呈现,具体的程序在附表中提供。
实验总结
从本次实验中,我们可以看出,对于缓冲区溢出漏洞功击的防御,一方面须要程序员使用才能防御缓冲区溢出的函数,当心功击的发生,另一方面,系统也为我们提供了许多防御机制,例如数据执行保护机制DEP和地址空间随机化机制ASLR。通过本次实验,我们看见DEP依然可以被绕开,因而目前大力发展ASLR是针对缓冲区溢出漏洞的趋势,目前也有一些论文针对这些防御机制作了剖析与功击,我们应当通过提升这些机制的复杂度达到更高的安全。
在本实验中我遇见了好多问题linux telnetd密钥处理缓冲区溢出漏洞,首先是KaliLinux操作系统上有好多的安装工具与其他系统不同,我重新安装了gdb,并且有好多操作习惯的问题,而且在实验过程中进一步熟悉了系统的使用方式;另外我在实验过程小学习了部份汇编的内容,尤其是在gdb调试过程中进一步加深了对栈帧结构的认识,对于之后在Linux系统中开发调试c语言程序奠定了良好的基础。最重要的一点是,我认识到了缓冲区溢出漏洞对系统影响的严重程度,也认识到了我们常常用到的Linux系统中采取的避免缓冲区溢出的举措,这种举措都是十分有效的,并且更重要的是程序员自身应当注意处理这方面的代码,避免被恶意用户借助,并且还要进一步提升安全举措避免黑客功击。