购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1 内存漏洞

本节首先介绍程序运行时的内存结构及缓冲区溢出漏洞的基本原理,接着分别介绍栈溢出、堆溢出,以及格式化字符串漏洞原理及利用技术。

3.1.1 内存结构及缓冲区溢出

为了清晰了解缓冲区溢出的工作原理,首先介绍Win32系统内存结构及函数调用时内存的工作原理。

在Win32环境下,由高级语言编写的程序经过编译、链接,最终生成可执行文件,即PE(Portable Executable)文件。在运行PE文件时,操作系统会自动加载该文件到内存,并为其映射出4GB的虚拟存储空间,然后继续运行,这就形成了所谓的进程空间。

Win32系统中,进程使用的内存按功能可以分为4个区域,如图3-1所示。

1)代码区:存放程序汇编后的机器代码和只读数据,这个段在内存中一般被标记为只读,任何企图修改这个段中数据的指令都将引发一个Segmentation Violation错误。当计算机运行程序时,会到这个区域读取指令并执行。

图3-1 进程的内存使用划分

2)数据区:用于存储全局变量和静态变量。

3)堆区:该区域内存由进程利用相关函数或运算符动态申请,用完后释放并归还给堆区。例如,C语言中用malloc/free函数、C++语言中用new/delete运算符申请的空间就在堆区。

4)栈区:该区域内存由系统自动分配,用于动态存储函数之间的调用关系。在函数调用时存储函数的入口参数(即形参)、返回地址和局部变量等信息,以保证被调用函数在返回时能恢复到主调函数中继续执行。

程序中所使用的缓冲区既可以是堆区和栈区,也可以是存放静态变量的数据区。由于进程中各个区域都有自己的用途,根据缓冲区利用的方法和缓冲区在内存中的所属区域,其可分为栈溢出和堆溢出。

缓冲区溢出漏洞就是在向缓冲区写入数据时,由于没有做边界检查,导致写入缓冲区的数据超过预先分配的边界,从而使溢出数据覆盖在合法数据上而引起系统异常的一种现象。

目前,缓冲区溢出漏洞普遍存在于各种操作系统(Windows、Linux、Solaris、Free BSD、HP-UX及IBM AIX),以及运行在操作系统上的各类应用程序中。著名的Morris蠕虫病毒,就是利用了VAX机上BSD UNIX的finger程序的缓冲区溢出错误。

3.1.2 栈溢出漏洞及利用分析

在介绍栈溢出之前,先对栈在程序运行期间的重要作用做一简单介绍。

1.函数的栈帧

(1)栈帧的概念

在程序设计中,栈通常是指一种后进先出(Last In First Out,LIFO)的数据结构,而入栈(Push)和出栈(Pop)则是进行栈操作的两种常见方法。为了标识栈的空间大小,同时为了更方便地访问栈中数据,栈通常还包括栈顶(Top)和栈底(Base)两个栈指针。栈顶指针随入栈和出栈操作而动态变化,但始终指向栈中最后入栈的数据;栈底指针指向先入栈的数据,栈顶和栈底之间的空间存储的就是当前栈中的数据。

相对于广义的栈而言,栈帧是操作系统为进程中的每个函数调用划分的一个空间,每个栈帧都是一个独立的栈结构,而系统栈则是这些函数调用栈帧的集合。

系统栈同样遵守后进先出的栈操作原则,它与一般的栈不同的地方如下。

●系统栈由系统自动维护,用于实现高级语言中函数的调用。当函数被调用时,系统会为这个函数开辟一个新的栈帧,并把它压入栈区中,所以正在运行的函数总是在系统栈区的栈顶(本书称为“当前栈帧”)。当函数返回时,系统会弹出该函数所对应的栈帧空间。

●对于类似C语言这样的高级语言,系统栈的Push和Pop等堆栈平衡的细节对用户是透明的。

●栈帧的生长方向是从高地址向低地址增长的。

(2)栈帧的标识

Win32系统提供了两个特殊的寄存器来标识当前栈帧。

●ESP:扩展栈指针(Extended Stack Pointer)寄存器,其存放的指针指向当前栈帧的栈顶。

●EBP:扩展基址指针(Extended Base Pointer)寄存器,其存放的指针指向当前栈帧的栈底。

显然,ESP与EBP之间的空间即为当前栈帧空间。

执行如图3-2a所示的代码段时,栈区中各函数栈帧的分布状态如图3-2b所示。

除了上述两个标识栈帧位置的寄存器外,在函数调用过程中,还有一个非常重要的寄存器——EIP,即扩展指令指针(Extended Instruction Pointer)寄存器,该寄存器存放的是指向下一条将要执行的指令。EIP控制了进程的执行流程,EIP指向哪里,CPU就会执行哪里的指令。

图3-2 栈区中各函数栈帧的分布状态

(3)栈帧中的内容

一个函数栈帧中主要包含以下信息。

1)前一个栈帧的栈底位置,即前栈帧EBP,用于在函数调用结束后恢复主调函数的栈帧(前栈帧的栈顶可计算得到)。

2)该函数的局部变量。

3)函数调用的参数。

4)函数的返回地址RET,用于保存函数调用前指令的位置,以便函数返回时能恢复到调用前的代码区中继续执行指令。

下面结合函数的调用,进一步介绍栈帧中这些信息是如何产生和使用的。

2.函数的调用

(1)函数调用的步骤

假设函数func_A调用函数func_B,这里称func_A函数为“主调函数”,func_B函数为“被调用函数”,函数调用的步骤如下。

1)参数入栈。将被调用函数(func_B)的实际参数从右到左依次压入主调函数(func_A)的函数栈帧中。

说明:

通常,不同的操作系统、不同的程序语言、不同的编译器在实现函数调用时,其对栈的基本操作都是一致的,但在函数调用约定上仍存在差异,这主要体现在函数参数的传递顺序和恢复堆栈平衡的方式,即参数入栈顺序是从左向右还是从右向左,函数返回时恢复堆栈的操作由被调用函数进行还是由主调函数进行。具体地,对于Windows平台下的Visual C++编译器而言,一般按照默认的stdcall方式对函数进行调用,即参数是按照从右向左的顺序入栈,堆栈平衡由主调函数完成。若无特殊说明,本章的函数调用均为stdcall调用方式。

2)返回地址RET入栈。将当前指令的下一条指令地址压入主调函数(func_A)的函数栈帧中。

3)代码区跳转。CPU从当前代码区跳转到被调用函数的入口,EIP指向被调用函数的入口处。

4)将当前栈帧调整为被调用函数的栈帧,具体方法如下。

●将主调函数(func_A)的栈帧底部指针EBP入栈,以便被调用函数返回时恢复主调函数的栈帧。

●更新当前栈帧底部:将主调函数(func_A)的栈帧顶部指针ESP的值赋给EBP,作为新的当前栈帧(即被调用函数func_B的栈帧)底部。

●为新栈帧分配空间:ESP减去适当的值,作为新的当前栈帧的栈顶。

(2)返回主调函数的步骤

被调用函数执行结束后,返回主调函数的步骤如下。

1)保存返回值。将函数的返回值保存在寄存器EAX中。

2)弹出当前栈帧,将前一个栈帧(即主调函数栈帧)恢复为当前栈帧,具体方法如下。

