缓冲区溢出的保护机制
缓冲区溢出的保护机制
缓冲区溢出原理
缓冲区是内存中存放数据的地方。在程序试图将数据放到机器内存中的某一个位置的时候,因为没有足够的空间就会发生缓冲区溢出。而人为的溢出则是有一定企图的,攻击者写一个超过缓冲区长度的字符串,植入到缓冲区,然后再向一个有限空间的缓冲区中植入超长的字符串,这时可能会出现两个结果:一是过长的字符串覆盖了相邻的存储单元,引起程序运行失败,严重的可导致系统崩溃;另一个结果就是利用这种漏洞可以执行任意指令,甚至可以取得系统root特级权限。
缓冲区是程序运行的时候机器内存中的一个连续块,它保存了给定类型的数据,随着动态分配变量会出现问题。大多时为了不占用太多的内存,一个有动态分配变量的程序在程序运行时才决定给它们分配多少内存。如果程序在动态分配缓冲区放入超长的数据,它就会溢出了。一个缓冲区溢出程序使用这个溢出的数据将汇编语言代码放到机器的内存里,通常是产生root权限的地方。仅仅单个的缓冲区溢出并不是问题的根本所在。但如果溢出送到能够以root权限运行命令的区域,一旦运行这些命令,那可就等于把机器拱手相让了。
造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。例如下面程序:
void func1(char *input)
{
char buffer[16];
strcpy(buffer, input);
}
上面的strcpy()将直接吧input中的内容copy到buffer中。这样只要input的长度大于16,就会造成buffer的溢出,使程序运行出错。存在像strcpy这样的问题的标准函数还有strcat(),sprintf(),vsprintf(),gets(),scanf(),以及在循环内的getc(),fgetc(),getchar()等。
当然,随便往缓冲区中填东西造成它溢出一般只会出现Segmentation fault 错误,而不能达到攻击的目的。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其他命令。如果该程序属于root且有suid权限的话,攻击者就获得了一个有root权限的shell,便可以对系统进行任意操作了。
请注意,如果没有特别说明,下面的内容都假设用户使用的平台为基于Intel x86 CPU的Linux系统。对其他平台来说,本文的概念同样适用,但程序要做相应修改。
CANNARY(栈保护)
这个选项表示栈保护功能有没有开启。
栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈里插入cookie信息,当函数真正返回的时候会验证cookie信息是否合法,如果不合法就停止程序运行。攻击者在覆盖返回地址的时候往往也会将cookie信息给覆盖掉,导致栈保护检查失败而阻止shellcode的执行。在Linux中我们将cookie信息称为canary。
gcc在4.2版本中添加了-fstack-protector和-fstack-protector-all编译参数以支持栈保护功能,4.9新增了-fstack-protector-strong编译参数让保护的范围更广。
因此在编译时可以控制是否开启栈保护以及程度,例如:
gcc -fno-stack-protector -o test test.c //禁用栈保护
gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码
FORTIFY
这个保护机制查了很久都没有个很好的汉语形容,根据我的理解它其实和栈保护都是gcc的新的为了增强保护的一种机制,防止缓冲区溢出攻击。由于并不是太常见,也没有太多的了解。
举个例子可能简单明了一些:
一段简单的存在缓冲区溢出的C代码
void fun(char *s) {
char buf[0x100];
strcpy(buf, s);
/* Don't allow gcc to optimise away the buf */
asm volatile("" :: "m" (buf));
}
用包含参数-U_FORTIFY_SOURCE编译
08048450 <fun>:
push %ebp ;
mov %esp,%ebp
sub $0x118,%esp ; 将0x118存储到栈上
mov 0x8(%ebp),%eax ; 将目标参数载入eax
mov %eax,0x4(%esp) ; 保存目标参数
lea -0x108(%ebp),%eax ; 数组buf
mov %eax,(%esp) ; 保存
call 8048320 <strcpy@plt>
leave ;
ret
用包含参数-D_FORTIFY_SOURCE=2编译
08048470 <fun>:
push %ebp ;
mov %esp,%ebp
sub $0x118,%esp ;
movl $0x100,0x8(%esp) ; 把0x100当作目标参数保存
mov 0x8(%ebp),%eax ;
mov %eax,0x4(%esp) ;
lea -0x108(%ebp),%eax ;
mov %eax,(%esp) ;
call 8048370 <__strcpy_chk@plt>
leave ;
ret
我们可以看到gcc生成了一些附加代码,通过对数组大小的判断替换strcpy, memcpy, memset等函数名,达到防止缓冲区溢出的作用。
NX(DEP)
NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。
gcc编译器默认开启了NX选项,如果需要关闭NX选项,可以给gcc编译器添加-z execstack参数。
例如:
gcc -z execstack -o test test.c
在Windows下,类似的概念为DEP(数据执行保护),在最新版的Visual Studio中默认开启了DEP编译选项。
PIE(ASLR)
一般情况下NX(Windows平台上称其为DEP)和地址空间分布随机化(ASLR)会同时工作。
内存地址随机化机制(address space layout randomization),有以下三种情况
0 - 表示关闭进程地址空间随机化。
1 - 表示将mmap的基址,stack和vdso页面随机化。
2 - 表示在1的基础上增加栈(heap)的随机化。
可以防范基于Ret2libc方式的针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码。
Built as PIE:位置独立的可执行区域(position-independent executables)。这样使得在利用缓冲溢出和移动操作系统中存在的其他内存崩溃缺陷时采用面向返回的编程(return-oriented programming)方法变得难得多。
liunx下关闭PIE的命令如下:
sudo -s echo 0 > /proc/sys/kernel/randomize_va_space
ASLR效果
1、映像随机化
映像随机化是在PE文件映射到内存时,对其加载的虚拟地址进行随机化处理,这个地址是在系统启动时确定的,系统重启后这个地址会有变化。
2、堆栈随机化
程序运行时随机选择堆栈的基址,与映像随机化不同的是堆栈的基址不是在系统启动时确定的,而是在程序打开时确定的,也就是说同一个程序任意两次运行时的堆栈基址都不相同。
3、PEB与TEB随机化
微软在XP SP2之后不再使用固定的PEB基址0x7FFDF000和TEB基址0x7FFDE000,而是使用具有一定随机性的基址。
TEB存放在FS:0和FS:[0x18]处,PEB存放在TEB偏移0x30的位置
RELRO
设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。
检测工具checksec
checksec是一个脚本软件,也就是用脚本写的一个文件,不到2000行,可用来学习shell。
源码参见
http://www.trapkit.de/tools/checksec.html
https://github.com/slimm609/checksec.sh/
下载方法之一为
wget https://github.com/slimm609/checksec.sh/archive/1.6.tar.gz
checksec到底是用来干什么的?
它是用来检查可执行文件属性,例如PIE, RELRO, PaX, Canaries, ASLR, Fortify Source等等属性。
一般来说,如果是学习二进制漏洞利用的朋友,建议大家使用gdb里peda插件里自带的checksec功能,如下:
gdb-peda$ checksec start
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : disabled