1、Windows 栈溢出分析摘要众所周知,缓冲区溢出是目前最为常见的漏洞利用方式,特别是栈溢出,原理简单,危害大。在 Windows xp sp2 以后,微软增加了许多安全检查措施来杜绝缓冲区溢出的发生。本文介绍两种 Windows xp 下两种常见的栈溢出利用方式,重点分析利用 S.E.H 进行栈溢出,并且归纳总结防范和检测方法。第 1 章 绪论1.1 栈溢出定义程序的局部变量一般都存放在栈空间内,如果用户输入的数据超过了定义的长度,就会非法覆盖栈空间的其他数据,这种现象就是栈溢出。1.2 栈溢出普通利用方式1.2.1 利用函数返回点函数调用约定描述了函数传递参数方式和栈协同工作的技术细节。不
2、同的操作系统,不同的语言,不同的编译器在实现函数调用时的原理虽然基本相同,但具体的调用约定还是有差别的,包括参数传递方式,参数入栈顺序是从右向左还是从左向右,函数返回时候恢复堆栈平衡的操作在子函数进行还是在母函数中进行。下表列出了几种调用方式。对于 Visual C+,可支持以下三种函数调用约定,如下表默认情况下,VC 使用_stdcall 的调用方式。本文以下讨论如不另加说明,即指这种默认的调用方式。函数调用大致包括以下几个步骤:(1)参数入栈:将参数从右向左依次压入系统栈中;(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行;(3)代码区跳转:处理器从当
3、前代码区跳转到被调函数的入口地址;(4)栈帧调整,包括保存当前栈帧状态值,已备后面恢复本栈帧时使用。将当前栈帧切换到新栈帧。给新栈帧分配空间。对于_stdcall 调用约定,函数调用时用到的汇编指令序列如下:Push 参数;从右至左Call 函数地址;Push ebp;Mov ebp,esp;Sub esp ,xxx;类似地,函数返回的步骤如下:(1)保存返回值:通常将函数的返回值保存在 Eax;(2)弹出当前栈帧,恢复上一个栈帧。(3)跳转:按照函数返回地址跳回母函数中继续执行。相关的汇编指令序列如下:Add esp,xxx;Pop ebp;Retn可以看到函数返回点保存在栈中,且栈中的数据
4、是可以被任意覆盖的,这就为利用提供了可能性。第二章 S.E.H 利用分析 2.1 结构化异常处理(SEH)操作系统或程序在运行,难免会遇到各种各样的错误,如除零,非法内存访问,文件打开错误,内存不足,磁盘读写错误,外设操作失败。为了保证系统在遇到错误时不至于崩溃,仍能够健壮稳定地继续运行下去,windows 会对运行在其中的程序提供一次补救的机会来处理错误这种机制就是异常处理机制。S.E.H 即异常处理结构体(Structure Exception Handler),它是windows 异常处理机制所采用的重要数据结构,每个 S.E.H 包含两个DWORD 指针:S.E.H 链表指针和异常处理
5、函数句柄,共 8 个字节,如下图下面分别对用户模式下的原始型SEH和封装型SEH 进行讨论。2.1.1 原始型SEHSEH 的进程相关类型是整个进程作用范围的异常处理函数,通过WIN32 API 函数SetUnhandledExceptionFilter 进行注册,而操作系统内部使用一个全局变量来记录这个顶层的处理函数,因此只能有一个全局性的异常处理函数。而线程相关类型的作用范围是本线程内,并且可注册多个,甚至可以嵌套注册。两者相比线程相关类型在实际应用中使用较为广泛,因此此处重点对此类型进行研究。当线程初始化时,会自动向栈中安装一个异常处理结构,作为线程默认的异常处理。SEH 最基本的数据结
6、构是保存在堆栈中的称为EXCEPTION_REGISTRATION 的结构体,结构体包括2 个元素: 第1 个元素是指向下一个EXCEPTION_REGISTRATION 结构的指针(prev),第2 个元素是指向异常处理程序的指针(handler)。这样一来,基于堆栈的异常处理程序就相互连接成一个链表。异常处理结构在堆栈中的典型分布如图1 所示。最顶端的异常处理结构通过线程控制块(TEB)0 Byte 偏移处指针标识,即FS:0处地址。用于进行实际异常处理的函数原型可表示如下:EXCEPTION_DISPOSITION _cdecl _except_handler(struct _EXCEP
7、TION_RECORD *ExceptionRecord,void * EstablisherFrame,struct _CONTEXT *ContextRecord,void * DispatcherContext)该函数的最重要的2 个参数是指向_EXCEPTION_RECORD 结构的ExceptionRecord 参数和指向_CONTEXT 结构的ContextRecord 参数,前者主要包括异常类别编码、异常发生地址等重要信息;后者主要包括异常发生时的通用寄存器、调试寄存器和指令寄存器的值等重要的线程执行环境。而用于注册异常处理函数的典型汇编代码可表示如下:PUSH handler
8、; handler 是新的异常处理函数地址PUSH FS:0 ;指向原来的处理函数的地址压入栈内MOV FS:0,ESP ;注册新的异常处理结构当异常发生时,操作系统的异常分发函数在进行初始处理后,如果异常没有被处理就会开始在上图所示的线程堆栈上遍历异常处理链,直到异常被处理,如果仍没有注册函数处理异常,则将异常交给缺省处理函数或直接结束产生异常的进程。2.1.2 封装型SEH通过使用_try/_except()/_finally等关键字,使开发人员更方便地在软件中使用SEH 是封装型SEH 的主要特点。该机制的异常处理数据结构定义如下:struct VC_EXCEPTION_REGISTRA
9、TION VC_EXCEPTION_REGISTRATION* prev;FARPROC handler;scopetable_entry* scopetable; /指向scopetable 数组指针int _index; /在scopetable_entry 中索引DWORD _ebp; /当前EBP 值显而易见,结构体中后3 个成员是新增的。而scopetable_entry 的结构如下所示:struct scopetable_entry DWORD prev_entryindex; /前一scopetable_entry 的索引FARPROC lpfnFilter; /过滤函数地址FA
10、RPROC lpfnHandler; /处理异常代码地址封装型SEH 的基本思想是为每个函数内的_try块建立一scopetable 表,每个_try块对应于scopetable 中的一项,该项指向_try块对应的scopetable_entry 结构,该结构含有与_except()/_finally对应的过滤函数和处理函数。若有_try块嵌套,则在scopetable_entry 结构的prev_entryindex成员中指明,多层嵌套形成单向链表。而每个函数只注册一个VC_EXCEPTION_REGISTRATION 结构, 该结构中的handler 成员是一个重要的运行时库函数_exce
11、pt_handler3。该异常处理回调函数负责对结构中的成员进行设置,查找处理函数并根据处理结果决定是继续执行还是让系统继续遍历外层SEH 链。为了弄清看似复杂的封装型SEH 原理,此处通过分析一个简单的使用封装型SEH 的函数的反汇编实现,从而深入地了解封装型SEH 的实现过程。该函数的C 语言实现如下:void A() _try / 0 号try 块_try / 1 号try 块*(PDWORD)0 = 0;_except(EXCEPTION_CONTINUE_SEARCH)printf(“Exception Handler!“); _finally puts(“in finally“);
12、 对应该函数的序言部分反汇编代码如下:push ebpmov ebp, esppush -1push offset _A_scopetablepush offset _except_handler3mov eax, large fs:0push eaxmov large fs:0, esp;显而易见, 压入堆栈的结构与VC_EXCEPTION_REGISTRATION 结构是一致的。查找scopetable 的地址为0x00422048,在调试器中查找该地址起始的内容如下:FFFFFFFF ;scopetable_entry0 的prev_entryindex 值00000000 ;lpfnF
13、ilter 地址值,为0,对应_finally004010EE ;lpfnHandler 地址值00000000 ;scopetable_entry1 的prev_entryindex 值004010C6 ;lpfnFilter 地址值004010C9 ;lpfnHandler 地址值第1 组值对应0 号try 块,而该块对应_finally块,所以过滤函数地址为0;第2 组值对应1 号try 块,而该块对应_except()块,所以有过滤函数和处理函数的地址。进一步查看0x004010EE 地址处的反汇编代码如下:PUSH OFFSET ?_C0LPEFDin?5finally?$AA; ”
14、infinally”CALL putsADD ESP,4RETN显然上述语句与_finally块中的C 语言语句是对应的,而其他的地址经过查找也是分别对应的。在进入0 号try 块时的反汇编语句如下:MOV DWORD PTR SS:EBP-4,0 ;对应第1 个_try 语句MOV DWORD PTR SS:EBP-4,1 ;对应第2 个_try 语句而在退出_try 块时对应的反汇编语句如下:MOV DWORD PTR SS:EBP-4,0 ;退出第2 个_try 块MOV DWORD PTR SS:EBP-4,-1 ;退出第1 个_try 块根据异常处理的堆栈结构可知, EBP-4 处值
15、就是VC_EXCEPTION_REGISTRATION 结构中_index 的值。异常处理机制在进入和退出每个_try 块前设置相应的_index 值,这样就可正确处理封装型SEH 内发生的各种异常。以上通过实例进一步验证和明确了封装型SHE 的内部机理,它只是扩展了原始型SEH 的功能,简化了软件开发人员的工作。2.2 SEH利用主要思想就是向程序的缓冲区中输入精心设计的指令编码并覆盖原来堆栈上的数据和指令,再通过一定的手段来执行这段输入指令(称作shellcode)从而达到预先的目的。而通过2.1 节的研究知道线程级的异常处理链是保存在程序运行的堆栈上,因此可以把它作为溢出攻击的一种方便手
16、段。利用SEH 执行shellcode 的基本原理就是,首先分析破解程序,找到其中的堆栈溢出漏洞(例如字符串拷贝函数strcpy()的使用)及存在的SEH 处理链,设计一段shellcode 执行指令保存在字符串变量中并确定该变量的地址。该段指令能将原来保存在堆栈中的异常处理函数地址覆盖并替换为shellcode 的地址,然后故意制造异常,由于异常处理函数已经被替换,因此系统会执行新的异常处理函数,也就是那段shellcode 代码,从而达到堆栈溢出的目的。下面的一段代码演示了这一过程的具体实现:1 #include 2 #include “stdio.h“3 char shellcode=“
17、x90x90x90x90.x98xFEx12x00“;4 DWORD myhandler(void)5 6 printf(“press Enter to kill process!n“);7 getchar();8 ExitProcess(1);9 return 1;10 11 void function(char *input)12 13 char buf200;14 int divide;15 divide=0;16 _try17 18 strcpy(buf,input);19 divide=10/divide;20 21 _except(myhandler()22 23 void mai
18、n()24 25 function(shellcode);26 从上面的代码中可以看出,第18 行语句strcpy(buf,input)是一个安全漏洞,通过对程序进行分析可以找到离栈顶最近的异常处理函数地址保存在堆栈地址0x0012ff68 处,buf 变量的地址0x0012fe98。因此设计shellcode 字符串长度要能覆盖到异常处理函数地址处,并将地址替换为0x0012fe98。注意此处shellcode 的中间部分为了节省篇幅被省去,在字符串的最后就是新地址的值。因此,当程序运行完第18 行语句后,已经完成替换,第19 行语句会抛出一个除零异常,而根据异常处理设计要执行异常处理代码,
19、此时会执行shellcode 中的语句,达到了堆栈溢出目的。为了防止异常处理机制被恶意利用,一种改进的方法是使用SAFESEH(Secure Exception Handling)技术。其基本思想是将模块中合法的异常处理函数登记在一个专用的称作SAFESEH 的表中。当有异常发生时,异常分发函数会根据异常处理函数的地址到它所对应的模块中查询这个函数是否在表中,如果在就执行,否则不执行。可见其核心思想是一种验证机制。而且SAFESEH 表存储在只读内存区域,由操作系统加载器在加载模块时写入。因此,一般情况下很难对其进行非法修改。第3章 防范及检测方法各种原因产生了大量存在漏洞的程序,而且利用缓冲
20、区溢出攻击主机也时有发生。现在,怎样防范这种危害巨大的攻击手段,已成为网络安全方面一个很重要的研究内容。3.1 编写程序中应该时刻注意的问题程序员有责任和义务养成安全编程的思想,应该熟悉那些可能会产生漏洞或需慎用的函数,清楚那些在编程中要小心使用的函数( 特别是在使用C语言时) ,例如:gets()、strcpy()等等。在软件测试阶段,要专门对程序中的每个缓冲区作边界检查和溢出检测。但是,由于程序编写者的经验不足和测试工作不够全面、充分,目前还不可能完全避免缓冲区溢出漏洞,因此这些漏洞在已经使用以及正在开发的软件中还是有存在的可能,还需要在使用软件时,对它做实时的测。3.2 使用安全语言编写
21、程序应使用Java等安全的语言编写程序,因为Java在对缓冲区进行操作时,有相应的边界检查,所以可以有效地防止缓冲区溢出漏洞的产生。但是,Java也并非绝对安全,Java的解释器是用C语言编写的,而C并不是一种安全的语言,所以Java解释器还是可能存在缓冲区溢出漏洞并受到攻击。3.3 改进编译器改进编译器的主要思想是在编译器中增加边界检查以及保护堆栈的功能,使得含有漏洞的程序和代码段无法通过编译。针对gcc编译器的很多补丁就提供了这些功能,比如说Stackguard。3.4 对堆栈栈底进行实时的监测监测一个堆栈应从其被建立到其消亡的全过程,需要监测的内容有堆栈的标志、栈底的地址、栈底存放的内容
22、、被压入栈的返回地址和EBP的值、可执行的压栈操作次数栈的大小等等。这些内容可以从操作系统获得,并需要监测CPU的状态。3.5 堆栈不可执行这种方法已经在很多种操作系统上有了相应的补丁,但它也不是一个万全之策,既然不可能在堆栈段执行程序,那么就将溢出字符串写入到数据段区或程序段区,这样就仍然可以执行。3.6 修改现在缓冲区的数据结构以上这些方法各有各的优点和缺点,但是仅用其中的一种方法或几种方法,并不能够完全杜绝缓冲区溢出漏洞。在防御缓冲区溢出攻击时,应综合使用其中的几种方法,才可以达到良好的效果。然而,这些方法都是一些治标不治本的方法,要从根本上解决缓冲区溢出漏洞的问题,必须从数据结构的角度
23、来考虑问题。其实绝大多数的缓冲区溢出漏洞,其根本原因就是C语言中的char * 数据结构。由于这一数据结构以及与之相关的各种函数的广泛应用,导致各种应用程序中的缓冲区溢出漏洞层出不穷。而大量业已存在并正在运行的C程序代码,又使得完全消除这一漏洞几乎不可能。事实上,要从根本上解决缓冲区溢出漏洞,必须从修改缓冲区的数据结构入手。只要有了安全的数据结构,就能构建出安全的函数和程序,从而防止由于数据结构上的不合理而造成的安全隐患。在C+中提倡使用的String函数库,正是针对C语言中的这一弱点而开发的。参考文献1看雪论坛:软件漏洞分析发现技术,电子工业出版社2 Russinovich M E, Solomon D A. 深入解析Windows 操作系统M.4 版. 潘爱民, 译. 北京: 电子工业出版社, 2007.3 齐 雷, 谢余强, 程东年, 等. Win32 SEH 异常处理机制分析J.信息工程大学学报, 2004, 5(6): 49-52.4Windows异常处理机制,张明,徐万里,计算机工程。