●降低栈顶,回收当前栈帧的空间。

●弹出当前EBP指向的值(即主调函数的栈帧EBP),并存入EBP寄存器,使得EBP指向主调函数栈帧的栈底。

●弹出返回地址RET,并存入EIP寄存器,使进程跳转到新的EIP所指执行指令处(即返回主调函数)。

需要注意的是,内存栈区由高地址向低地址增长,因此当4字节压入栈帧时,ESP=ESP-4;弹出栈帧时,ESP=ESP+4。

下面通过一个简单的程序来分析函数调用时栈帧中的内容变化情况,程序的运行环境为Windows 7操作系统和Visual C++ 6.0编译器。

【例3-1】函数调用时的栈帧分析

在Visual C++ 6.0中输入程序完成后,按〈F10〉键单步执行一次,进入Visual C++ 6.0的调试环境,黄色箭头指向当前要运行的代码位置。选择Debug(调试)工具栏中的Step Over(单步执行)程序,选择Debug工具栏按钮中的Memery(内存)和Registers(寄存器),可看到内存中栈的内容变化情况。

(1)main函数的栈帧情况

图3-3a所示为在fun()函数被调用之前main函数执行时内存和寄存器的情况,图3-3b所示为图3-3a对应的main函数栈帧的情况。图中阴影部分表示当前栈帧。

在查看图3-3时,需要特别注意以下3点。

1)main函数栈帧中EBP值的由来。用C语言编写的控制台程序都是以一个main函数作为用户代码的入口。但实际上,程序的执行过程如图3-4所示,首先,操作系统加载系统动态链接库kernel32.dll,由kernel32.dll中的一个间接调用函数来运行用户程序。这里还要说明的是,用户程序在编译时已经由编译器把C运行时库(C Run-time Library)的代码合并到一起了,所编译出的程序里肯定有一个mainCRTStartup函数,然后由其调用用户程序中的main函数。因此,main函数栈帧中EBP的值就是调用main函数的栈帧底部指针的EBP。

图3-3 示例程序main函数栈帧的情况

2)接着存储的是main函数的局部变量。这里要注意内存中数据的存储方式与数据值的关系。Win32系统内存中,由低位向高位存储一个4字节的双字,但作为实际数值时,是按由高向低字节进行解释的。例如,变量t1在内存中的存储为0x11110000,其实际数值应为0x00001111。

图3-4 C语言控制台程序调用过程

3)栈帧中ESP值的确定。ESP的值会随着局部变量、被调用函数参数等值的压入而减少,系统还会将ESP减去一定的值,为被调用函数执行时的栈帧预留一定大小的空间。

知识拓展:C运行时库(C Run-time Library)

运行时库是程序在运行时所需要的库文件,通常运行时库是以LIB或DLL形式提供的。C运行时库诞生于20世纪70年代。

C运行时库除了为人们提供必要的库函数调用(如memcpy、printf、malloc等)外,另一个重要的功能是为应用程序添加启动函数。

C运行时库启动函数的主要功能是进行程序的初始化,对全局变量赋初值,加载用户程序的入口函数,如控制台程序的入口点为mainCRTStartup(void)。

到了C++里,有了另外一个概念——C++标准库(C++ Standard Library),它包括了C运行时库和标准模板库(Standard Template Library,STL)。

(2)调用函数fun时main函数栈帧的变化情况

当调用函数fun时,根据上述步骤,首先将被调函数参数n和m的值压入主调函数main的栈帧(ESP=ESP-8),再将主调函数返回地址RET(0x00401093)压入main函数栈帧(ESP=ESP-4),如图3-5所示,此时当前栈帧仍为main函数栈帧,图中阴影部分表示当前栈帧。

(3)函数fun执行被调用时的栈帧

进程跳转至被调用函数的入口,同时将当前EBP(0x0018FF48)入栈(ESP=ESP-4),再将当前ESP的值赋给EBP,使当前栈帧调整为被调用函数fun的栈帧,如图3-6所示,图中阴影部分表示当前函数fun的栈帧。系统自动设置新的ESP值,为被调用函数的执行预留一定大小的空间。

图3-5 调用函数fun时main函数栈帧的情况

图3-6 当前栈帧为被调用函数fun栈帧的情况

(4)被调用函数fun执行结束时栈帧的变化

被调用函数fun执行结束时,函数返回值存入寄存器EAX中(EAX=0x00003333),同时弹出该函数的栈帧,将EBP指向的值(0x0018FF48)弹出存入EBP寄存器,以恢复主调函数栈帧底部。然后弹出返回地址RET(0x00401093),存入EIP中,如图3-7所示,图中阴影部分表示当前栈帧。

最后弹出被调函数的两个参数(ESP=ESP+8),将当前栈帧恢复到调用fun函数前主调函数main的栈帧状态,如图3-8所示,图中阴影部分表示当前栈帧。

通过【例3-1】了解了在函数调用时内存栈区的数据分布情况。在对这些知识理解的基础上,下面开始介绍栈溢出漏洞的原理和利用方法。

图3-7 被调用函数fun执行结束时栈帧的情况

图3-8 被调用函数执行结束后栈帧的情况

3.栈溢出漏洞基本原理

在函数的栈帧中,局部变量是顺序排列的,局部变量下面紧跟着的是前栈帧EBP及函数返回地址RET。如果这些局部变量为数组,由于存在越界的漏洞,那么越界的数组元素将会覆盖相邻的局部变量,甚至覆盖前栈帧EBP及函数返回地址RET,从而造成程序的异常。

下面通过两个例子来说明缓冲区溢出的两种情况:修改相邻变量和修改返回地址。

(1)栈溢出修改相邻变量

【例3-2】修改相邻变量

fun()函数实现了一个基于口令认证的功能:用户输入的口令存放在局部变量str数组中,然后程序将其与预设在局部变量password中的口令进行比较,以得出是否通过认证的判断(此处仅为示例,并非实际采用的方法)。图3-9所示为程序执行时,用户输入了正确口令ABCDE后内存的状态。注意:数组大小为6,字符串结束字符“\0”占1个字节,因此口令应当为5个字节,图中阴影部分表示当前栈帧。

图3-9 程序执行时用户输入正确口令后内存的状态

从图3-9中可以看出,内存分配是按字节对齐的,因此根据变量定义的顺序,在函数栈帧中首先分配2个字节给password数组,然后再分配2个字节给str数组。

由于C语言中没有数组越界检查,因此,当用户输入的口令超过2个字节时,将会覆盖紧邻的password数组。如图3-10所示,当用户输入13个字符“aaaaaaaaaaaaa”时,password数组中的内容将被覆盖。此时,password数组和str数组的内容就是同一个字符串“aaaaa”,从而比较结果为二者相等。因此,在不知道正确口令的情况下,只要输入13个字符,其中前5个字符与后5个字符相同,就可以绕过口令的验证了。

图3-10 用户输入13个字符“aaaaaaaaaaaaa”后内存的状态

如果用户增加输入字符串的长度,将会超过password数组的边界,从而覆盖前栈帧EBP,甚至是覆盖返回地址RET。当返回地址RET被覆盖后,将会造成进程执行跳转的异常。图3-11给出了当用户输入23个字符“aaaabbbbccccddddeeeefff”后内存的状态,出于对4字节对齐的考虑,输入时按4个相同字符一组进行组织,图中阴影部分表示当前栈帧。

