从本节开始,我们就要研究一些稍微高级点的话题了,如同在1.2节中看到的,Windows中为抵抗栈溢出做了很多保护性的检查工作,编译的程序默认开启了这些保护。如果我们不能绕过这些保护,那么我们的Shellcode也就是一个玩具而已,什么都做不了。
我们从SEH(结构化异常处理)开始。
这篇文章讲SEH简洁易懂:http://www.securitysift.com/windows-exploit-development-part-6-seh-exploits/
因此,本文的前面部分就直接对其进行翻译了,后面动手的部分再结合自己的例子进行,因为动手实践还是用自己写的代码好。
(1)什么是结构化异常处理?
Windows下的硬件和软件异常统一采用结构化异常处理(SEH)机制。异常处理结构通常包含在一个try/except或try/catch代码块中。如下:
/*****************************************************************************/
__try {
// 受保护的代码区域
...
}
__except (exception filter) {
// 异常处理代码
...
}
/*****************************************************************************/
含义很简单,try保护的代码一定会执行,在发生指定的错误/异常之后,就执行except中的代码,进行异常处理。异常处理器(exception filter)就是告诉操作系统对指定的错误/异常执行什么操作。
异常处理器(exception filter)可能由应用程序实现(通过__try/__except结构),或者使用系统自带的。由于错误的种类很多(除0,越界等),对应的异常处理器也有很多。
所有种类的异常处理器,包括应用程序实现和操作系统实现的,都由Windows系统通过一些数据结构和函数进行统一管理。
(2)SEH的主要组成
每个异常处理器都对应一个EXCEPTION_REGISTRATION_RECORD结构,该结构如下:
这些异常处理器的EXCEPTION_REGISTRATION_RECORD结构连接在一起,组成一个SEH链表。EXCEPTION_REGISTRATION_RECORD结构中的第一个成员Next指向SEH链表中的下一个成员,因此,你可以通过Next来遍历SEH链。EXCEPTION_REGISTRATION_RECORD结构中的第二个成员Handler为一个异常处理函数的函数指针,该异常处理函数定义如下:
函数的第一个参数指向一个_EXCEPTION_RECORD结构。该结构保存了某个异常的相关信息,包括异常码,异常发生的地址,参数的个数等,如下:
_except_handler异常处理函数使用该结构中的信息(还有ContextRecord 参数中的寄存器信息)来判断该异常能否被SEH链中的某个异常处理器处理。EstablisherFrame 参数也很重要,后面会说到。
_except_handler异常处理函数返回EXCEPTION_DISPOSITION,如果为ExceptionContinueExecution,表示该异常是否已经被成功处理,如果为ExceptionContinueSearch,表示当前异常处理器无法处理该异常,异常移交给SEH链中的下一个异常处理器。
那么,异常处理机制是如何使用这些结构和函数来进行异常处理的呢?当一个异常发生的时候,操作系统从SEH链头部开始,检查第一个_EXCEPTION_REGISTRATION_RECORD(即异常处理器)的异常处理函数,看它能否处理该异常(通过ExceptionRecord 和ContextRecord参数)。如果不能,则移动到下一个_EXCEPTION_REGISTRATION_RECORD,继续检查,直到找到合适的异常处理器。Windows在SEH链的末尾放置了一个默认的通用异常处理器,保证异常肯定能被处理。如果使用默认的异常处理器处理,你通常会看到“程序遇到了一个问题,需要关闭…”之类的信息。
每个线程有它自己的SEH链。操作系统通过TEB中的ExceptionList成员定位SEH链的起始地址,TEB位于FS:[0]。下面为SEH链的一个示意图(图中简化了_EXCEPTION_REGISTRATION_RECORD结构):
图47 Windows SEH链
上图不是SEH机制的全部,但是足够你理解基本的原理。现在,我们用一个示例来看一看SEH机制。
好了,翻译到此为止,但是我后面所写的内容基本也就是原文的内容,只是我换了自己的示例,这样便于实际操作,基本上也就相当于翻译。我们找出1.2节中的example_2(具有栈溢出漏洞的那个程序),来看看它的SEH是什么样的。在Immunity Debugger中选择如下菜单:
图48 在Immunity Debugger查看SEH链
即可查看SEH链。我们看一看example_2的SEH链:
图49 example_2的SEH链
SEH的try/except或try/catch代码块实际上是宏定义的一段代码,将我们自己的代码包裹起来,因此,我们可以从当前线程的栈上来找到SEH链,对照上面的地址,找到它:
图50 栈上的SEH链
对照前面讲述的EXCEPTION_REGISTRATION_RECORD结构,Next成员为链中的下一个异常处理器地址,为0xFFFFFFFF表示已经结尾,即最后的一个默认异常处理器。0x7c839ac0为该默认异常处理器的异常处理函数地址。
回看example_2的代码,我们并没有定义自己的异常处理块(try/except或try/catch),因此,程序自带一个默认异常处理器。前面说到,每个线程都有一个异常处理链,而线程是动态变化的,随着指令流的进行,执行不同的代码块,调用函数等。那么,程序执行起来又是什么样子的呢?
为了回答上面的问题,我们再来看一看。这个程序有输入字符串的操作(gets),因此,我们让程序运行,到达等待输入的时刻,然后再来看SEH链:
图51 暂停于gets时刻的SEH链
好大一串。其中有系统的,有VS2008的,还有一个我们“自己”的,最后才是系统默认的。这些异常都是用来干嘛的?现在,我们把断点设在调用gets函数之后:
图52
看来,刚刚我们应该是看错了位置。我们前面是在gets函数等待输入的时候看的,也就是说停在了gets函数内部,而gets函数由编译器实现,因此,它内部包装有自己的异常处理,这就是图51中为什么我们看到了那么多系统和编译器提供的异常处理函数。看来,SEH链是在动态变化的,进入了包装有异常处理的代码,就会在SEH链中添加异常处理器,退出其代码块之后,又会从SEH链中删除异常处理器。这就是为什么说SEH链是与线程对应的。但是,既然我们自己没有定义异常处理,这里为什么还多出来一个?这个后面再说。
接下来,我们给example_2的程序包装一个异常处理块,然后再看看SEH链的样子:
/*****************************************************************************/
// example_10: 演示SEH链
#include <Windows.h>
#include <stdio.h>
void get_print()
{
char str[11];
__try
{
gets(str);
printf("%s\n", str);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
//
}
}
int main()
{
get_print();
return 0;
}
/*****************************************************************************/
最初还是只有一个SEH链。同样在调用gets之后的语句暂停:
图55 example_10的SEH链
和图53对比,SEH链中多了一个节点,因为我们自己添加了一个异常处理块。现在还有一个疑问,多出来的那个是什么?按照SEH链的原理,局部的应该位于前面,因此,第一个是我们自己定义的,那第二个是哪里来的呢?(注意不要根据地址来和图53比较进行判断,现在已经是一个不同的程序了)它的异常处理函数地址为0x0041104B,明显位于本模块中。我们把断点设置调用 get_print()之前,也就是main函数中,来看:
图56
这个时候,第二个异常处理器就已经出现了,因此,这个异常处理器是main函数的,VC++实现main函数的时候也包装了一个异常处理块。你可以自己去找到是何时设置的。
我们来看看两个异常处理函数的地址,分别为0x411046和0x41104B:
图56
图57
第一个指向MSVCR90D.dll中的_except_handler3,第二个最终指向MSVCR90D.dll中的_except_handler4_common。这是VC++对SEH的实现,并非使用原生的SEH,要理解这个_except_handler3和_except_handler4_common,你需要这篇文章:https://www.microsoft.com/msj/0197/exception/exception.aspx。这篇经典的文章有中文翻译。
本节先到这里,下一节继续。