缓存区与缓存区溢出
- 缓冲区的定义
连续的一段存储空间。 - 缓冲区溢出的定义
指写入缓冲区的数据量超过该缓冲区能容纳的最大限度,造成溢出的数据改写了与该缓冲区相邻的原始数据的情形。 - 缓冲区溢出的危害
利用缓冲区溢出实现在本地或者远程系统上实现任意执行代码的目的,从而进一步达到对被攻击系统的完全掌控;
利用缓冲区溢出进行DoS(Denial of Service)攻击;
利用缓冲区溢出破坏关键数据,使系统的稳定性和有效性受到不同程度的影响;
4.缓冲区溢出的根本原因:主要是因为分配内存的时候没有检测缓冲区的边界,当分配的内存超过缓冲区容纳的最大的限度时,就会发生溢出,导致溢出的函数有:Strcpy、Strcat、Gets、Sprintf等,这些函数都没有检测数值边界。
缓存区攻击模式
攻击者主要是利用溢出来覆盖某些空闲块头部指针,进而进行重定向到恶意程序,进而进行攻击。恶意数据可以通过命令行参数、环境变量、输入文件或者网络数据注入。
进程在内存中的布局
1.代码段:放置程序的可执行代码
2.数据段:放置初始化的全局变量和初始化的局部静态变量
3.BBS段:放置未初始化的全局变量和未初始化的局部静态变量
4.堆:可以进行内存的动态分配
5.堆栈:用于存放函数中的局部变量、函数参数、返回地址、调用函数的栈基址。
6.进程的环境变量和参赛
缓冲区溢出的位置
1.堆栈
2.堆
3.数据段
4.BBS段
堆栈溢出
函数调用过程及溢出原理
1.参数入栈
2.保存指令寄存器中的内容,作为返回地址
3.当前的EBP入栈
4.把ESP指针中的值拷到EBP中
5.为本地变量留出一定的空间,把ESP减出适当的值
那么调用函数前入栈的参数有:传给函数的参数、函数的局部变量等
调用后:恢复EBP、恢复eip、局部变量不变
溢出原理:
1.通过缓冲区溢出修改栈中的返回地址
2.当函数调用返回时,eip的地址为修改后的地址,并执行shellcode
堆栈的缓冲区溢出
溢出实例如下:
实例1:
void function(char *str)
{
char buffer[4];
strcpy(buffer, str);
}
void main (int argc, char **argv)
{
char large_string[8];
int i;
for(i=0; i<8; i++)
//large_string的长度为8
large_string[i]=‘A’
//下面函数内部的变量数组的长度为4,发生溢出
function(large_string);
}
实例2:
char shellcode[]=
"\x41\x41\x41\x41\x41\x41\x41\x41"//填充buffer的8字节;
"\x41\x41\x41\x41"//覆盖掉EBP
"\x12\x45\xfa\x7f"//覆盖返回后的EIP,windows中JMP ESP通用地址,ESP即指向下代码:
"\x33\xC0\x50\xC6\x04\x24\x6C\xC6\x44\x24\x01\x6C\x68"
"\x52\x54\x2E\x44\x68\x4D\x53\x56\x43\x8B\xC4\x50\xB8"
"\x77\x1D\x80\x7C" //LoadLibraryA()
"\xFF\xD0\x33\xC0\x50\xC6\x04\x24\x63\xC6\x44\x24\x01"
"\x6F\xC6\x44\x24\x02\x6D\x68\x61\x6E\x64\x2E\x68\x63"
"\x6F\x6D\x6D\x8B\xC4\x50\xB8"
"\xC7\x93\xBF\x77" //system的地址
"\xFF\xD0";
void main()
{
char buffer[8]={0};
unsigned int i;
strcpy(buffer,shellcode);
}
堆溢出
堆溢出实例代码:
void main(int argc, char **argv)
{
char *buf1 = (char *) malloc(16);
char *buf2 = (char *) malloc(16);
strcpy(buf1,”AAAAAAAAAAAAAAA”);
strcpy(buf2, argv[1]);
}
当在控制端输入的字符串长度大于16时,则会发生溢出,正常情况下和溢出情况下的示意图如下:
堆溢出分析
-
同LINUX一样,Windows的HEAP区是程序动态分配一块内存区域,动态分配和释放对象,用于事先不知道程序所需对象数量和大小或者对象太大不适合堆栈分配的情况。
-
程序员一般调用C函数malloc/free或者C++的new/delete或者WIN32
API函数HeapAlloc/HeapFree来动态分配内存,这些函数最终都将调用ntdll.dll中的RtlAllocateHeap/
RtlFreeHeap来进行实际的内存分配工作,所以只需要分析RtlAllocateHeap/RtlFreeHeap。 -
当分配的堆在存储数据时超过了所分配的内存空间,就会产生堆溢出。
-
对于一个进程来说可以有多个HEAP区,每一个HEAP的首地址以句柄来表示:hHeap,这也就是RtlAllocateHeap的第一个参数。
每个HEAP区的整体结构如下:
-
heap总体管理结构区存放着一些用于HEAP总体管理的结构。
-
双指针区存放着一些成对出现的指针,用于定位分配内存以及释放内存的位置。
-
用户分配内存区是用户动态分配内存时实际用到区域,也这是HEAP的主体。
-
当调用RtlAllocateHeap(HANDLE hHeap,DWORD dwFlags,SIZE_T
dwBytes)来分配内存时将进行以下操作:
1.对参数进行检查,如果dwBytes过大或小于0都按照出错处理,根据dwFlags来设置一些管理结 构;检查是否为DEBUG程序,对于DEBUG的程序与实际运行的程序每个内存块之间的结构是不同的 。
2.根据要分配的内存的大小(dwBytes)决定不同的内存分配算法,我们只分析小于1024 bytes的情况;
3.从双指针区找到用户内存区的末尾位置,如果有足够的空间分配所需的内存,就在末尾+dwBytes+8的位置放置一对指针来指向双指针区的指向用户内存区末尾位置的地方;
4.在后面同时设置双指针区的指向用户内存区末尾位置的指针指向进行完分配之后的用户内存区末尾位置。 -
两块连续分配的内存块之间并不是紧挨着的,而是有8字节的管理结构,最末尾的一块内存后面还另外多了8字节的指针指向双指针区。
假设有以下程序:
buf1 = HeapAlloc(hHeap, 0, 16);
buf2 = HeapAlloc(hHeap, 0, 16);
连续分配了两块16字节内存,实际在内存中(用户分配区)的情况是这样的:
-
在第二次分配内存的时候会利用第一块内存管理结构后面那两个指针进行一些操作,其中会有一次写内存的操作:
mov [ecx], eax
mov [eax+4], ecx
-
假设分配完buf1之后向其中拷贝内容,拷贝的内容大小超过buf1的大小,即大于16字节,就会发生溢出,当覆盖掉了那两个4字节的指针,而下一次分配buf2之前又没有把buf1释放掉的话,那么就会把一个4字节的内容写入一个地址当中,而这个内容和地址都是能够控制的,这样我们就可以控制函数的流程转向的shellcode了。
数据段的缓冲区溢出
下面是一个导致数据段溢出的程序,在该程序中可以看到程序对buf数组进行了不合理的内存分配导致该数组越界。
void Overflow_Data(char* input)
{
static char buf[4]=”CCCC”;
int i;
for (i = 0; i < 12 ; i++)
buf[i] = ‘A’;
}
溢出攻击实例
栈溢出的攻击实例
在分析栈溢出的攻击实例前,先分析一下Win32对废弃栈的处理,对废弃栈的处理一共含有三种模式。
Win32对废弃栈的处理
NSR模式
在下图中,R指向了Shellcode地址, 但执行“mov esp,ebp”恢复调用者栈信息时,Win32会在被废弃的栈中填入一些随机数据。
该模式下的攻击代码有:
#include<stdio.h>
int main(int argc,char **argv)
{
char buf[500];
strcpy(buf,argv[1]);
printf("buf 0x%8x\n",&buf);
getchar();
return 0;
}
NRS模式
当函数执行返回指令时,程序跳转到shellcode所在的地方,栈在1G(~0x00FFFFFF)以下
如果R直接指向Shellcode,则在R中必然含有空字节‘\0’. Shellcode将被截断。原理图如下:
攻击的示例代码如下:
R.S模式
该溢出模式主要是使用环境变量来发生溢出,进而执行shellcode,Win32平台无SUID机制,本地溢出没有意义,同样会由于R中含空字节会被截断,原理图如下:
堆栈缓冲区溢出的危害
- 改写返回地址
- 改写调用函数栈的栈帧地址
- 改写函数指针
- 改写虚函数指针
- 改写异常处理指针
- 改写数据指针
基于堆的缓冲区溢出
- 在Linux中,堆空间按照Doug Lea算法实现动态分配。
- 在C程序中,标准库函数malloc()/free()用于从堆中动态申请/释放块;对于C++程序,相应函数为new/delete。
缓冲区溢出攻击的防御技术
类型安全的编程语言
Java, C#, Visual Basic,Pascal, Ada, Lisp, ML属于类型安全的编程语言。
相对安全的函数库
例如在使用C的标准库函数时,做如下替换
strcpy -> strncpy
strcat -> strncat
gets -> fgets
缺点
使用不当仍然会造成缓冲区溢出问题。
修改的编译器
1.增强边界检查能力的C/C++编译器:
例如针对GNU C编译器扩展数组和指针的边界检查。Windows Visual C++ .NET的GS 选项也提供动态检测缓冲区溢出的能力。
2.返回地址的完整性保护
将堆栈上的返回地址备份到另一个内存空间;在函数执行返回指令前,将备份的返回地址重新写回堆栈。
许多高性能超标量微处理器具有一个返回地址栈,用于指令分支预测。返回地址栈保存了返回地址的备份,可用于返回地址的完整性保护
3.缺点:
性能代价
检查方法仍不完善
内核补丁
1.将堆栈标志为不可执行来阻止缓冲区溢出攻击;
2.将堆或者数据段标志为不可执行。
3.例如Linux的内核补丁Openwall 、 RSX、 kNoX、ExecShield和PaX等实现了不可执行堆栈,并且RSX、 kNoX、ExecShield、PaX还支持不可执行堆。另外,为了抵制return-to-libc这类的攻击,PaX增加了一个特性,将函数库映射到随机的内存空间
4.缺点:对于一些需要堆栈/堆/数据段为可执行状态的应用程序不合适;需要重新编译原来的程序,如果没有源代码,就不能获得这种保护
静态分析方法
字典检查法
1.遍历源程序查找其中使用到的不安全的库函数和系统调用。
2.例如静态分析工具ITS4、RATS (Rough Auditing Tool for Security)等。其中RATS提供对C, C++, Perl, PHP以及Python语言的扫描检测。
3.缺点:误报率很高,需要配合大量的人工检查工作
程序注解法
1.包括缓冲区的大小,指针是否可以为空,输入的有效约定等等。
2.例如静态分析工具LCLINT、SPLINT (Secure Programming Lint)。
3.缺点:依赖注释的质量
动态检测方法
输入检测方法
1.向运行程序提供不同的输入,检查在这些输入条件下程序是否出现缓冲区溢出问题。不仅能检测缓冲区溢出问题,还可以检测其它内存越界问题。
2.采用输入检测方法的工具有Purify、Fuzz和FIST(Fault Injection Security Tool)。
3.缺点:系统性能明显降低。输入检测方法的检测效果取决于输入能否激发缓冲区溢出等安全问题的出现。
Canary-based 检测方法
1.将canary(一个检测值)放在缓冲区和需要保护的数据之间,并且假设如果从缓冲区溢出的数据改写了被保护数据,检测值也必定被改写。
2.例如动态检测工具StackGuard、StackGhost、ProPolice、PointGuard等。
3.缺点:多少工具通过修改编译器实现检测功能,需要重新编译程序;这种方法无法检测能过绕过检测值的缓冲区溢出攻击
基于硬件的防御技术
1.Intel/AMD 64位处理器引入称为NX(No Execute)或者AVP(Advanced Virus Protection)的新特性,将以前的CPU合为一个状态存在的“数据页只读”和“数据页可执行”分成两个独立的状态。
2.ELF64 SystemV ABI通过寄存器传递函数参数而不再放置在堆栈上,使得64位处理器不仅可以抵制需要注入攻击代码的缓冲区溢出攻击还可以抵制return-to-libc这类的攻击。
参考资料:
教材:
李毅超 曹跃,网络与系统攻击技术 电子科大出版社 2007
周世杰 陈伟 钟婷,网络与系统防御技术 电子科大出版社 2007
参考书:
阙喜戎 等 编著,信息安全原理及应用,清华大学出版社
Christopher M.King, Curitis E.Dalton, T. Ertem Osmanoglu(常晓波等译). 安全体系结构的设计、部署与操作,清华大学出版社,2003(Christopher M.King, et al, Security Architecture, design, deployment & Operations )
William Stallings,密码编码学与网络安全-原理与实践(第三版),电子工业出版社,2004
Stephen Northcutt, 深入剖析网络边界安全,机械工业出版社,2003
冯登国,计算机通信网络安全,2001
Bruce Schneier, Applied Cryptography, Protocols, algorithms, and source code in C (2nd Edition)( 应用密码学 -协议、算法与C源程序, 吴世忠、祝世雄、张文政等译)
蔡皖东,网络与信息安全,西北工业大学出版社,2004