图3-11 用户输入23个字符“aaaabbbbccccddddeeeefff”后内存的状态

显然,在EBP+4所指空间本应该存放返回地址RET,但现在已经被覆盖成了字符串“fff”。当程序进一步执行,返回主调函数main时,弹出该空间的值,作为返回地址存入EIP中,即EIP=0x00666666。此时,CPU将按照EIP给出的地址去取指令,由于内存0x00666666处没有合法指令可执行,因此程序报错,如图3-12所示。

图3-12 程序报错

(2)栈溢出后修改返回地址RET

栈溢出后修改相邻变量这种漏洞利用对代码环境的要求比较苛刻。更常用的栈溢出修改的目标往往不是某个变量,而是栈帧中的EBP和函数返回地址RET等值。

接下来【例3-3】演示的是,将一个有效指令地址写入返回地址区域中,这样就可以让CPU跳转到所希望执行的指令处,从而达到控制程序执行流程的目的。

【例3-3】修改返回地址

由于从键盘上输入的字符必须是可打印字符,而将一个有效地址作为ASCII对应的字符不一定能打印,如0x011。因此,将【例3-2】程序稍作修改,程序输入改为读取密码文件password.txt。

如果在password.txt文件中,存入23个字符“aaaabbbbccccddddeeeefff”,程序运行的效果将与从键盘输入一致。为了让程序在调用fun()函数返回后,去执行所希望的函数Attack(),必须将password.txt文件中的最后4个字节改为Attack()函数的入口地址0x0040100F(得到函数的入口地址的方法有很多,可以利用OllyDbg工具查看)。利用十六进制编辑软件UltraEdit打开password.txt文件,将最后4个字节改为相应的地址即可,如图3-13所示。

图3-13 利用UltraEdit修改password.txt文件中最后4个字节

函数调用返回时,从栈帧中弹出返回地址,存入EIP中。从图3-14a中可以看到,此时EIP=0x0040100F,CPU按该地址去获取指令,跳转到Attack()函数。图3-14b显示的程序运行结果表明,函数Attack()被正常执行了,说明溢出修改返回地址成功。

图3-14 溢出覆盖返回地址

4.栈溢出攻击

上面介绍了用户进程能够修改相邻变量和修改返回地址两种缓冲区溢出漏洞。实际攻击中,攻击者通过缓冲区溢出改写的目标往往不是某一个变量,而是栈帧高地址的EBP和函数的返回地址等值。通过覆盖程序中的函数返回地址和函数指针等值,攻击者可以直接将程序跳转到其预先设定或已经注入到目标程序的代码上去执行。

栈溢出攻击是一种利用栈溢出漏洞所进行的攻击行动,目的在于扰乱具有某些特权运行的程序的功能,使得攻击者取得程序的控制权,如果该程序具有足够的权限,那么整个主机就被控制了。

下面介绍两种栈溢出攻击的基本原理。

(1)JMP ESP覆盖方法

上述【例3-3】演示了代码植入攻击的基本方法。实际攻击者会向被攻击的程序中输入一个包含Shellcode的字符串,让被攻击程序转而执行Shellcode。不过,在实际的漏洞利用中,由于动态链接库的装入和卸载等原因,Windows进程的函数栈帧可能发生移位,即Shellcode在内存中的地址是动态变化的,所以这种采用直接赋地址值的简单方式在以后的运行过程中会出现跳转异常。

为了避免这种情况的发生,可以在覆盖返回地址时用系统动态链接库中某条处于高地址且位置固定的跳转指令所在的地址进行覆盖,然后再通过这条跳转指令指向动态变化的Shellcode地址。这样便能够确保程序执行流程在目标系统运行时可以被如期进行。

JMP ESP覆盖方法是覆盖函数返回地址的一种攻击方式。考虑到函数返回时ESP总是指向函数返回后的下一条指令,根据这一特点,如果用指令JMP ESP的地址覆盖返回地址,则函数也可以跳转到函数返回后的下一条指令,而从函数返回后的下一条指令开始都已经被Shellcode所覆盖,那么程序就可以跳转到该Shellcode上并执行,从而实现了程序流程的控制。

在这种方法中,输入被攻击程序的字符串格式为:NN…NNRSS…SS,其中N(Nop)表示可填充一些无关的数据,R表示JMP ESP的地址,S表示Shellcode。这一格式的字符串输入后,栈帧的状态如图3-15所示,图中阴影部分表示当前栈帧。

当函数返回时,取出JMP ESP后,ESP向高地址移4个字节,正好指向Shellcode,而此时EIP指向JMP ESP指令,所以程序就转去执行Shellcode了。

为了能成功地进行JMP ESP覆盖法攻击,攻击者需要做如下工作。

1)分析调试有漏洞的被攻击程序,获得栈帧的状态。

2)获得本机JMP ESP指令的地址。

3)写出希望运行的Shellcode。

4)根据栈帧状态,构造输入字符串。

在内存中搜索JMP ESP指令是比较容易的(可以通过Ollydbg软件在内存中搜索)。一般选择kernel32.dll或者user32.dll中的地址。

(2)SEH覆盖方法

Windows平台下,操作系统或应用程序运行时,为了保证在出现除零、非法内存访问等错误时,系统也能正常运行而不至于崩溃或宕机,Windows会对运行在其中的程序提供一次补救的机会来处理错误,这种机制就是Windows下的异常处理机制。异常处理就是在程序出错后,系统关闭之前,让程序转去执行一个预设的回调函数(异常处理函数)。

异常处理机制的一个重要数据结构是位于系统栈中的异常处理结构体(Struct Exception Handler,SHE),它包含两个DWORD指针。

●SHE链表指针prev,用于指向下一个SEH结构。

●异常处理函数句柄handler,用于指向异常处理函数的指针。

当线程初始化时,会自动向栈中安装一个异常处理结构,作为线程默认的异常处理。这样,多个异常处理程序就连接成了一个由栈顶向栈底延伸的单链表,链表头部位置通过TEB(Thread Environment Block,线程控制块)0字节偏移处的指针标识。如图3-16所示,当发生异常时,操作系统会中断程序,并首先从TEB的0字节偏移处取出最顶端的SEH结构地址,使用异常处理函数句柄所指向的代码来处理异常。如果该异常处理函数运行失败,则顺着SEH链表依次尝试其他的异常处理函数。如果程序预先安装的所有异常处理函数均无法处理,系统将采用默认的异常处理函数,弹出错误对话框并强制关闭程序。

图3-15 使用JMP ESP覆盖方法跳转到Shellcode执行

图3-16 SEM链表结构

SEH覆盖方法就是覆盖异常处理程序地址的一种攻击方式。由于SHE结构存放在栈中,因此攻击者可以利用栈溢出漏洞,设计特定的溢出数据,将SEH中异常函数的入口地址覆盖为Shellcode的起始地址或可以跳转到Shellcode的跳转指令地址,从而导致程序发生异常时,Windows异常处理机制执行的不是预设的异常处理函数,而是Shellcode。

3.1.3 堆溢出漏洞及利用分析

