偶尔分析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版本不一样也可能不同
msvc举例(win11)
iassam.dll里
触发异常, 理论上应该处理 v12, v9, v11, 跟踪其struc后:
可以看到有三个释放操作的rva. 所以它触发异常后, 应该就会调用这些析构操作.
上层函数ChangePassword::onSyncRequest
:
可以看到, 下半截是catch相关的操作, 上半截是析构相关的操作. 因此这个函数有try catch
.
而调试对栈回溯里
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
的位置.
旧版本msvc举例(server 2016)
这里只有个rva stru_1800321D0
, 跟入:
stru_180033B7C
,stru_180033B9C
就是上一个图里的下半截, 分别是析构函数, 和catch相关的函数.
导览图方法
在目标函数里直接ida看导览图, 就可以看到ida识别的异常处理流.
析构操作可能会以这样的形式存在:
其它
https://www.openrce.org/articles/full_view/21 这篇文章更详细地介绍了具体的情况, 我没有完全照着来, 有兴趣可以自己看一下.