3.1.1 缓冲区的工作原理

3.1.1 缓冲区的工作原理

1.进程内存组织

进程是已载入内存并受操作系统管理的程序实例的名字。如图3-1(a)所示,进程的内存一般分为代码段(Code Segment)、数据段(Data Segment)、堆(Heap)以及栈(Stack)等段。代码段包含了程序的指令,数据段包含了程序运行的一部分临时数据。它们可以被标记为只读[1],从而当试图对其对应的内存进行修改时,就会引发错误。数据段包含了初始化数据、未初始化数据、静态变量以及全局变量。堆则用于动态地分配进程内存。栈用于支持进程的执行。进程内存的精确组织形式依赖于操作系统、编译器、链接器以及载入器。图3.1(b)和图3.1(c)展示了UNIX和Win32上可能的进程内存组织形式。

图3.1 进程内存组织

2.栈管理

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

相对于广义的栈而言,系统栈则是操作系统在每个进程的虚拟内存空间中为每个线程划分出来的一片存储空间,它也同样遵守后进先出的栈操作原则,但是与一般的栈不同的是系统栈由系统自动维护,用于实现高级语言中函数的调用。对于类似C语言这样的高级语言,系统栈的PUSH和POP等堆栈平衡的细节相对于用户是透明的。此外,栈帧的生长方向一般是从高地址向低地址增长的,操作系统为进程中的每个函数调用都划分了一个称为栈帧的空间,每个栈帧都是一个独立的栈结构,而系统栈则是这些函数调用栈帧的集合。对于每个函数而言,其栈帧分布如图3.2所示。

图3.2 函数栈帧分布图

(1)局部变量:为函数中局部变量开辟的内存空间。

(2)栈帧状态值:保存前栈帧的顶部和底部,用于在函数调用结束后恢复调用者函数(caller function)的栈帧。实际上栈帧只保存前栈帧的底部,因为前栈帧的顶部可以通过对栈平衡计算得到。

(3)函数返回地址:保存当前函数调用前的“断点”信息,即函数调用指令的后面一条指令的地址,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

(4)函数的调用参数。

系统栈在工作的过程中主要用到了三个寄存器。

(1)ESP:栈指针寄存器(extended stack pointer),其存放的是当前栈帧的栈顶指针。

(2)EBP:基址指针寄存器(exteded base pointer),其存放的是当前栈帧的栈底指针。

(3)EIP:指令寄存器(extended instruction pointer),其存放的是下一条等待执行的指令地址。

如果控制了EIP寄存器的内容,就可以控制进程行为——通过设置EIP的内容,使CPU去执行我们想要执行的指令,从而劫持进程。

3.函数调用

进程中的函数调用主要通过以下几个步骤实现。

(1)参数入栈:将被调用函数的参数按照从右向左的顺序依次入栈。

(2)返回地址入栈:将call指令的下一条指令的地址入栈。

(3)代码区跳转:处理器从代码区的当前位置跳到被调用函数的入口处。

(4)栈帧调整:这主要包括保存当前栈帧状态、切换栈帧和给新栈帧分配空间。

下面的汇编代码就是一个典型的函数调用过程,其中后面三条指令实现栈帧调整。

执行上述指令后,进程内存中的栈帧状态如图3.3所示。

图3.3 执行函数调用指令后的栈帧状态图

类似地,函数返回步骤如下:

(1)根据需要保存函数返回值到EAX寄存器中(一般使用EAX寄存器存储返回值);

(2)降低栈顶,回收当前栈帧空间;

(3)恢复母函数栈帧;

(4)按照函数返回地址跳转回到父函数,继续执行。

具体指令序列如下:

4.堆内存管理

不同的操作系统对堆内存的管理机制略有不同,这里以Windows系统为例进行描述。Rtlheap是Windows操作系统的内存管理器,是Windows操作系统上大多数应用层的动态内存管理的心脏。与大多数软件一样,它也在不断地进化,不同的Windows版本通常都有不同的Rtlheap实现,它们的行为稍有不同。因此,Windows应用程序开发人员必须对目标平台上的Rtlheap实现的安全性仅作最低限度的假设。

要想理解误用内存管理API如何导致漏洞的发生,首先需要理解Win32中为了支持动态内存管理所使用的一些内部数据结构,包括进程环境块、空闲链表、look-aside链表以及内存块的结构等,这里以进程环境块和空闲链表为例进行介绍。

(1)进程环境块

Rtlheap数据结构的相关信息被存储在进程环境块(Process Environment Block,PEB)中。

PEB维护每一个进程的全局变量。PEB被每一个进程的线程环境块(Thread Environment Block,TEB)所引用,而TEB则被FS寄存器所引用。

PEB结构提供的定义来获取关于堆数据结构的信息,包括堆的最大数量、堆的实际数量、默认堆的位置,以及一个指向包含所有堆位置的数组的指针。这些数据结构之间的关系如图3.4所示。

图3.4 进程环境块与堆结构

(2)空闲链表

Rtlheap带来的安全问题有关的堆数据结构中最重要的一个就是位于堆起始(也就是调用Heap Create()返回的地址)偏移0X178处的包含有128个元素的数组,数组中每一个元素指向一个双链表。我们称这个数组为Freelist[]。这个链表被Rtlheap用来跟踪空闲内存块。Freelist[]是一个LIST_ENTRY结构的数组,每一个LIST-ENTRY表示一个双链表的头部。

一般来讲,操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确地释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入空闲链表中。