上一节介绍了栈溢出的原理及其利用技术。另一种基于堆溢出的攻击逐渐成为主流,相对于栈溢出攻击,这类攻击更难防范。本节首先介绍堆的相关知识,然后介绍堆溢出漏洞及利用技术。

1.堆的基本知识

(1)堆与栈的区别

程序在执行时需要两种不同类型的内存来协同配合,即如图3-1中所示的栈和堆。

典型的栈变量包括函数内部的普通变量、数组等。栈变量在使用时不需要额外的申请操作,系统栈会根据函数中的变量声明自动在函数栈帧中给其预留空间。栈空间由系统维护,它的分配和回收都是由系统来完成的,最终达到栈平衡,所有这些对程序员来说都是透明的。

另一种类型的内存结构是堆,主要具备以下特性。

●堆是一种在程序运行时动态分配的内存。所谓动态,是指所需内存的大小在程序设计时不能预先确定或内存过大无法在栈中进行分配,需要在程序运行时参考用户的反馈。

●堆在使用时需要程序员使用专有的函数进行申请,如C语言中的malloc等函数,C++中的new运算符等都是最常见的分配堆内存的方法。堆内存申请有可能成功,也有可能失败,这与申请内存的大小、机器性能和当前运行环境有关。

●一般用一个堆指针来使用申请得到的内存,读、写、释放都通过这个指针来完成。

●堆使用完毕后需要将堆指针传给堆释放函数以回收这片内存,否则会造成内存泄露。典型的释放方法包括free、delete等。

堆内存和栈内存的特点比较如表3-1所示。

表3-1 堆内存和栈内存的比较

(2)堆的结构

现代操作系统中堆的数据结构一般包括堆块和堆表两类。

1)堆块:出于性能的考虑,堆区的内存按不同大小组织成块,以堆块为单位进行标识,而不是传统的按字节标识。一个堆块包括两个部分:块首和块身。块首是一个堆块头部的几个字节,用来标识这个堆块自身的信息,例如,本块的大小,本块是空闲还是占用等信息;块身是紧跟在块首后面的部分,也是最终分配给用户使用的数据区。

堆的内存组织如图3-17所示。

堆区和堆块的分配都由程序员来完成。对一个堆块而言,被分配之后,如果不被合并,那么会有两种状态:占有态和空闲态。其中,空闲态的堆块会被链入空链表中,由系统管理。而占有态的堆块会返回一个由程序员定义的句柄,由程序员管理。

堆块被分为两部分:块首和块身。其中块首是一个8字节的数据,存放着堆块的信息。指向堆块的指针或者句柄指向的是块身的首地址。

图3-17 堆在内存中的组织

2)堆表:堆表一般位于堆区的起始位置,用于索引堆区中所有堆块的重要信息,包括堆块的位置、堆块的大小、空闲还是占用等。堆表的数据结构决定了整个堆区的组织方式,是快速检索空闲块、保证堆分配效率的关键。堆表在设计时可能会考虑采用平衡二叉树等高级数据结构来优化查找效率。现代操作系统的堆表往往不止一种数据结构。

在Windows中,占用态的堆块被使用它的程序索引,而堆表只索引所有空闲块的堆块。其中最重要的堆表有两种:空闲双向链表freelist(简称空表)和快速单向链表lookaside(简称快表)。

①空表。

空闲堆的块首中包含一对重要的指针:前向指针(flink)和后向指针(blink),用于将空闲堆块组织成双向链表。空闲双向表中的结点结构如图3-18所示。

图3-18 空闲双向表结点结构

堆区一开始的堆表区中有一个128项的指针数组,称为空表索引(freelistarray)。该数组的每一项包括两个指针,用于表示一个空表。

图3-19中,空表索引的第二项free[1]标识了堆中所有大小为8字节的空闲堆块,之后每个索引项指示的空闲块递增8字节,例如,free[2]标识大小为16字节的空闲堆块,free[3]标识大小为24字节的空闲堆块,free[127]标识大小为1016字节的空闲堆块。因此有:

空闲堆块的大小=索引项×8(字节)

其中,空表索引项的第一项free[0]标识的空表比较特殊。这条双向链表链入了所有大于等于1024字节并且小于512K字节的堆块。这些堆块按照各自的大小在零号空表中按升序依次排列。

图3-19 空闲双向链表

②快表。

快表是Windows用来加速堆块分配而采用的一种堆表。之所以把它称为“快表”,是因为这类单向链表中从来不会发生堆块合并(其中的空闲块块首被设置为占有态,用来防止堆块合并)。

快表也有128条,组织结构与空表类似,只是其中的堆块按照单链表组织。快表总是被初始化为空,而且每条快表最多只有4个结点,故很快就会被填满。由于在堆溢出中一般不利用快表,故不作详述。

2.堆溢出漏洞及利用

堆管理系统的3类操作:堆块分配、堆块释放和堆块合并,归根到底都是对空链表的修改。分配就是将堆块从空表中“卸下”;释放就是把堆块“链入”空表;合并可以看成是把若干块先从空表中“卸下”,修改块首信息,然后把更新后的块“链入”空表。所有“卸下”和“链入”堆块的工作都发生在链表中,如果能够修改链表结点的指针,在“卸下”和“链入”的过程中就有可能获得一次读写内存的机会。

(1)DWORD Shoot

堆溢出利用的精髓就是用精心构造的数据去溢出覆盖下一个堆块的块首,使其改写块首中的前向指针(flink)和后向指针(blink),然后在分配、释放和合并等操作发生时伺机获得一次向内存任意地址写入任意数据的机会。这种能够向内存任意位置写任意数据的机会称为Arbitrary Dword Reset(又称Dword Shoot)。Arbitrary Dword Reset发生时,不但可以控制射击的目标(任意地址),还可以选用适当的目标数据(4字节恶意数据)。通过Arbitrary Dword Reset,攻击者可以进而劫持进程,运行shellcode。

下面先简单分析一下空表中结点的正常拆卸操作。

根据链表操作的常识,可以了解到,拆卸时发生如下操作。

图3-20所示为“卸掉”空闲双向链表中阴影结点的操作。

图3-20 空闲双向链表中结点的拆卸

如果用精心构造的数据淹没该结点块身的前8个字节,即该堆块的前向指针和后向指针时,在flink里面放入4字节的任意恶意数据内容,在blink里面放入目标地址;若该结点被拆卸,执行node->blink->flink=node->flink操作时,对于node->blink->flink,系统会认为node->blink指向的是一个堆块的块身,而flink正是这个块身的第一个4字节单元,而node->flink为node的前4字节,因此该拆卸操作导致目标地址的内容被修改为该4字节的恶意数据。通过这种构造可以实现对任意地址的4字节(DWORD)数据的任意操作。

图3-21所示为上述过程的图示。

图3-21 DWORD Shoot攻击原理

(2)Heap Spray

Heap spray技术是使用栈溢出和堆结合的一种技术,这种技术可以在很大程度上解决溢出攻击在不同版本上的不兼容问题,并且可以减少对栈的破坏。缺陷在于只能在浏览器相关溢出中使用,但是相关思想却被广泛应用于其他类型攻击中,如JIT spray、ActivexSpray等。

这种技术的关键在于,首先将shellcode放置到堆中,然后在栈溢出时,控制函数执行流程,跳转到堆中执行shellcode。

