偶尔分析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’查看引用:

1712127361714

跟进.pdata的引用:

1712127336162

跟进 stru_14002DFAC:

1712127420884

可以看到多个析构函数.

再次调试

开启调试, 分别对所有偏移的函数下断点

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

1712127520943

第二个命中的是unique_ptr的析构函数, 释放的是myObjectPtr2

1712127599116

第三个命中的直接是Strobj的析构函数, 释放的是oops3,

1712127849317

第四个是oops2

最后是oops

1712127884472

从断点命中情况来看, 它命中了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.

1712128404597

1712128467028

可以看到 main的.pdata里对应的stru_14002E010里, 有main的异常处理的代码块的rva. 这里因为有4个catch, 所以有4个异常处理块.

所以我们也可以发现, 在try catch操作中, 存在catch的函数, 它的stru指向里就有catch的操作模块, 也会给出异常的类型.

对于包含在其中的其它调用子函数, 如果存在自动释放的对象, 也会自动调用析构函数, 而且析构函数是比catch先调用的. 但是对于不会自动释放的对象, 就需要我们注意它的释放情况了.

Debug的版本会略有不同. MSVC版本不一样也可能不同

另外, 异常里的rdx指向的是触发异常时的rsp指针

msvc举例(win11)

iassam.dll里

1712112112479

触发异常, 理论上应该处理 v12, v9, v11, 跟踪其struc后:

1712112164618

1712112155063

可以看到有三个释放操作的rva. 所以它触发异常后, 应该就会调用这些析构操作.

上层函数ChangePassword::onSyncRequest:

1712112295271

1712113662570

可以看到, 下半截是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)

1712114532284

1712114562160

这里只有个rva stru_1800321D0, 跟入:

1712114602198

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef const struct _s_FuncInfo
{
unsigned int magicNumber:29;//19930522h // Identifies version of compiler
unsigned int bbtFlags:3; // flags that may be set by BBT processing
__ehstate_t maxState;// 4 // Highest state number plus one (thus
// number of entries in unwind map)
int dispUnwindMap;// rva stru_180033B7C // Image relative offset of the unwind map
unsigned int nTryBlocks;// 1 // Number of 'try' blocks in this function
int dispTryBlockMap;// rva stru_180033B9C // Image relative offset of the handler map
unsigned int nIPMapEntries;// 12 // # entries in the IP-to-state map. NYI (reserved)
int dispIPtoStateMap;// rva stru_180033BE0 IPtoStateMap // Image relative offset of the IP to state map. rva of struct IptoStateMapEntry
int dispUwindHelp;// 32 // Displacement of unwind helpers from base
int dispESTypeList;// 0 // Image relative list of types for exception specifications
int EHFlags;// 1 // Flags for some features.
} FuncInfo;

这里 stru_1800321D0 对应的结构体就是如上所示.

UnwindMapEntry:

1712134784970

1
2
3
4
struct UnwindMapEntry {
int toState; // target state
void (*action)(); // action to perform (unwind funclet address)
};

可以看到当state为1转为0时, 会调用_ChangePassword__onSyncRequest____1___dtor$0.

TryBlockMapEntry:

1712134847779

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; // this try {} covers states ranging from tryLow to tryHigh
int catchHigh; // highest state inside catch handlers of this try
int nCatches; // number of catch handlers
HandlerType* pHandlerArray; //catch handlers table
};

struct HandlerType {
// 0x01: const, 0x02: volatile, 0x08: reference
DWORD adjectives;

// RTTI descriptor of the exception type. 0=any (ellipsis)
TypeDescriptor* pType;

// ebp-based offset of the exception object in the function stack.
// 0 = no object (catch by type)
int dispCatchObj;

// address of the catch handler code.
// returns address where to continues execution (i.e. code after the try block)
void* addressOfHandler;
};

IPtoStateMap:

1712136119507

在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的, 但是前一个字节是什么意义, 我还不太懂.

导览图方法

1712115551300

在目标函数里直接ida看导览图, 就可以看到ida识别的异常处理流.

析构操作可能会以这样的形式存在:

1712115649867

其它

另外, 形如:

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() { /* ... */ }
// 其他成员函数和变量
};

// 创建一个对象并使用 unique_ptr 管理
std::unique_ptr<MyClass> createUniqueObject() {
return std::unique_ptr<MyClass>(new MyClass());
}

int main() {
std::unique_ptr<MyClass> myUniquePtr = createUniqueObject();
// 使用 myUniquePtr ...
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() { /* ... */ }
// 其他成员函数和变量
};

// 创建一个对象并使用 shared_ptr 管理
std::shared_ptr<MyClass> createSharedObject() {
return std::shared_ptr<MyClass>(new MyClass());
}

int main() {
std::shared_ptr<MyClass> mySharedPtr = createSharedObject();
// 使用 mySharedPtr ...
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的差异