作为与时俱进的Web狗,不学点二进制安全相关的东西就太狭隘了,所以翻出收藏夹收藏已久的Corelan Team二进制经典系列教程。本篇文章是我在完成这篇教程学习之后,重新总结梳理的记录,结合自己的认识来讲述栈溢出的SEH利用方式。

教程原文稍显繁琐而且文章顺序稍显混乱,我在重新梳理的过程中也舍去了一些冗余的内容(比如SEH结构被反复讲了3次)和一些我觉得没有必要的内容(比如通过Ollydbg观察SEH的过程)。如果大家想了解被这写内容,可以去阅读这篇原文:[《Exploit writing tutorial part 3 : SEH Based Exploits》]

(https://www.corelan.be/index.php/2009/07/25/writing-buffer-overflow-exploits-a-quick-and-basic-tutorial-part-3-seh/)。 至于为什么一上来就是教程的第三部分,因为第一部分学习之后的一段时间去处理其他事情,一直没来的及梳理(我难道要告诉你是因为我懒?),稍后会补上的。也希望自己能够完成这个系列教程的学习,并像这篇一样写成学习笔记分享出来。

0x01 什么是SHE

SEH为”Structured Exception Handler“的缩写,我们在编写程序时经常会遇到需要捕获某些异常进行特殊处理的情况,简单来说,就是try&catch语法。 我们简单来看下在栈中try&catch语法是怎么放置的:

SEH为”Structured Exception Handler“的缩写,我们在编写程序时经常会遇到需要捕获某些异常进行特殊处理的情况,简单来说,就是try&catch语法。

我们简单来看下在栈中try&catch语法是怎么放置的:

seh_in_stack

Windows有一套默认的SEH来捕获异常,如果Windows捕获到一个异常,你可以看到一个写着” xxx has encountered a problem and needs to close”的弹窗,这一般就是默认异常处理被触发的结果。显而易见,为了写出一套健壮的软件,在大多数情况下,开发人员会自己写代码捕获和处理这些异常(try&catch)。

为了应用程序能够捕获到异常后执行catch部分代码,异常处理指针会存储在每个代码块的栈中。如上图所示,每段代码都有它自己的栈结构,而异常处理作为栈的一部分存储在其中。换句话说,每个函数/程序都有一个栈结构,如果这个函数/程序自己实现了一个异常处理,那么这个异常处理就在这个函数/程序的栈结构中。

为了方便表达,之后的内容中使用EH(exception handler)来代表异常处理

EH以链表的形式存储在栈中,每一条SEH记录大小为8字节,包含2个元素:

  1. 指向下一条记录地址的指针(4字节)
  2. 指向处理当前异常的代码地址的指针(4字节)

下图为一个简单的SEH链条结构:

seh_structure

SEH链条的开始指针存在放在数据块的最开始位置(包括主函数、TEB(Thread Environment Block)或TIB(Thread Information Block)的数据块),所以SEH链条调用时经常会出现FS:[0]。在Intel机器中,如果你反汇编SEH的代码,就会看到类似MOV EAX,DWORD PTR FS:[0]这样的指令。在SEH链条的最末端,指针指向了FFFFFFFF,这代表程序并没有对这个异常进行处理,操作系统异常处理机制触发。

0x02 Windows的一些保护机制

XOR

从Windows XP SP1开始,当EH被调用时,操作系统会将所有的寄存器都会和它们自己进行一次异或操作,使它们都成为0x00000000,来确保你无法通过引用寄存器的方式找到你的payload。简单来说就是,EH触发,寄存器被清空了,我们无法简单的使用jmp eax这种方式完成利用了。

SafeSEH

SafeSEH是一些在编译阶段添加的保护机制,以阻止SEH覆盖的滥用。

有了XOR和SafeSEH的保护,我们不能通过jump寄存器的方式来跳到shellcode,这时就需要一些其他的指令来帮助我们达到jump到shellcode(指定地址)的目的。

从上面的栈结构图中我们可以得知,通过栈溢出可以将SEH链条进行覆盖。而每一条SEH记录的内容是两个指针,一个指向下一条记录,一个指向当前EH的处理函数,前者在栈上的位置是后者的上面。有了这些内容,我们可以打出这样的一套组合拳:

  1. 通过溢出覆盖栈中SEH,将指向下一条记录的指针覆盖为shellcode地址,将指向当前EH处理函数的指针指向一个带有“POP POP RET”操作的函数
  2. 触发一个EH
  3. 应用程序处理EH时调用了“POP POP RET”指令,将指向下一条记录的指针作为EIP内容
  4. 跳到shellcode地址,执行shellcode

exploit_flow

我们的输入大概是这样的:

对照上面的步骤,nSEH放置shellcode地址,SEH放置“POP POP RET”操作函数的地址。

0x03 触发&调试

这次实验使用的是Soritong MP3 player 1.0,发布于2009年7月20日。

首先使用python实现一个生成脚本(原文为perl),用以生成可以触发崩溃的文件:

把生成的文件放在安装路径下的skin/default目录中,使用windbg打开soritong.exe。

windbg_open

在程序加载完成后,执行之前,windbg会添加一个断点,让程序暂停,在最下面的输入框中输入g或者按下F5,运行程序。

windbg_run

windbg在捕获到异常后向程序插入断点,并提示“This exception may be expected and handled“。

windbg_expception

打印栈信息看一下:

可以在0012da64那行看到SEH链条中最终的ffffffff被调用,下面我们运行!analyze -v来看下:

EH处理指针指向了ffffffff,说明应用程序并没有实现相关的try&catch,下面我们打印一下TEB:

根据我们上面所说的,fs:[0]的开始部分就是指向SEH链条最开始部分的指针,我们接着打印下这个地址(0x0012FD64)下的内容:

里面全是A,我们再通过!exchain命令查看下EH链条的指向:

可以看出我们已经成功的覆盖了EH的处理指针,程序如果再向下执行,就会将0x41414141作为地址返回给EIP:

windbg_41

windbg_41

上面这张图,我们也可以看到eax、ebx、esi和edi已经被清空了。

0x04 利用

针对SEH的利用,在Windows XP SP1之前可以通过直接jmp到寄存器的方式跳转到shellcode,而在之后的版本,只能采用我们之前所说的”POP POP RET“方法。使用RET的方式覆盖EIP存在一定的稳定性问题,并且也可能会出现buffer大小不够的尴尬。但这通常都是值得的,每当你发现了一个栈溢出可以使你覆盖EIP,那你可以更深入的覆盖栈空间,尝试触发SEH链条。”更深入“意味着你讲填充更多的有效buffer空间,并且当你覆盖EIP的同时,一个异常被自动触发,将一个传统的exploit转变成一个SEH exploit。

现在我们再来回顾一下之前所说的利用思路:

实现过程中,我们需要先知道填充多少个字符到Junk buffer,才能到达next SEH的位置。这里我们使用msf提供的pattern_create.rb和pattern_offset.rb套装工具完成,首先使用pattern_create.rb生成一个5000字节长度的字符串,放到我们前面的python代码的junk变量中。

像之前一样执行,在windbg中我们在2次断点后使用!exchain命令查看当前EH指针的内容:

指针内容是41367441,通过python print来将它们的ascii形式打印:

再使用pattern_offset.rb查看位置:

这时我们知道了到达SE Handler需要填充588个字节,到达next Handler就是588-4=584字节。下面我们需要找到一个带有”POP POP RET“的函数地址,参照第一篇教程的方法,我们最好在应用程序自带的dll中找到带有这样功能的函数(好了,我知道你们又要吐槽我没写第一篇就写第三篇了,我会尽快的233)。

从windbg最开始的加载内容中,可以看到有一个C:\Program Files\SoriTong\Player.dll,我们先来看看它有没有我们想要的。这里我们使用msf提供的msfpescan来查看:

因为空字节00作为字符串的截止标识,所以我们在查找地址时应避免使用带有空字节的地址。这里我们选择0x10018de8,下面我们再来看下payload还缺什么:

next SEH还需要填充上shellcode的地址来实现跳转,那么shellcode的地址是是多少呢?我们需要重新生成一个ui.txt,来获取,在新生成的ui.txt中next SEH的位置我们暂时使用断点字符0xcc来代替,方便我们查看next SEH触发时shellcode的地址。

通过打印EIP地址内容,我们可以发现shellcode距离cc的位置刚好是next SEH + current SEH的距离。如此一来,我们就可以用jmp short指令通过偏移来完成跳转的工作,jmp short对应的机器码是eb占一个字节,后面需要添加一个偏移位置占一个字节,这样剩下的间隔应该就是6个字节,所以next SEH的位置应该填eb 06,但是由于next SEH占4字节,所以我们需要使用0x90来填充空余的两个字节。最后填充结构如下:

最终生成payload的python代码如下:

 

pwn

0x05 总结

总结本篇教程学习到的东西,有以下几个:

  1. SEH链条结构及其在栈中的位置
  2. Windows XP SP1添加的XOR和SafeSEH保护机制
  3. 通过msf的msfpescan从dll中查找带有想要操作的函数(“POP POP RET”)
  4. RET操作作为exploit方式的优缺点
  5. 通过windbg的”!analyze -v“ 和“!exchain”命令查看current SEH指针内容

如果您需要了解更多内容,可以
加入QQ群:570982169、486207500
直接询问:010-68438880-8669

 

源链接

Hacking more

...