在一次漏洞利用过程中,关键是用传入的shellcode所在的位置去覆盖EIP。在实际攻击中,用什么值覆盖EIP是可控的,但是这个值指向的地址是否有shellcode就很关键了。假设“地址A”表示shellcode的起始地址,“地址B”表示在缓冲区溢出中用于覆盖的函数返回地址或者函数指针的值。因此如果B<A,而地址B到地址A之间如果有诸如nop这样的不改变程序状态的指令,那么在执行完B到A间的这些指令后,就可以继续执行shellcode。

Heap spray应用环境一般是浏览器,因为在这种环境下,内存布局比较困难,想要跳转到某个固定位置几乎不可能,即使使用jmp esp等间接跳转有时也不太可靠。因此,Heap Spray技术应运而生。

3.1.4 格式化字符串漏洞及利用分析

格式化字符串(简称格式化串)的漏洞本身并不算缓冲区溢出漏洞,这里作为一类比较典型的系统函数存在的漏洞做一介绍。

1.格式化串漏洞

格式化串漏洞的产生源于数据输出函数中对输出格式解析的缺陷,其根源也是C语言中不对数组边界进行检查的缓冲区错误。

下面以printf函数为例进行介绍。

格式控制format中的内容可能为“%s,%d,%p,%x,%n,…”等格式控制符,用于将数据格式化后输出。

printf函数进行格式化输出时,会根据格式化串中的格式化控制符在栈上取相应的参数,然后按照所需格式输出。因此,这里存在的漏洞是,如果函数调用给出的输出数据列表少于格式控制符个数,甚至没有给出输出数据列表,系统仍然会按照格式化串中格式化控制符的个数输出栈中的数据。

【例3-4】分析下面的程序

对于上述代码,第一个printf函数调用时,3个参数按照从右到左的顺序,即b、a、"a=%d,b=%d\n"的顺序入栈,栈中状态如图3-22所示,输出结果正常,显示:

第二次调用printf函数没有引起编译错误,程序正常执行,只是输出的数据出乎预料。一种可能的运行结果为:

这是因为,第二次调用的printf函数参数中缺少了输出数据列表部分,故只压入格式控制符参数,这时栈中状态如图3-23所示。当在栈上取与格式化控制符%d和%d对应的两个变量输出时,错误地把栈上其他数据当作a、b的值进行了输出。

格式符除了常见的d、f、u、o、x之外,还有一些指针型的格式符。

●s:参数对应的是指向字符串的指针。

●n:这个参数对应的是一个整数型指针,将这个参数之前输出的字符的个数写入该格式符对应参数指向的地址中。例如,对于如下代码:

图3-22 printf函数调用时的内存布局

图3-23 格式化漏洞原理

格式化串中指定了%n,此前输出了1~0这10个字符,因此会将10写入变量a中。

类似地,利用%p、%s和%n等格式符精心构造格式化串即可实现对程序内数据的任意读、任意写,从而造成信息泄露、数据篡改和程序流程的非法控制等威胁。

除了printf函数之外,该系列中的其他函数也可能产生格式化串漏洞,如fprintf、sprintf、snprintf、vprintf、vfprintf、vsprintf和wprintf等。

2.格式化串漏洞利用

格式化串漏洞的利用可以通过以下方法实现。

1)通过改变格式化串中输出参数的个数实现修改指定地址的值:可以修改填充字符串长度实现;也可以通过改变输出的宽度实现,如%8d。

2)通过改变格式化串中格式符的个数,调整格式符对应参数在栈中的位置,从而实现对栈中特定位置数据的修改。如果恰当地修改栈中函数的返回地址,那么就有可能实现程序执行流程的控制。也可以修改其他函数指针,改变执行流程。相对于修改返回地址,改写指向异常处理程序的指针,然后引起异常,这种方法猜测地址的难度比较小,成功率较高。

下面通过两个例子说明格式化串漏洞利用的基本原理。

【例3-5】利用格式化串漏洞读内存数据

在Windows XP SP2操作系统下,运行Visual C++6.0编译器,生成release版本的可执行文件。当向程序中传入普通字符串(如“Buffer Overflow”)时,将输出该字符串中的第一个单词。但如果传入的字符串中带有格式控制符时,printf就会打印出栈中的数据。例如,输入“%p,%p,%p,…”可以读出栈中的数据(%p控制以十六进制整数方式输出指针的值),如图3-24所示。

【例3-5】演示的利用格式化串读内存数据还不算很糟糕,如果配合修改内存数据,就有可能引起进程劫持和shellcode植入了。

图3-24 利用格式化串漏洞读内存

在格式化控制符中,有一种较少用到的控制符%n。这个控制符用于把当前输出的所有字符的个数值写回一个变量中去。下面【例3-6】中的代码展示了这种方法。

【例3-6】用printf向内存写数据

上述代码在Windows 7操作系统下,运行Visual C++6.0编译器。当程序执行第1条语句后,内存布局如图3-25所示,注意变量num的地址为0x0018FF44。

图3-25 执行第1条语句后的内存布局

程序执行第2条语句(第1条printf语句)后,内存布局如图3-26所示。注意,参数从右向左依次压栈。

图3-26 执行第1条printf语句后的内存布局示意图

执行第3条语句(第2条printf语句)后,参数压栈之后,内存布局如图3-27所示。

当执行第3条printf语句后,变量num的值已经变成了0x00000014(对应十进制值为20),如图3-28所示。这是因为程序中将变量num的地址压入栈,作为第2条printf()的第2个参数,“%n”会将打印总长度保存到对应参数的地址中去,打印结果如图3-29所示。0x61616161的十进制值为1633771873,按照“%.20d”格式输出,其长度为20。

图3-27 执行第2条printf语句后的内存布局示意图

图3-28 执行第3条printf语句后的内存布局

图3-29 输出结果

如果不将num的地址压入堆栈,如下面的程序所示。

运行结果如图3-30所示。

图3-30 运行结果

程序在执行第2条printf()语句时发生错误,printf()将堆栈中main()函数的变量num当作了%n所对应的参数,而0x61616161肯定是不能访问的。

格式化串漏洞是一类真实存在、危害较大的漏洞,但是相对于栈溢出等漏洞而言,实际案例并不多。并且格式化串漏洞的形成原因较为简单,只要通过静态扫描等方法,就可以发现这类漏洞。此外,在Visual Studio 2005以上版本中的编译级别对参数进行了检查,且默认情况下关闭了对%n控制符的使用。

拓展阅读

读者要想了解更多缓冲区溢出攻击方法,可以阅读以下书籍资料。

[1]王清.0 day安全:软件漏洞分析技术[M].2版.北京:电子工业出版社,2011.

[2]ChrisAnley,等.黑客攻防技术宝典:系统实战篇[M].2版.北京:人民邮电出版社,2010.

[3]林桠泉.漏洞战争:软件漏洞分析精要[M].北京:电子工业出版社,2016.

[4]吴世忠,郭涛,董国伟.软件漏洞分析技术[M].北京:科学出版社,2014. LwtN/kh41jsXTs6afoN4o4oXMB5itfLUIgk4KZWiscwRdWRSeXU+EjPbxXuCrRTJ



