偶尔分析C++的模块, 遇到触发异常操作, 但是不知道它SEH到底干啥了, 所以研究了下MSVC下的c++异常处理到底是怎么回事, 没理解透彻, 但是逆向应该是够用了, 如果有不对之处, 还望指正.
C++ try catch 这是一段示例的测试代码:
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 #include <iostream> #include <exception> #include <cstring> using namespace std;#pragma warning ("disable" :4996) struct ExceptionA : public exception{ ExceptionA (int a, int b) :a (a), b (b) {} int a, b; }; struct ExceptionB : public exception{ ExceptionB (int a, int b) {} }; struct ExceptionC : public exception{ ExceptionC (int a, int b) {} }; class Strobj {public : Strobj () = delete ; Strobj (char * a) { int len = strlen (a); str_ = new char [len + 1 ]; strcpy_s (str_, len+1 , a); } char * str_ = NULL ; ~Strobj () { if (str_) { delete str_; } } }; Strobj doThrow (bool doth) { int a = 1 , b = 2 ; char str[] = "123456" ; Strobj oops (str) ; if (doth) throw ExceptionA (a, b); return oops; } int main () { try { Strobj a = doThrow (true ); std::cout << a.str_ << std::endl; } catch (ExceptionC& e) { std::cout << "ExceptionC caught" << std::endl; } catch (ExceptionB& e) { std::cout << "ExceptionB caught" << std::endl; } catch (ExceptionA& e) { std::cout << "ExceptionA caught" << std::endl; } catch (std::exception& e) { } }
这段代码很简单, 就是申请一个对象后触发异常. 正常情况下, 在触发异常后, 它会调用析构函数释放str_
, 然后调用异常模块输出 “ExceptionA caught”.
这里简要介绍一下SEH, SEH就是异常捕获流程, 当程序发生异常的时候, 会跳转到最近(指最近的try catch)异常捕获函数, 它可以获取触发异常时的寄存器数据, 通过寄存器就知道触发异常时的原因, 如果能处理, 就修改异常时的rip值来跳转到后续的正常指令流, 如果不能处理就交给下一个SEH handler.
所以如果要知道触发异常后它怎么操作, 就看它注册的seh handler就行. 不过呢, msvc的C++ 对SEH做了封装, 异常由 _CxxThrowException
抛出. 处理会由__GSHandlerCheck_EH4
进行简单操作后调用__CxxFrameHandler4
进行处理
调试 我们在调试器里对析构函数下断点, 触发断点后, 查看栈回溯, 得到如下:
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 test.exe!`doThrow'::`1'::dtor$0() vcruntime140_1d.dll!00007ff990de1030() vcruntime140_1d.dll!00007ff990de4307() vcruntime140_1d.dll!00007ff990de66ab() vcruntime140_1d.dll!00007ff990de2cd2() vcruntime140_1d.dll!00007ff990de2f5a() vcruntime140_1d.dll!00007ff990de6dfb() test.exe!__GSHandlerCheck_EH4(_EXCEPTION_RECORD * ExceptionRecord=0x000000473ff0de60, void * EstablisherFrame=0x000000473ff0f9d0, _CONTEXT * ContextRecord=0x000000473ff0d760, _DISPATCHER_CONTEXT * DispatcherContext=0x000000473ff0dcd0) 行 73 在 D:\a\_work\1\s\src\vctools\crt\vcstartup\src\gs\amd64\gshandlereh4.cpp(73) ntdll.dll!00007ff9c5bb242f() ntdll.dll!00007ff9c5b40939() vcruntime140_1d.dll!00007ff990de6a7f() vcruntime140_1d.dll!00007ff990de1c1e() vcruntime140_1d.dll!00007ff990de218b() vcruntime140_1d.dll!00007ff990de2ec5() vcruntime140_1d.dll!00007ff990de2f5a() vcruntime140_1d.dll!00007ff990de6dfb() test.exe!__GSHandlerCheck_EH4(_EXCEPTION_RECORD * ExceptionRecord=0x000000473ff0eff0, void * EstablisherFrame=0x000000473ff0fbc0, _CONTEXT * ContextRecord=0x000000473ff0eb00, _DISPATCHER_CONTEXT * DispatcherContext=0x000000473ff0e980) 行 73 在 D:\a\_work\1\s\src\vctools\crt\vcstartup\src\gs\amd64\gshandlereh4.cpp(73) ntdll.dll!00007ff9c5bb23af() ntdll.dll!00007ff9c5b614b4() ntdll.dll!00007ff9c5bb0ebe() KernelBase.dll!00007ff9c341cf19() vcruntime140d.dll!00007ff96105bbf1() test.exe!doThrow(bool doth=true) 行 45 在 D:\Projects\test\test\main.cpp(45) test.exe!main() 行 52 在 D:\Projects\test\test\main.cpp(52)
可以看到, 它先调用的__GSHandlerCheck_EH4
两次, 再调用了````doThrow’::`1’::dtor$0```操作.
而且析构函数是先于catch模块调用的. C++没有finally
关键词, 也是因为有析构函数, 就不需要finally了
修改 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 #include <iostream> #include <exception> #include <cstring> using namespace std;#pragma warning ("disable" :4996) struct ExceptionA : public exception{ ExceptionA (int a, int b) :a (a), b (b) {} int a, b; }; struct ExceptionB : public exception{ ExceptionB (int a, int b) {} }; struct ExceptionC : public exception{ ExceptionC (int a, int b) {} }; class Strobj { static int index; public : Strobj () { index += 1 ; char a[] = "abcdefghijklmnopq" ; char * s = &a[index]; int len = strlen (s); str_ = new char [len + 1 ]; strcpy_s (str_, len + 1 , s); } Strobj (char * a) { int len = strlen (a); str_ = new char [len + 1 ]; strcpy_s (str_, len+1 , a); } char * str_ = NULL ; ~Strobj () { if (str_) { delete str_; str_ = NULL ; } } }; int Strobj::index = 0 ;Strobj doThrow (bool doth) { int a = 1 , b = 2 ; char str[] = "123456" ; Strobj oops (str) ; Strobj oops2 (&str[1 ]) ; Strobj oops3 (&str[2 ]) ; Strobj* myObjectPtr = new Strobj; auto myObjectPtr2 = std::make_unique <Strobj>(); auto myObjectPtr3 = std::make_shared <Strobj>(); if (doth) throw ExceptionA (a, b); std::cout << oops2.str_ << std::endl; std::cout << oops3.str_ << std::endl; std::cout << myObjectPtr->str_ << std::endl; std::cout << myObjectPtr2->str_ << std::endl; std::cout << myObjectPtr3->str_ << std::endl; delete myObjectPtr; return oops; } int main () { try { Strobj a = doThrow (true ); std::cout << a.str_ << std::endl; } catch (ExceptionC& e) { std::cout << "ExceptionC caught" << std::endl; } catch (ExceptionB& e) { std::cout << "ExceptionB caught" << std::endl; } catch (ExceptionA& e) { std::cout << "ExceptionA caught" << std::endl; } catch (std::exception& e) { std::cout << "ExceptionA1 caught" << std::endl; } catch (...) { std::cout << "ExceptionA2 caught" << std::endl; } }
这次新增了两个Strobj
对象, 新增了, release
编译后, 使用ida看看有什么不一样.
选择doThrow
函数, 按’X’查看引用:
跟进.pdata
的引用:
跟进 stru_14002DFAC
:
可以看到多个析构函数.
再次调试 开启调试, 分别对所有偏移的函数下断点
1 2 3 4 5 6 7 8 ??1?$shared_ptr@VStrobj@@@std@@QEAA@XZ _doThrow____1___dtor$10 ??1?$unique_ptr@VStrobj@@U?$default_delete@VStrobj@@@std@@@std@@QEAA@XZ _doThrow____1___dtor$7 _doThrow____1___dtor$3 ??1Strobj@@QEAA@XZ ??1Strobj@@QEAA@XZ _doThrow____1___dtor$0
继续运行, 看看触发情况:
第一个命中的是最后的一个shared_ptr的析构函数, 释放的是myObjectPtr3
第二个命中的是unique_ptr的析构函数, 释放的是myObjectPtr2
第三个命中的直接是Strobj的析构函数, 释放的是oops3
,
第四个是oops2
最后是oops
从断点命中情况来看, 它命中了stru_14002DFAC
里的部分析构函数
1 2 3 4 5 ??1?$shared_ptr@VStrobj@@@std@@QEAA@XZ ??1?$unique_ptr@VStrobj@@U?$default_delete@VStrobj@@@std@@@std@@QEAA@XZ ??1Strobj@@QEAA@XZ ??1Strobj@@QEAA@XZ _doThrow____1___dtor$0
但是, 这里有一个问题, 那就是myObjectPtr
并没有得到释放!!
我们用debug调试, 记录myObjectPtr->str_
的地址, 当命中catch时, 查看该值, 发现没有被重置, 再次说明它并没有被释放
因为它是new申请的, 需要我们手动释放, 而它又是个局部变量, 我们没法在catch里正常释放它, 导致它的内存丢失了.
再来看main.
可以看到 main的.pdata
里对应的stru_14002E010
里, 有main的异常处理的代码块的rva
. 这里因为有4个catch, 所以有4个异常处理块.
所以我们也可以发现, 在try catch操作中, 存在catch的函数, 它的stru指向里就有catch的操作模块, 也会给出异常的类型.
对于包含在其中的其它调用子函数, 如果存在自动释放的对象, 也会自动调用析构函数, 而且析构函数是比catch先调用的. 但是对于不会自动释放的对象, 就需要我们注意它的释放情况了.
Debug的版本会略有不同. MSVC版本不一样也可能不同
另外, 异常里的rdx
指向的是触发异常时的rsp
指针
msvc举例(win11) iassam.dll里
触发异常, 理论上应该处理 v12, v9, v11, 跟踪其struc后:
可以看到有三个释放操作的rva. 所以它触发异常后, 应该就会调用这些析构操作.
上层函数ChangePassword::onSyncRequest
:
可以看到, 下半截是catch相关的操作, 上半截是析构相关的操作. 因此这个函数有try catch
.
下面是ChangePassword::doChangePassword
的栈回溯:
1 2 3 4 5 6 00 00000097`ea97f020 00007ff9`e6a76ad2 iassam!ChangePassword::doChangePassword+0x9c 01 00000097`ea97f090 00007ff9`e6a759dc iassam!ChangePassword::onSyncRequest+0xb2 02 00000097`ea97f0e0 00007ff9`e6a75948 iassam!IASTL::IASRequestHandlerSync::onAsyncRequest+0x7c 03 00000097`ea97f110 00007ff9`e846311c iassam!IASTL::IASRequestHandler::OnRequest+0xa8 04 00000097`ea97f140 00007ff9`e8462e3f iaspolcy!Pipeline::executeNext+0x154
对这些位置下断点, 在ChangePassword::doChangePassword
里面触发异常, 可以看到最后命中的是IASTL::IASRequestHandlerSync::onAsyncRequest+0x7C
(即 7ff9e6a759dc
)的位置.
说明触发异常后, ChangePassword::onSyncRequest
里处理了函数异常, 所以正常返回到了上层函数call之后的位置.
旧版本msvc举例(server 2016)
这里只有个rva stru_1800321D0
, 跟入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef const struct _s_FuncInfo { unsigned int magicNumber:29 ; unsigned int bbtFlags:3 ; __ehstate_t maxState; int dispUnwindMap; unsigned int nTryBlocks; int dispTryBlockMap; unsigned int nIPMapEntries; int dispIPtoStateMap; int dispUwindHelp; int dispESTypeList; int EHFlags; } FuncInfo;
这里 stru_1800321D0
对应的结构体就是如上所示.
UnwindMapEntry
:
1 2 3 4 struct UnwindMapEntry { int toState; void (*action)(); };
可以看到当state为1转为0时, 会调用_ChangePassword__onSyncRequest____1___dtor$0
.
TryBlockMapEntry
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct TryBlockMapEntry { int tryLow; int tryHigh; int catchHigh; int nCatches; HandlerType* pHandlerArray; }; struct HandlerType { DWORD adjectives; TypeDescriptor* pType; int dispCatchObj; void * addressOfHandler; };
IPtoStateMap
:
在x86中, 它是在栈中显式地声明当前的state, 在x64中, 通过 IptoStateMapEntry
隐式地展示当前的state. 所以当到达表里的rva时, 表示当前的state为多少. 比如这里就是到达loc_180006A99
时, state为 2.
对比win10及以上的版本, 会发现有明显的不一样的情况.(以下是win11版本的unwind map)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .rdata:0000000180035698 byte_180035698 db 78h ; DATA XREF: .rdata:0000000180035694↑o .rdata:0000000180035698 ; FuncInfo4 .rdata:0000000180035699 dd rva byte_1800356A5 ; unwind map .rdata:000000018003569D dd rva byte_1800356B9 ; try block map .rdata:00000001800356A1 dd rva byte_1800356DB ; ip2state map .rdata:00000001800356A5 byte_1800356A5 db 0Ah ; DATA XREF: .rdata:0000000180035699↑o .rdata:00000001800356A5 ; num unwind entries: 5 .rdata:00000001800356A6 db 8 ; funclet type: 0 .rdata:00000001800356A7 db 0Ah ; funclet type: 1 .rdata:00000001800356A8 dd rva ??1IASRequest@IASTL@@QEAA@XZ ; funclet .rdata:00000001800356AC db 50h ; frame offset of object ptr to be destructed .rdata:00000001800356AD db 32h ; funclet type: 1 .rdata:00000001800356AE dd rva ??1IASAttribute@IASTL@@QEAA@XZ ; funclet .rdata:00000001800356B2 db 0C0h ; frame offset of object ptr to be destructed .rdata:00000001800356B3 db 6Eh ; funclet type: 3 .rdata:00000001800356B4 dd rva __std_terminate ; funclet
byte_1800356A5
的unwind map, 是有rva的, 但是前一个字节是什么意义, 我还不太懂.
导览图方法
在目标函数里直接ida看导览图, 就可以看到ida识别的异常处理流.
析构操作可能 会以这样的形式存在:
其它 另外, 形如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <memory> class MyClass {public : MyClass () { } ~MyClass () { } }; std::unique_ptr<MyClass> createUniqueObject () { return std::unique_ptr <MyClass>(new MyClass ()); } int main () { std::unique_ptr<MyClass> myUniquePtr = createUniqueObject (); return 0 ; }
和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <memory> class MyClass {public : MyClass () { } ~MyClass () { } }; std::shared_ptr<MyClass> createSharedObject () { return std::shared_ptr <MyClass>(new MyClass ()); } int main () { std::shared_ptr<MyClass> mySharedPtr = createSharedObject (); return 0 ; }
这种, 也都会自动触发析构操作, 即使发生了异常也会如此.
参考 https://www.openrce.org/articles/full_view/21 介绍基本原理
https://reactos.org/wiki/Techwiki:SEH64 第一个引用中涉及到的结构体出处
https://learn.microsoft.com/en-us/cpp/build/exception-handling-x64?view=msvc-160 一些相关结构体
http://www.hexblog.com/wp-content/uploads/2012/06/Recon-2012-Skochinsky-Compiler-Internals.pdf 介绍x64与x86的差异