3.2 Windows安全漏洞保护分析

微软等大型软件厂商为了最大程度地保护系统安全运行,逐渐在操作系统中增加了多种保护机制,致力于减少系统程序漏洞,以及漏洞被触发和利用的可能性。本节主要介绍Windows平台下几种典型的漏洞利用阻断技术,包括/GS、DEP、ASLR及SafeSEH等,同时本节对这些技术的不足也进行了分析,最后对微软为缓解操作系统漏洞被利用而推出的EMET工具进行简要介绍。

3.2.1 栈溢出检测选项/GS

1./GS保护机制

Visual Studio 2003中加入了/GS栈溢出检测这个编译选项,微软命名为Stack Canary(栈金丝雀)。金丝雀对空气中的甲烷和一氧化碳浓度的高敏感度使它成为最早的煤矿安全报警器,这里是指,调用函数时将一个随机生成的秘密值存放在栈上,当函数返回时,检查这个堆栈检测仪的值是否被修改,以此判断是否发生了栈溢出。

/GS栈溢出检测选项最初在Visual Studio 2003中默认情况下是关闭的。自Visual Studio 2005以后的版本中,微软将/GS选项改为默认开启,也可以在系统中对其进行禁用设置。如图3-31所示,在Visual Studio 2013中新建一个项目后,可以选择菜单中的“项目”→“项目属性”→“配置属性”→“C/C++”→“代码生成”→“安全检查”找到该选项。

如果编译器启用了/GS选项,那么当程序编译时,它首先会计算出程序的一个安全Cookie(伪随机数,4字节DWORD无符号整型);然后将安全Cookie保存在加载模块的数据区中,在调用函数时,这个Cookie被复制到栈中,位于RET返回地址、EBP与局部变量之间,如图3-31所示;在函数调用结束时,程序会把这个Cookie和事先保存的Cookie进行比较。如果不相等,就说明进程的系统栈被破坏,需要终止程序运行。

在典型的缓冲区溢出中,栈上的返回地址会被数据所覆盖(如图3-32所示),但在返回地址被覆盖之前,安全Cookie早己经被覆盖了,因此在函数调用结束时检查Cookie会发现异常,并终止程序,这将导致漏洞利用的失效。

图3-31 /GS编译选项设置

图3-32 使用/GS选项编译的程序的栈帧结构

/GS保护机制除了在栈中加入安全Cookie之外,在Visual Studio 2008及以后的版本中还增加了对函数内部的局部变量和参数的保护功能,编译器会进行以下操作。

●对函数栈重新排序,把字符串缓冲区分配在栈帧的高地址上,这样当字符串缓冲区被溢出时,也就不能溢出任何本地局部变量了。

●将函数参数复制到寄存器或放到栈缓冲区上,以防止参数被溢出。

2.对抗/GS保护

从/GS栈溢出检测机制来看,其关键之处就是在栈中加入安全Cookie来保护相关参数和变量,所以对抗这种栈溢出保护机制的直接方法是围绕Cookie值展开的。

(1)猜测Cookie值

Skape曾经在文献 Reducing the Effective Entropy of GS Cookies 中讨论并证明了/GS保护机制使用了几个较弱的熵源,攻击者可以对其进行计算并使用它们来预测(或猜测)Cookie值。但是这种方法只适用于针对本地系统的攻击。

(2)通过同时替换栈中的Cookie和Cookie副本

替换加载模块数据区中的Cookie值(它必须是可写的,否则程序就无法在运行中动态更新Cookie),同时用相同的值替换栈中的Cookie,以此来绕过栈上的Cookie保护。

(3)覆盖SEH绕过Cookie检查

/GS保护机制并没有保护存放在栈上的SEH结构。因此,如果能够写入足够的数据来覆盖SEH记录,并在Cookie检查之前触发异常,那么可以控制程序的执行流程。该方法相当于是利用SEH进行漏洞攻击。虽然有SEH保护机制SafeSEH,但SafeSEH也是可以被绕过的,因而可以同时绕过/GS保护机制。

(4)覆盖父函数的栈数据绕过Cookie检查

当函数的参数是对象指针或结构指针时,这些对象或结构存在于调用者的堆栈中,这种情况下可能导致/GS保护被绕过:覆盖对象的虚函数表指针,将虚函数重定向到需要执行的恶意代码,那么如果在检查Cookie前存在对该虚函数的调用,则可以触发恶意代码的执行。

3.2.2 数据执行保护DEP

1.数据执行保护DEP(Data Execution Prevention)机制

栈溢出漏洞的最常见利用方式是:在栈中精心构造二进制串溢出原有数据结构,进而改写函数返回地址,使其跳转到位于栈中的Shellcode执行。如果使栈上数据不可执行,那么就可以阻止这种漏洞利用方式的成功实施。而DEP就是通过使可写内存不可执行或使可执行内存不可写,以消除类似威胁的。

DEP是微软随Windows XP SP2和Windows 2003 SP1的发布而引入的一种数据执行保护机制。类似DEP这样的内存保护方式较早就出现了,但是叫法不尽相同,较为通用的称呼是NX,即No eXecute。此外,Intel把它这种技术称为Execute Disable或XD-bit;AMD把它称为Enhanced Virus Protection,也写成W^X,意思就是可写或可执行,但二者绝不允许同时发生。

事实上,不可执行堆栈技术并不新鲜,早在多年前,SUN公司(已被甲骨文公司收购)的Solaris操作系统中就提供了启用不可执行栈的选项,这要早于Windows的DEP技术。早在1993年,NT 3.1系统里就有了VirtualProtect函数的使用,在内存页中包含了是否可执行的标志,但是由于当时的处理器不支持对每个内存页上的数据进行不可执行检查,所以这种标志因缺乏硬件支持而实际上并未发挥作用。

2003年9月,AMD率先为不可执行内存页提供了硬件级的支持,即NX的特性;随后Intel也提供了类似的被称为XD(eXecute Disable)的特性。在硬件支持的基础上,微软开始在Windows系统上真正引入了DEP保护机制。

DEP在具体实现上有两种模式:硬件实现和软件实现。如果CPU支持内存页NX属性,就是硬件支持的DEP。如果CPU不支持,那就是软件支持的DEP模式,这种DEP不能直接组织在数据页上执行代码,但可以防止其他形式的漏洞利用,如SEH覆盖。Windows中的DEP tabsheet会表明是否支持硬件DEP。

根据操作系统和Service Pack版本的不同,DEP对软件的保护行为是不同的。在Windows的早期版本及客户端版本中,只为Windows核心进程启用了DEP,但此设置已在新版本中改变。

在Windows服务器操作系统上,除了那些手动添加到排除列表中的进程外,系统为其他所有进程都开启了DEP保护,而客户端操作系统使用了可选择启用的方式。微软的这种做法很容易理解:客户端操作系统通常需要能够运行各种软件,而有的软件可能和DEP不兼容;在服务器上,在部署到服务器前都经过了严格的测试(如果确实是不兼容,仍然可以把它们放到排除名单中)。

此外,Visual Studio编译器提供了一个链接标志(/NXCOMPAT),可以在生成目标应用程序时使程序启用DEP保护。

2.对抗数据执行保护DEP

DEP技术使得在栈上或其他一些内存区域执行代码成为不可能,但是执行已经加载的模块中的指令或调用系统函数则不受DEP影响,而栈上的数据只需作为这些函数/指令的参数即可。从已有的技术来看,要绕过DEP保护可以有以下几种选择。

●利用ret-to-libc执行命令或进行API调用,如调用WinExec实现执行程序。

●将包含Shellcode的内存页面标记为可执行,然后再跳过去执行。

●通过分配可执行内存,再将Shellcode复制到内存区域,然后跳过去执行。

●先尝试关闭当前进程的DEP保护,然后再运行Shellcode。

3.2.3 地址空间布局随机化ASLR

1.地址空间布局随机化ASLR机制

从Windows Vista开始,微软向其新版本操作系统中引入了地址空间布局随机化(Address Space Layout Randomization,ASLR)保护机制。其原理很简单:通过对堆、栈和共享库映射等线性区域布局的随机化,增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止漏洞利用的目的。例如,同一版本的Windows XP上系统里DLL模块的加载地址是固定的,那么攻击者只需针对不同操作系统版本进行分别处理即可,但是使用ASLR之后,攻击者必须在攻击代码中进行额外的地址定位操作,才有可能成功利用漏洞,这在一定程度上确保了系统安全。

ASLR保护机制进行随机化的对象主要包括以下几个方面。

1)映像随机化:改变可执行文件和DLL文件的加载地址。

2)栈随机化:改变每个线程栈的起始地址。

3)堆随机化:改变已分配堆的基地址。

对于地址空间布局随机化ASLR机制,微软从可执行程序编译时的编译器选项和操作系统加载时地址变化两个方面进行了实现和完善。

在Visual Studio 2005 SP 1及更高版本的Visual Studio编译器中,均提供了连接选项/DYNAMICBASE。使用了该连接选项之后,编译后的程序每次运行时,其内部的栈等结构的地址都会被随机化。

2.ASLR机制的缺陷和绕过方法

(1)对本地攻击者无能为力

系统每次重启,ASLR就会在整个系统中生效。如果使用了特定DLL的所有进程都卸载该DLL,那么在下次加载时它能被加载到新的随机地址,但是,如果系统中的DLL总是由多个进程加载,导致只有在操作系统重启时才能再次随机化。

因此,虽然ASLR对于来自网络的攻击,比如像蠕虫之类的攻击行为都能很好地防止,可是,对于在本地计算机的攻击却显得无能为力。对于本地攻击,攻击者很容易获得所需要的地址。

(2)造成内存碎片的增多

ASLR的运行时间越长,其带来的内存碎片也会越多,如果不及时进行清理,有可能会影响系统的效率,特殊情况下甚至会降低系统的稳定性。

当然,为了减少虚拟地址空间的碎片,操作系统把随机加载库文件的地址空间限制为8位,即地址空间为256,而且随机化发生在地址前两个有意义的字节上。

例如,对于地址:0x12345678,其存储方式如图3-33所示。

图3-33 地址存储方式

当启用了ASRL技术后,只有4 3和2 1是随机化的。在某些情况下,攻击者可以利用或者触发任意代码:当利用一个允许覆盖栈里返回地址的漏洞时,原来固定的返回地址被系统放在栈中;而如果启用ASLR,则地址被随机处理后才放入栈中,比如返回地址是0x12345678(0x1234是被随机部分,5678始终不变)。如果可以在0x1234XXXX(1234是随机的,并且操作系统已经把它们放在栈中了)空间中找到有用的跳转指令,如JMP ESP等,则只需要用这些找到的跳转指令的地址的低字节替换栈中的低字节即可,该方法也称为返回地址部分覆盖法。

(3)利用没有采用/DYNAMICBASE选项保护的模块作跳板

当前很多程序和DLL模块未采用/DYNAMICBASE连接选项进行分发,这就导致即使系统每次重启,也并非对所有应用程序地址空间分布都进行了随机化,仍然有模块的基地址没有发生变化。利用程序中没有启用ASLR的模块中的相关指令作为跳板,使得这些跳转指令的地址在重启前后一致,故用该地址来覆盖异常处理函数指针或返回地址即可绕过ASLR。

3.2.4 安全结构化异常处理SafeSEH

1.SafeSEH机制

为了防止SEH机制被攻击者恶意利用,微软通过在.Net编译器中加入/SafeSEH连接选项,从而正式引入了SafeSEH技术。

SafeSEH的实现原理较为简单,就是编译器在链接生成二进制IMAGE时,把所有合法的异常处理函数的地址解析出来制成一张安全的SEH表,保存在程序的IMAGE数据块里面,当程序调用异常处理函数时会将函数地址与安全SEH表中的地址进行匹配,检查调用的异常处理函数是否位于该表中。如果IMAGE不支持SafeSEH,则表的地址为0。

安全结构化异常处理(Safe Structured Exception Handling,SafeSEH)保护机制的作用是防止覆盖和使用存储栈上的SEH结构。如果使用/SafeSEH链接器选项编译和链接一个程序,那么对应二进制的头部将包含一个由所有合法异常处理程序组成的表,当调用异常处理程序时会检查这张表,以确保所需的处理程序在这张表中。这项检查工作是作为ntdll.dll中的RtlDispatchException例程的一部分来完成的,它会执行以下测试。

●确保异常记录位于当前线程的栈上。

●确保处理程序的指针没有指回栈。

●确保处理程序已经在经授权处理程序列表中登记。

●确保处理程序位于可执行的内存映像中。

由此可以看出,SafeSEH保护机制对于保护异常处理程序而言相当有效,但稍后将看到,它也并非绝对安全。

2.对抗SafeSEH机制的方法

SafeSEH是一种非常有效的漏洞利用防护机制,如果一个进程加载的所有模块都支持SafeSEH的IMAGE,覆盖SafeSEH进行漏洞利用就基本不可能。Windows 7下绝大部分的系统库都支持SafeSEH的IMAGE,但Windows XP/2003等绝大部分系统库不支持Windows的IMAGE。当进程中存在一个不支持SafeSEH的IMAGE时,整个SafeSEH的机制就很有可能失效。此外,由于支持SafeSEH需要.Net的编译器支持,现在仍有大量的第三方程序和库未使用.Net编译或者未采用/safeSEH链接选项,这就使得绕过SafeSEH成为可能。

(1)利用未启用SafeSEH的模块作为跳板进行绕过

对于目前的大部分Windows操作系统,其系统模块都受SafeSEH保护,可以选用未开启SafeSEH保护的模块来利用,比如漏洞软件本身自带的dll文件。在这些模块中寻找特定的某些跳转指令如pop/POP/ret等,用其地址进行SEH函数指针的覆盖,使得SEH函数被重定位到这些跳转指令,由于这些指令位于加载模块的IMAGE空间内,且所在模块不支持SHE,因此异常被触发时,可以执行到这些指令,通过合理安排shellcode,那么就有可能绕过SafeSEH机制,执行shellcode中的功能代码。

(2)利用加载模块之外的地址进行绕过

利用加载模块之外的地址进行绕过,包括从堆中进行绕过和从其他一些特定内存绕过。从堆中绕过,源于这样的缺陷:如果SEH中的异常处理函数指针指向堆区,则通常可以执行该异常处理函数,因此只需将shellcode布置到堆区就可以直接跳转执行。此外,如果在进程内存空间中的一些特定的、不属于加载模块的内存中找到跳转指令,则仍然可以用这些跳转指令的地址来覆盖异常处理函数的指针,从而绕过SafeSEH。

3.2.5 增强缓解体验工具包EMET

1.EMET机制

增强缓解体验工具包(Enhanced Mitigation Experience Toolkit,EMET)是微软推出的一套用来缓解漏洞攻击、提高应用软件安全性的增强型体验工具。与前面几种保护机制不同,EMET并不随Windows操作系统一起发布或预装,而是用户可自行选择安装,通过配置可实现对指定应用的增强型保护,但由于操作系统版本的差异性,不同版本的操作系统上所能提供的增强型保护机制也不尽相同,且目前仅支持Windows XP SP3及以上版本。

目前,微软官网提供EMET 5.5版本的下载(http://www.microsoft.com/emet),不过2018年7月31日以后,微软将不再提供该工具的更新支持,微软建议使用安全性更强的Windows 10系统。

EMET的基本保护功能介绍如下。

(1)增强型DEP

自Windows XP SP3起,操作系统内建支持DEP,但对于特定应用程序而言,则还与生成时所使用的编译连接选项有关,同时还需要结合OPT-in和OPT-out的配置来使DEP生效。而EMET则能够通过在指定应用中强制调用SetProcessPolicy来打开DEP保护,使其生效。

(2)SafeSEH的升级版——SEHOP

SEHOP正是看到了SafeSEH被绕过的可能性,从而增加的一项针对目标程序的运行时防护方案——在分发异常处理函数前,动态检验SEH链的完整性。需要说明的是:SEHOP技术在较新的操作系统中已经内建支持了,EMET对于这些版本的操作系统则更多的是一个完善和增强。

(3)强制性ASLR

在前面针对ASLR的介绍中已经提及,ASLR的防护能力的有效性和程序生成时是否采用/DYNAMICBASE连接选项有关,因此EMET对此进行了增强——EMET能够对生成时未使用/DYNAMICBASE连接选项的模块进行加载基址的强制随机化。实现思路则是对于那些动态加载的模块或延迟载入的模块,强制占用其首选基址,从而迫使DLL模块选择其他基址通过重定位的方式实现模块加载。

(4)HeapSpray防护

在前面的章节中,对HeapSpray攻击进行了介绍,针对这种很重要的攻击方式,EMET通过采用强制分配内存、占用常用攻击地址的方式来迫使HeapSpray攻击中的内存分配失效,从而挫败攻击。

【案例3】Windows安全漏洞保护技术应用

体验微软目前提供的/GS、DEP和ASLR漏洞利用阻断技术。

1)在Visual Studio 2013中新建一个Win32控制台应用程序,体验/GS保护机制的作用。

2)了解Windows系统中DEP功能的启用情况。

3)启用ASLR服务。

【案例3思考与分析】

1./GS保护机制体验

在Visual Studio 2013中新建一个Win32控制台应用程序,附加选项中将“安全开发生命周期(SDL)检查”取消设置。

运行结果如图3-34所示。由于默认开启了/GS编译选项,系统可以有效地检测到溢出并抛出异常,从而防止缓冲区溢出。

图3-34 【例3-7】运行结果

分别查看使用/GS选项和未使用/GS选项编译的VulnerableFunc函数反汇编代码,如表3-2所示。

表3-2 使用/GS选项和未使用/GS选项编译的反汇编代码比较

观察表3-2中使用/GS选项编译的VulnerableFunc函数反汇编代码可以发现,在函数调用发生时,向栈帧内压入一个额外的随机DWORD值,也就是安全Cookie,该值位于EBP之前,系统还将在内存区域中存放该值的一个副本。

在函数返回之前,系统执行一个额外的安全验证操作,称为Security Check Cookie。在检查过程中,系统比较栈中存放的安全Cookie和原先副本的值。本程序中由于name数组中的字符个数多于output字符数组的长度,产生了溢出,破坏了安全Cookie值,导致与原先副本值不吻合,说明栈核中的安全Cookie已被破坏,即栈中发生了溢出。编译器给出出错提示。

出于性能优化的考虑,Visual Studio会评估程序中哪些函数需要保护,通常只有当一个函数中包含字符串缓冲区或使用_alloc函数在栈上分配空间时,编译器才在栈中设置安全Cookie。此外,当缓冲区少于5个字节时,在栈中也不保存安全Cookie。

2.查询Windows系统中的DEP是否启用

如图3-35所示,在Windows 8.1系统中,打开“控制面板”,依次选择“系统和安全”→“系统”→“高级系统设置”→“高级”→“性能”页框中的“设置”,就能看到“数据执行保护”选项卡。

3.开启编译器选项/DYNAMICBASE

在Windows系统中,Windows Server 2008及以后版本默认情况下启用ASLR,但它仅适用于动态链接库和可执行文件。Windows Server 2008及以上版本都启用ASLR服务,但可以在项目属性中修改/dynamicbase属性,使其取消ASLR保护。

本例在Visual Studio 2013中通过开启和关闭/DYNAMICBASE选项作示例。首先关闭/DYNAMICBASE选项,如图3-36所示。

图3-35 Windows系统中DEP功能设置

图3-36 关闭/DYNAMICBASE选项

在Visual Studio 2013中新建一个Win32控制台应用程序,附加选项中取消选择“安全开发生命周期(SDL)检查”复选框。输入下列代码。

这段程序的目的是输出kerner32.dll基址、loadlibrary函数的入口地址,以及应用程序本身一个函数printAddress()的入口地址。

在不开启ASLR的情况下,选择“调试”→“开始执行(不调试)”命令,可以查看程序初次运行结果,如图3-37a所示,重启系统后,运行结果如图3-37b所示。

图3-37 关闭ASLR情况下的运行结果对比

a)初次运行结果 b) 重启系统后的运行结果

由图3-37可以看出,即使程序本身没有使用ASLR,Kernel32.dll加载地址也发生了变化,这是因为Kernel32.dll库已经选择了被ASLR保护,但是应用程序自身printAddress()函数的地址是固定的。

在开启ASLR的情况下,初次运行结果如图3-38a所示,重启系统后,运行结果如图3-38b所示。

图3-38 开启ASLR情况下运行结果对比

a)初次运行结果 b) 重启系统后的运行结果

由图3-38可以看出,应用程序自身函数printAddress()的加载地址随着系统重启发生了变化,即一旦使用了/DYNAMICBASE选项,生成的程序在运行时就会受到ASLR机制的保护。

拓展阅读

读者要想了解更多Windows内存攻击与保护的方法,可以阅读以下书籍资料。

[1]王清.0 day安全:软件漏洞分析技术[M].2版.北京:电子工业出版社,2011.

[2]DanielRegalado.灰帽黑客:正义黑客的道德规范、渗透测试、攻击方法和漏洞分析技术[M].4版.李枫,译.北京:清华大学出版社,2016. cFTrYuJ/sPdc6oYU5OLCqk7FMm/nVzwE3NzmeB2lwH3VlK1Ue+RYkJ/71KXInhNX

点击中间区域
呼出菜单
上一章
目录
下一章
×