成员函数
普通成员函数
调用约定是 __thiscall
,即 ecx 寄存器存 this
指针,内平栈。
a.f(1, 2, 3);
008A1050 push 3
008A1052 push 2
008A1054 push 1
008A1056 lea ecx,[a]
008A1059 call Struct::f (08A1010h)
在不开优化的前提下如果使用 __stdcall
修饰成员函数则 this
指针作为第一个参数栈传参。
a.f(1, 2, 3);
000C1050 push 3
000C1052 push 2
000C1054 push 1
000C1056 lea eax,[a]
000C1059 push eax
000C105A call Struct::f (0C1010h)
同理,使用 __fastcall
和 __cdecl
修饰成员函数后成员函数满足对应的调用约定,只不过 this
指针当做函数的第一个参数。
构造函数
栈对象
栈对象即 Class 在栈上实例化出的 Object 。
栈对象的构造函数是对应类的作用域中第一个被调用的成员函数,调用约定是 __thiscall
。
Class a = Class();
007C1128 lea ecx,[a]
007C112B call Class::Class (07C10C0h)
构造函数的返回值是 this
指针。
Class() {
007C10C0 push ebp
007C10C1 mov ebp,esp
007C10C3 push ecx
007C10C4 mov dword ptr [this],ecx
puts("Construct");
007C10C7 push offset string "Construct" (07C2120h)
007C10CC call dword ptr [__imp__puts (07C20BCh)]
007C10D2 add esp,4
}
007C10D5 mov eax,dword ptr [this] ; 返回 this 指针
007C10D8 mov esp,ebp
007C10DA pop ebp
007C10DB ret
因此构造函数反编译代码如下:
Class *__thiscall Class::Class(Class *this)
{
_puts("Construct");
return this;
}
堆对象
堆对象即 Class 在堆上实例化出的 Object 。
由于堆对象没有作用域概念,因此构造函数是在 new
之后(编译器自动添加)调用的,同理析构函数是在 delete
前调用的。
Class* a=new Class();
00EC1065 push 30h
00EC1067 call operator new (0EC111Ch) ; new 一块 0x30 大小的内存
00EC106C add esp,4
00EC106F mov dword ptr [ebp-10h],eax ; 把 new 到的指针给一个临时变量 temp
00EC1072 mov dword ptr [ebp-4],0 ; [ebp-4] 是 TryLevel,实际上调用构造函数的代码外面包着一层异常处理。
00EC1079 cmp dword ptr [ebp-10h],0 ; 判断内存是否分配成功,如果分配失败则跳过构造函数。
00EC107D je main+4Ch (0EC108Ch)
00EC107F mov ecx,dword ptr [ebp-10h] ; 取出 [ebp-10h] 存放的 Object 地址作为 this 指针 temp
00EC1082 call Class::Class (0EC1000h) ; 调用构造函数
00EC1087 mov dword ptr [ebp-14h],eax
00EC108A jmp main+53h (0EC1093h)
00EC108C mov dword ptr [ebp-14h],0 ; 如果 new 分配内存失败会将存放构造函数返回值的栈上局部变量置 0 表示没有调用构造函数
00EC1093 mov eax,dword ptr [ebp-14h]
00EC1096 mov dword ptr [ebp-1Ch],eax
00EC1099 mov dword ptr [ebp-4],0FFFFFFFFh ; TryLevel 置为 -1 表示已经不在 try...catch... 范围了。
00EC10A0 mov ecx,dword ptr [ebp-1Ch]
00EC10A3 mov dword ptr [a],ecx ; [ebp-14h] -> eax -> [ebp-1Ch] -> ecx -> [a]
不考虑异常处理可反编译成如下 C++ 代码:
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class *a; // [esp+14h] [ebp-14h]
Class *temp; // [esp+18h] [ebp-10h]
temp = (Class *)operator new(0x30u);
if ( temp )
a = Class::Class(temp);
else
a = 0;
if ( a )
Class::`scalar deleting destructor'(a, 1u);
return 0;
}
全局对象(静态对象)
以动态链接程序为例,在程序启动后有如下调用链:mainCRTStartup -> _scrt_common_main_seh
。
在 _scrt_common_main_seh
函数中有如 _initterm_e
和 _initterm
函数,这个两个函数分别是 C 和 C++ 的初始化函数。以 _initterm
函数为例,这个函数会依次调用 __xc_a
和 __xc_z
之间的函数指针。
__scrt_current_native_startup_state = initializing;
if ( _initterm_e(__xi_a, __xi_z) )
return 255;
_initterm(__xc_a, __xc_z);
__scrt_current_native_startup_state = initialized;
我们看到在这些函数指针中有一个 _dynamic_initializer_for__a__
函数,这个函数主要做了两件事:
- 调用全局对象的构造函数
- 调用
_atexit
注册_dynamic_atexit_destructor_for__a__
函数以便在程序结束的时候调用该函数析构全局对象。
; int dynamic_initializer_for__a__()
_dynamic_initializer_for__a__ proc near
push ebp
mov ebp, esp
mov ecx, offset ?a@@3VClass@@A ; this
call ??0Class@@QAE@XZ ; Class::Class(void)
push offset _dynamic_atexit_destructor_for__a__ ; function
call _atexit
add esp, 4
pop ebp
retn
_dynamic_initializer_for__a__ endp
反编译代码如下:
int dynamic_initializer_for__a__()
{
Class::Class(&a);
return atexit(dynamic_atexit_destructor_for__a__);
}
对象数组
如果定义一个栈上对象数组则程序会依次调用数组中每个对象的构造函数,而 Visual C++ 编译器会将这一过程定义为一个函数 eh vector constructor iterator
:
push offset ??1Class@@QAE@XZ ; destructor
push offset ??0Class@@QAE@XZ ; constructor
push 0Ah ; count
push 30h ; '0' ; size
lea eax, [ebp+a]
push eax ; ptr
call ??_L@YGXPAXIIP6EX0@Z1@Z ; `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))
反编译代码如下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class a[10]; // [esp+4h] [ebp-1E4h] BYREF
`eh vector constructor iterator'(
a,
0x30u,
0xAu,
(void (__thiscall *)(void *))Class::Class,
(void (__thiscall *)(void *))Class::~Class);
`eh vector destructor iterator'(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class);
return 0;
}
eh vector constructor iterator
的参数分别是:
- 对象数组的首地址
- 对象的大小
- 对象的数量
- 构造函数地址
- 析构函数地址
该函数的会依次为每个对象调用构造函数。
void __stdcall `eh vector constructor iterator'(
char *ptr,
unsigned int size,
unsigned int count,
void (__thiscall *constructor)(void *),
void (__thiscall *destructor)(void *))
{
int i; // ebx
for ( i = 0; i != count; ++i )
{
constructor(ptr);
ptr += size;
}
}
从 IDA 反编译结果来看 destructor
函数指针没有用到,但实际上这里有一个异常处理,即构造出现异常时会调用 __ArrayUnwind
函数将已初始化的对象析构。
.text:004011C3 ; void __stdcall `eh vector constructor iterator'(char *ptr, unsigned int size, unsigned int count, void (__thiscall *constructor)(void *), void (__thiscall *destructor)(void *))
.text:004011C3 ??_L@YGXPAXIIP6EX0@Z1@Z proc near ; CODE XREF: _main+28↑p
.text:004011C3
.text:004011C3 i= dword ptr -20h
.text:004011C3 success= byte ptr -19h
.text:004011C3 ms_exc= CPPEH_RECORD ptr -18h
.text:004011C3 ptr= dword ptr 8
.text:004011C3 size= dword ptr 0Ch
.text:004011C3 count= dword ptr 10h
.text:004011C3 constructor= dword ptr 14h
.text:004011C3 destructor= dword ptr 18h
.text:004011C3
.text:004011C3 ; __unwind { // __SEH_prolog4
.text:004011C3 push 10h
.text:004011C5 push offset ScopeTable
.text:004011CA call __SEH_prolog4
.text:004011CA
.text:004011CF xor ebx, ebx
.text:004011D1 mov [ebp+i], ebx
.text:004011D4 mov [ebp+success], bl ; 初始化局部变量 success 为 false
.text:004011D7 ; __try { // __finally(HandlerFunc)
.text:004011D7 mov [ebp+ms_exc.registration.TryLevel], ebx
.text:004011D7
.text:004011DA
.text:004011DA LOOP: ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+35↓j
.text:004011DA cmp ebx, [ebp+count]
.text:004011DD jz short SUCCESS
.text:004011DD
.text:004011DF mov ecx, [ebp+constructor] ; Target
.text:004011E2 call ds:___guard_check_icall_fptr ; _guard_check_icall_nop(x)
.text:004011E2
.text:004011E8 mov ecx, [ebp+ptr] ; void *
.text:004011EB call [ebp+constructor]
.text:004011EB
.text:004011EE mov eax, [ebp+size]
.text:004011F1 add [ebp+ptr], eax
.text:004011F4 inc ebx
.text:004011F5 mov [ebp+i], ebx
.text:004011F8 jmp short LOOP
.text:004011F8
.text:004011FA ; ---------------------------------------------------------------------------
.text:004011FA
.text:004011FA SUCCESS: ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+1A↑j
.text:004011FA mov al, 1 ; 更新 al 寄存器和局部变量 success 为 true
.text:004011FC mov [ebp+success], al
.text:004011FC ; } // starts at 4011D7
.text:004011FF mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:00401206 call HandleIfFailure ; 调用
.text:00401206
.text:0040120B ; ---------------------------------------------------------------------------
.text:0040120B
.text:0040120B END: ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *)):RETN↓j
.text:0040120B mov ecx, [ebp+ms_exc.registration.Next]
.text:0040120E mov large fs:0, ecx
.text:00401215 pop ecx
.text:00401216 pop edi
.text:00401217 pop esi
.text:00401218 pop ebx
.text:00401219 leave
.text:0040121A retn 14h
.text:0040121A
.text:0040121D ; ---------------------------------------------------------------------------
.text:0040121D
.text:0040121D HandlerFunc: ; DATA XREF: .rdata:ScopeTable↓o
.text:0040121D ; __finally // owned by 4011D7 ; 设置 ebp 为 i,这是需要调用析构函数的对象的数量。
.text:0040121D mov ebx, [ebp+i]
.text:00401220 mov al, [ebp+success] ; 设置 al 为 局部变量 success 即 false
.text:00401220
.text:00401223
.text:00401223 HandleIfFailure: ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+43↑j
.text:00401223 test al, al
.text:00401225 jnz short RETN ; 如果 al 为 true 则直接返回
.text:00401225
.text:00401227 push [ebp+destructor] ; destructor
.text:0040122A push ebx ; count
.text:0040122B push [ebp+size] ; size
.text:0040122E push [ebp+ptr] ; ptr
.text:00401231 call ?__ArrayUnwind@@YGXPAXIIP6EX0@Z@Z ; 否则调用 __ArrayUnwind 函数将已初始化的对象析构
.text:00401231
.text:00401236
.text:00401236 RETN: ; CODE XREF: `eh vector constructor iterator'(void *,uint,uint,void (*)(void *),void (*)(void *))+62↑j
.text:00401236 retn
.text:00401236 ; } // starts at 4011C3
.text:00401236
.text:00401236 ??_L@YGXPAXIIP6EX0@Z1@Z endp
对于堆上对象数组也是调用 eh vector constructor iterator
函数构造,大致逻辑如下,因为是数组,所以申请的内存的前 4 字节用来记录数组中成员的个数。
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class *v4; // [esp+14h] [ebp-14h]
_DWORD *block; // [esp+18h] [ebp-10h]
block = operator new[](0x1E4u);
if ( block )
{
*block = 10;
`eh vector constructor iterator'(
block + 1,
0x30u,
0xAu,
(void (__thiscall *)(void *))Class::Class,
(void (__thiscall *)(void *))Class::~Class);
v4 = (Class *)(block + 1);
}
else
{
v4 = 0;
}
if ( v4 )
Class::`vector deleting destructor'(v4, 3u);
return 0;
}
对于全局对象数组,则是在 dynamic_initializer_for__a__
函数中调用 eh vector constructor iterator
函数。
int dynamic_initializer_for__a__()
{
`eh vector constructor iterator'(
a,
0x30u,
0xAu,
(void (__thiscall *)(void *))Class::Class,
(void (__thiscall *)(void *))Class::~Class);
return atexit(dynamic_atexit_destructor_for__a__);
}
析构函数
栈对象
栈对象有如下特点:
- 对应类的作用域中最后一个被调用的成员函数。(通常用来判断类的作用于结束位置)
- 只有一个参数
this
指针,且用 ecx 传参。(实际上也是__thiscall
调用约定) - 析构函数没有返回值。(或者说返回值为 0)
- 如果没有重载析构函数那么一般析构函数都会被优化掉。
因此声明一个局部变量对象的反编译代码如下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class a; // [esp+4h] [ebp-34h] BYREF
Class::Class(&a);
Class::~Class(&a);
return 0;
}
显式调用析构函数不会直接调用类的析构函数而是调用析构代理函数 scalar deleting destructor
。这个函数会根据参数是否为 1 决定是否调用 delete
函数释放 Object 。由于是栈上的对象不需要释放,因此传入的参数为 0 。
a.~Class(); // Class::`scalar deleting destructor'(&a, 0);
007C1147 push 0
007C1149 lea ecx,[a]
007C114C call Class::`scalar deleting destructor' (07C1190h)
return 0;
007C1151 mov dword ptr [ebp-44h],0
007C1158 mov dword ptr [ebp-4],0FFFFFFFFh
007C115F lea ecx,[a]
007C1162 call Class::~Class (07C10E0h)
007C1167 mov eax,dword ptr [ebp-44h]
在析构函数外面包裹的一层析构代理函数 scalar deleting destructor
会根据传入的参数是否为 1 决定是否调用 delete
函数释放对象,不过真正的析构函数一定会被调用。
ConsoleApplication2.exe!Class::`scalar deleting destructor'(unsigned int):
008211C0 push ebp
008211C1 mov ebp,esp
008211C3 push ecx
008211C4 mov dword ptr [this],ecx
008211C7 mov ecx,dword ptr [this]
008211CA call Class::~Class (08210E0h) ; 调用 Object 真正的析构函数,同样也是 thiscall
008211CF mov eax,dword ptr [ebp+8] ; 获取析构函数传入的参数
008211D2 and eax,1
008211D5 je Class::`scalar deleting destructor'+25h (08211E5h) ; 如果传入的参数为 0 则为显式调用析构函数,因此直接跳过 delete 。
008211D7 push 30h
008211D9 mov ecx,dword ptr [this]
008211DC push ecx
008211DD call operator delete (082122Ch) ; 调用 delete 函数释放 Object,采用 thiscall 调用约定。
008211E2 add esp,8
008211E5 mov eax,dword ptr [this] ; 返回值为 this 指针。
008211E8 mov esp,ebp
008211EA pop ebp
008211EB ret 4
该函数反编译代码如下:
Class *__thiscall Class::`scalar deleting destructor'(Class *this, bool need_free)
{
Class::~Class(this);
if ( need_free )
operator delete(this, 0x30u);
return this;
}
堆对象
由于析构函数可以被显式调用,因此析构函数还会在栈上传一个参数。如果是显式调用则会传一个 1 ,否则传一个 0 。
例如这段代码:
int main() {
Class* a=new Class();
a->~Class();
delete a;
return 0;
}
对应汇编分析如下:
a->~Class();
002F10A6 push 0 ; 参数为 0 表示显式调用析构函数。
002F10A8 mov ecx,dword ptr [a] ; this 指针
002F10AB call Class::`scalar deleting destructor' (02F10F0h) ; 调用析构代理函数
delete a;
002F10B0 mov edx,dword ptr [a]
002F10B3 mov dword ptr [ebp-1Ch],edx
002F10B6 cmp dword ptr [ebp-1Ch],0 ; 判断 this 指针是否为空,如果为空则跳过析构和 delete。
002F10BA je main+8Bh (02F10CBh)
002F10BC push 1 ; 参数为 1 表示隐式调用析构函数。
002F10BE mov ecx,dword ptr [ebp-1Ch] ; this 指针
002F10C1 call Class::`scalar deleting destructor' (02F10F0h) ; 调用析构代理函数
002F10C6 mov dword ptr [ebp-24h],eax ; 析构函数返回值保存在 [ebp-24h] 中
002F10C9 jmp main+92h (02F10D2h) ; 直接跳转到函数返回
002F10CB mov dword ptr [ebp-24h],0 ; 因为 this 指针为空,因此将析构函数执行结果置为 0 。
return 0;
002F10D2 xor eax,eax
因此反编译代码如下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class *a; // [esp+14h] [ebp-14h]
Class *temp; // [esp+18h] [ebp-10h]
temp = (Class *)operator new(0x30u);
if ( temp )
a = Class::Class(temp);
else
a = 0;
Class::`scalar deleting destructor'(a, 0);
if ( a )
Class::`scalar deleting destructor'(a, 1u);
return 0;
}
如果没有重写类的析构函数,那么编译器会优化掉所有显式调用析构函数的代码,而隐式调用析构函数会被优化成直接调用 delete
函数释放 Object 。
a->~Class();
delete a;
001B1156 mov eax,dword ptr [a]
001B1159 mov dword ptr [ebp-1Ch],eax
001B115C push 30h
001B115E mov ecx,dword ptr [ebp-1Ch]
001B1161 push ecx
001B1162 call operator delete (01B11D5h)
001B1167 add esp,8
001B116A cmp dword ptr [ebp-1Ch],0
001B116E jne main+99h (01B1179h)
001B1170 mov dword ptr [ebp-24h],0
001B1177 jmp main+0A6h (01B1186h)
001B1179 mov dword ptr [a],8123h
001B1180 mov edx,dword ptr [a]
001B1183 mov dword ptr [ebp-24h],edx
反编译代码如下:
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class *a;
Class *temp;
temp = (Class *)operator new(0x30u);
if ( temp )
a = Class::Class(temp);
else
a = 0;
operator delete(a, 0x30u);
return 0;
}
全局对象(静态对象)
dynamic_initializer_for__a__
函数调用 _atexit
注册的 _dynamic_atexit_destructor_for__a__
函数会直接调用析构函数。
.text:00401DB0 ; void __cdecl dynamic_atexit_destructor_for__a__()
.text:00401DB0 _dynamic_atexit_destructor_for__a__ proc near
.text:00401DB0 ; DATA XREF: _dynamic_initializer_for__a__+D↑o
.text:00401DB0 55 push ebp
.text:00401DB1 8B EC mov ebp, esp
.text:00401DB3 B9 78 33 40 00 mov ecx, offset ?a@@3VClass@@A ; this
.text:00401DB8 E8 33 F3 FF FF call ??1Class@@QAE@XZ ; Class::~Class(void)
.text:00401DB8
.text:00401DBD 5D pop ebp
.text:00401DBE C3 retn
反编译代码如下:
void __cdecl dynamic_atexit_destructor_for__a__()
{
Class::~Class(&a);
}
dynamic_initializer_for__a__
之所以调用 _atexit
不直接注册析构函数是因为析构函数需要传入 this 指针,即全局对象地址,而 _atexit
注册的函数不能有参数。
对象数组
与构造相似,如果一个栈上对象数组作用域结束则程序会依次调用数组中每个对象的析构函数,而 Visual C++ 编译器会将这一过程定义为一个函数 eh vector destructor iterator
:
push offset ??1Class@@QAE@XZ ; destructor ; `eh vector destructor iterator'(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class);
push 0Ah ; count
push 30h ; '0' ; size
lea ecx, [ebp+a]
push ecx ; ptr
call ??_M@YGXPAXIIP6EX0@Z@Z ; `eh vector destructor iterator'(void *,uint,uint,void (*)(void *))
该函数的参数分别是:
- 数组首地址
- 对象大小
- 对象数量
- 析构函数地址
eh vector destructor iterator
函数会依次为对象数组中的每个对象调用析构函数。异常处理过程就不具体分析了。
void __stdcall `eh vector destructor iterator'(
char *ptr,
unsigned int size,
unsigned int count,
void (__thiscall *destructor)(void *))
{
unsigned int v4; // edi
char *i; // esi
v4 = count;
for ( i = &ptr[count * size]; v4--; destructor(i) )
i -= size;
}
对于全局对象数组则是在 dynamic_atexit_destructor_for__a__
函数中调用 eh vector destructor iterator
函数。
void __cdecl dynamic_atexit_destructor_for__a__()
{
`eh vector destructor iterator'(a, 0x30u, 0xAu, (void (__thiscall *)(void *))Class::~Class);
}
而对于堆上对象数组如果指向该数组的指针不为空则会调用 vector deleting destructor
函数。
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class *v4; // [esp+14h] [ebp-14h]
_DWORD *block; // [esp+18h] [ebp-10h]
block = operator new[](0x1E4u);
if ( block )
{
*block = 10;
`eh vector constructor iterator'(
block + 1,
0x30u,
0xAu,
(void (__thiscall *)(void *))Class::Class,
(void (__thiscall *)(void *))Class::~Class);
v4 = (Class *)(block + 1);
}
else
{
v4 = 0;
}
if ( v4 )
Class::`vector deleting destructor'(v4, 3u);
return 0;
}
该函数会先调用 eh vector destructor iterator
析构对象数组中的每个成员,之后调用 delete
函数释放内存。
Class *__thiscall Class::`vector deleting destructor'(Class *this, char a2)
{
if ( (a2 & 2) != 0 )
{
`eh vector destructor iterator'(this, 0x30u, this[-1].z[9], (void (__thiscall *)(void *))Class::~Class);
if ( (a2 & 1) != 0 )
operator delete[](&this[-1].z[9], 48 * this[-1].z[9] + 4);
return (Class *)((char *)this - 4);
}
else
{
Class::~Class(this);
if ( (a2 & 1) != 0 )
operator delete(this, 0x30u);
return this;
}
}
对象的传递
对象作为参数
指针\引用对象传参
无论是指针还是引用传参,都是直接把对象地址作为参数传入。
foo(a);
00B71117 lea eax,[a]
00B7111A push eax
00B7111B call foo (0B710C0h)
00B71120 add esp,4
对象传参
浅拷贝
如果类里面没有实现拷贝构造,那么直接将对象作为参数传递就是浅拷贝。
浅拷贝和结构体传参类似,都是把整个对象复制到参数位置。
foo(a);
00111119 sub esp,30h
0011111C mov ecx,0Ch
00111121 lea esi,[a]
00111124 mov edi,esp
00111126 rep movs dword ptr es:[edi],dword ptr [esi]
00111128 call foo (01110C0h)
0011112D add esp,30h
对象传参但时是在调用函数中会将传入的对象进行析构。
void foo(Class a) {
001110C0 push ebp
001110C1 mov ebp,esp
printf("%d", a.x);
001110C3 mov eax,dword ptr [a]
001110C6 push eax
001110C7 push 112120h
001110CC call printf (0111040h)
001110D1 add esp,8
}
001110D4 lea ecx,[a]
001110D7 call Class::~Class (01110A0h)
001110DC pop ebp
001110DD ret
深拷贝
如果对象中存在些指针指向申请的内存那么浅拷贝会将这些指针复制一份,而在函数内部析构的时候会调用析构函数将这些内存释放。如果调用完函数之后再使用对象内的这些指针指向的内存就会造成 UAF 。
因此这里需要实现拷贝构造函数 Class(const Class& a)
。这里参数 a
必须是 const
类型,否则无法传参 const
类型的对象。
#include <iostream>
class Class {
public:
int x, y, z[10];
char *pMem;
Class() {
pMem = new char[0x100];
}
Class(const Class& a) {
pMem = new char[0x100];
memcpy(pMem, a.pMem, 0x100);
x = a.x;
y = a.y;
for (int i = 0; i < 10; i++) {
z[i] = a.z[i];
}
}
~Class() {
delete[] pMem;
}
};
void foo(Class a) {
printf("%d", a.x);
}
int main() {
const Class a;
foo(a);
return 0;
}
此时我们看到再调用 foo
函数前首先会在栈上开辟一块对象所需的内存空间,之后会调用拷贝构造函数,最后调用 foo
函数。由于拷贝构造函数的调用约定 __thiscall
是内平栈,因此调用完拷贝构造函数之后栈顶就是已经完成初始化的对象参数,作为下一步调用的 foo
函数的参数。
foo(a);
00D01241 sub esp,34h
00D01244 mov ecx,esp ; 传入拷贝构造函数的 this 指针指向栈上开辟出的一块对象所需的内存空间
00D01246 mov dword ptr [ebp-4Ch],esp
00D01249 lea eax,[a]
00D0124C push eax ; 传入拷贝构造函数的局部变量 a 的地址
00D0124D call Class::Class (0D010F0h)
00D01252 call foo (0D011E0h)
00D01257 add esp,34h
拷贝构造函数本质上就是前面浅拷贝拷贝对象内存的操作交由用户去实现。
void __thiscall Class::Class(Class *this, const Class *a)
{
int i; // [esp+Ch] [ebp-8h]
this->pMem = (char *)operator new[](0x100u);
qmemcpy(this->pMem, a->pMem, 0x100u);
this->x = a->x;
this->y = a->y;
for ( i = 0; i < 10; ++i )
this->z[i] = a->z[i];
}
同样在 foo
函数中会析构传入的对象。
void __cdecl foo(Class a)
{
printf("%d", a.x);
Class::~Class(&a);
}
对象作为返回值
指针对象
直接返回对象的地址。
int main() {
007E1120 push ebp
007E1121 mov ebp,esp
007E1123 push ecx
Class* b = foo();
007E1124 call foo (07E1060h)
007E1129 mov dword ptr [b],eax
return 0;
007E112C xor eax,eax
}
007E112E mov esp,ebp
007E1130 pop ebp
007E1131 ret
不过如果取返回的对象指针的值赋值给局部变量会形成对象拷贝。
int main() {
Class b = *foo();
return 0;
}
int __cdecl main()
{
const Class *v0; // eax
Class b; // [esp+50h] [ebp-38h] BYREF
__CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
v0 = foo();
Class::Class(&b, v0);
Class::~Class(&b);
return 0;
}
临时对象
例如下面这段代码:
Class foo() {
Class a;
return a;
}
int main() {
foo();
return 0;
}
实际上是向 foo
函数传递一个 main
函数的局部变量地址,然后再 foo
函数内部构造,在 main
函数析构。这里的 __autoclassinit2
实际上是将对象初始化为全 0 。
在一些版本的编译器中 foo
函数可能会实现为构造一个局部变量 a
然后浅拷贝到函数外部的临时对象,但依旧满足函数内构造,函数外析构的原则。
Class *__cdecl foo(Class *result)
{
Class::__autoclassinit2(result, 0x34u);
Class::Class(result);
return result;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
Class result; // [esp+0h] [ebp-34h] BYREF
foo(&result);
Class::~Class(&result);
return 0;
}
如果是使用一个局部变量保存返回的对象:
int main() {
Class b = foo();
puts("main end");
return 0;
}
那么该局部变量析构的时间由局部变量作用域决定。
int __cdecl main()
{
Class b; // [esp+50h] [ebp-38h] BYREF
__CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
foo(&b);
_puts("main end");
Class::~Class(&b);
return 0;
}
引用对象
例如下面这段代码:
Class &foo() {
static Class a;
return a;
}
int main() {
Class &b = foo();
return 0;
}
本质还是返回对象的指针。
Class *__cdecl foo()
{
__CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
if ( _TSS0 > *(_DWORD *)(*((_DWORD *)NtCurrentTeb()->ThreadLocalStoragePointer + _tls_index) + 260) )
{
j___Init_thread_header(&_TSS0);
if ( _TSS0 == -1 )
{
Class::Class(&a);
j__atexit(foo_::_2_::_dynamic_atexit_destructor_for__a__);
j___Init_thread_footer(&_TSS0);
}
}
return &a;
}
Class &b = foo();
00C91973 call foo (0C913F2h)
00C91978 mov dword ptr [b],eax
但是如果我们让 b
不在是引用:
int main() {
Class b = foo();
return 0;
}
那么会存在一个拷贝构造:
int __cdecl main()
{
const Class *v0; // eax
Class b; // [esp+50h] [ebp-38h] BYREF
__CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
v0 = foo();
Class::Class(&b, v0);
Class::~Class(&b);
return 0;
}
无名对象
无名对象就是不用变量存对象,而是直接返回:
Class foo() {
return Class();
}
int main() {
Class a = foo();
return 0;
}
这种本质和返回临时对象一样:
Class *__cdecl foo(Class *result)
{
__CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
Class::Class(result);
return result;
}
int __cdecl main()
{
Class a; // [esp+50h] [ebp-38h] BYREF
__CheckForDebuggerJustMyCode(&1C2F97D9_ConsoleApplication2_cpp);
foo(&a);
Class::~Class(&a);
return 0;
}
RTTI(运行时类型信息)
typeid 是 C++ 中的运算符,用于获取对象的类型信息。
typeid
运算符接受一个表达式作为参数,并返回一个表示该表达式类型的 std::type_info
对象。std::type_info
类定义在 <typeinfo>
头文件中。
std::type_info
类提供了一些成员函数和操作符,用于比较类型信息。以下是一些常用的成员函数:
name()
:返回一个指向类型名称的 C 字符串。raw_name()
:返回一个指向类型名称的内部字符串,该字符串可能包含特定于实现的修饰符和命名约定。这个原始名称是特定于编译器和平台的。hash_code()
:用于获取类型信息的哈希码。before(const std::type_info& rhs)
:比较类型信息之间的顺序。如果当前类型在rhs
之前,则返回true
;否则返回false
。before() 函数的比较结果是特定于实现的,并且不受 C++ 标准的具体规定。不同的编译器和平台可能会有不同的比较策略和结果。
为了实现这一功能,Visual C++ 会定义 RTTI 相关结构来存储类相关符号。
这就是为什么 IDA 即使没有符号依旧能识别出虚表的名称:
_DWORD *__thiscall sub_412D50(_DWORD *this)
{
__CheckForDebuggerJustMyCode(&unk_41C067);
*this = &CVirtual::`vftable';
this[1] = 1;
this[2] = 2;
puts("CVirtual()");
return this;
}
低版本的 Visual C++ 只有在使用 typeid
相关功能的时候才会出现 RTTI 相关结构,而高版本默认开启 RTTI 。可以在 Visual C++ 的 项目设置 -> 配置属性 -> C/C++ -> 所有选项 -> 启用运行时类型信息
开启或关闭 RTTI ,但是即使关闭如果使用 typeid
或者使用 try...catch
(catch
有数据类型,一般在类的构造析构代码中编译器会自动加异常处理)会强制开启 RTTI ,不过不会与虚表关系起来,也就是 IDA 不能识别虚表符号。
关于 RTTI 具体结构见异常处理相关结构。
虚函数
虚函数(Virtual Function)是面向对象编程中的一个重要概念,用于实现多态性(Polymorphism)。为了实现虚函数,Visual C++ 编译器引入了虚表和虚函数等结构。
注意:构造函数不能写为虚函数,但是析构函数可以写为虚函数。因为虚函数的调用依赖于对象的类型信息,而构造函数在创建对象时用于初始化对象的状态,此时对象的类型尚未确定。因此,构造函数不能是虚函数。
这里使用如下代码来介绍虚函数:
#include <iostream>
class CVirtual {
public:
CVirtual() {
m_nMember1 = 1;
m_nMember2 = 2;
puts("CVirtual()");
}
virtual ~CVirtual() {
puts("~CVirtual()");
}
virtual void fun1() {
puts("fun1()");
}
virtual void fun2() {
puts("fun2()");
}
private:
int m_nMember1;
int m_nMember2;
};
int main() {
CVirtual object;
object.fun1();
object.fun2();
return 0;
}
虚表
在构造函数中会初始化 CVirtual
中的虚表指针 __vftable
指向 .rdata
段中的虚表 CVirtual::vftable
。
struct __cppobj CVirtual
{
CVirtual_vtbl *__vftable /*VFT*/;
int m_nMember1;
int m_nMember2;
};
void __thiscall CVirtual::CVirtual(CVirtual *this)
{
this->__vftable = (CVirtual_vtbl *)CVirtual::`vftable';
this->m_nMember1 = 1;
this->m_nMember2 = 2;
_puts("CVirtual()");
}
其中虚表的类型和虚表的定义如下:
struct /*VFT*/ CVirtual_vtbl
{
void (__thiscall *~CVirtual)(CVirtual *this);
void (__thiscall *fun1)(CVirtual *this);
void (__thiscall *fun2)(CVirtual *this);
};
void (__cdecl *const ??_7CVirtual@@6B@[4])() =
{
&CVirtual::`vector deleting destructor',
&CVirtual::fun1,
&CVirtual::fun2,
NULL
};
在析构函数代码如下,可以看到再析构函数开始的地方会将类的虚表指针指向该析构函数对应的类的虚表。因为在存在继承的类中子类析构之后在调用子类的虚函数可能会访问到已释放的资源造成 UAF,因此需要再析构函数中还原虚表指针。
void __thiscall CVirtual::~CVirtual(CVirtual *this)
{
this->__vftable = (CVirtual_vtbl *)CVirtual::`vftable';
_puts("~CVirtual()");
}
因此虚表指针有如下特征:
- 构造函数赋值虚表指针。
- 析构函数还原虚表指针。
即使我们不实现构造析构函数,为了安全起见编译器还是会自动生成构造析构函数来赋值和还原虚表指针。不过这种函数比较短,通常被优化内联到代码中,不以单独函数存在。
另外我们发现虚表中记录的析构函数不是对象真正的析构函数,而是析构代理函数 vector deleting destructor
。
CVirtual *__thiscall CVirtual::`vector deleting destructor'(CVirtual *this, char a2)
{
CVirtual::~CVirtual(this);
if ( (a2 & 1) != 0 )
operator delete(this, 0xCu);
return this;
}
根据对上述代码的分析可知,虚表及在内存中的布局如下:
虚表的特征总结如下:
- 不考虑继承的情况下一个类至少有一个虚函数才会存在虚表。
- 不同类的虚表不同,相同类的对象共享一个虚表。
- 虚表不可修改,通常存放在全局数据区,由编译器生成。
- 虚表的结尾不一定为 0 因此从逆向角度不能确定虚表的范围。
- 虚表由函数指针构成。
- 虚表的成员函数顺序按照类中函数声明的顺序排列。
- 对象首地址处保存虚表指针。
虚函数的调用
调用声明虚函数的成员函数实际上是直接 call
的函数地址,没有查虚表。
.text:004011B7 lea ecx, [ebp+object] ; this
.text:004011BA call ?fun1@CVirtual@@UAEXXZ ; CVirtual::fun1(void)
成员函数必须产生多态才会通过虚表调用成员函数。成员函数产生多态的条件有:
- 是虚函数:成员函数必须在基类中声明为虚函数(使用 virtual 关键字),以便在派生类中进行覆盖(override)。
- 使用指针或者使用引用:成员函数必须通过指针或引用进行调用,而不是直接通过对象进行调用。这样,编译器会在运行时根据实际对象的类型来确定要调用的虚函数。
只要满足这两个条件,即便是在析构函数中也可以进行多态(强转指针)。
另外强转指针是一个很危险的操作,以下面这段代码为例,虽然强转成 CDerived
但是虚表用的还是 CBase
的虚表,因此调用 CDerived
中的函数可能会调用到其它函数或者无效的函数指针。
CBase base;
((CDerived *) &base)->fun2();
例如我们将 main
函数改为下面这种形式:
int main() {
CVirtual object;
CVirtual *p_object = &object;
p_object->fun1();
p_object->fun2();
return 0;
}
这时候成员函数是通过虚表调用的。
.text:0040111D 8B 4D E0 mov ecx, [ebp+p_object] ; ecx 是 CVirtual 的地址
.text:00401120 8B 11 mov edx, [ecx] ; edx 是虚表地址
.text:00401122 8B 4D E0 mov ecx, [ebp+p_object] ; ecx 是 CVirtual 的地址
.text:00401125 8B 42 04 mov eax, [edx+4] ; eax 是函数 fun1 的地址
.text:00401128 FF D0 call eax ; 调用 fun1
继承
单重继承
这里使用如下代码来介绍单重继承:
#include <iostream>
class CBase {
public:
CBase() {
m_nMember = 1;
puts(__FUNCTION__);
}
virtual ~CBase() {
puts(__FUNCTION__);
}
virtual void fun1() {
puts(__FUNCTION__);
}
virtual void fun3() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
class CDerived : public CBase {
public:
CDerived() {
m_nMember = 2;
puts(__FUNCTION__);
}
~CDerived() {
puts(__FUNCTION__);
}
virtual void fun1() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
private:
int m_nMember;
CBase base;
};
int main() {
CDerived Derived;
return 0;
}
构造析构顺序
在类的构造和析构过程中,并不仅仅执行用户定义的构造和析构函数,还涉及到其他构造和析构操作的顺序。
构造顺序:
- 构造基类
- 构造成员对象(对象内部定义的一些成员变量)
- 构造自身
析构顺序:
- 析构自身
- 析构成员对象
- 析构基类
这里有以下几点需要注意:
- 构造析构顺序通常是我们还原类的继承关系的一个重要依据,不过这里要区分基类和成员对象。
- 区分基类和成员对象的构造可以根据传入的
this
指针。基类传的是整个对象的地址,而成员对象传的是成员变量的地址。如果这两个地址相同就根据代码可读性还原。
- 区分基类和成员对象的构造可以根据传入的
- 基类的构造一定在修改虚表指针之前,而成员对象的构造时间看编译器版本。
- 对于老版本编译器(例如 VC 6.0)成员对象的构造在修改虚表之前。
- 对于新版本编译器成员对象的构造在修改虚表之后。(也可以作为区分基类和成员对象的一个依据)
构造函数:
void __thiscall CBase::CBase(CBase *this)
{
this->__vftable = (CBase_vtbl *)CBase::`vftable';
this->m_nMember = 1;
_puts("CBase::CBase");
}
void __thiscall CDerived::CDerived(CDerived *this)
{
// 构造基类
CBase::CBase(this);
this->__vftable = (CDerived_vtbl *)CDerived::`vftable';
// 构造成员对象
CBase::CBase(&this->base);
// 构造自身
this->m_nMember = 2;
_puts("CDerived::CDerived");
}
析构函数:
void __thiscall CBase::~CBase(CBase *this)
{
this->__vftable = (CBase_vtbl *)CBase::`vftable';
_puts("CBase::~CBase");
}
void __thiscall CDerived::~CDerived(CDerived *this)
{
this->__vftable = (CDerived_vtbl *)CDerived::`vftable';
// 析构自身
_puts("CDerived::~CDerived");
// 析构成员对象
CBase::~CBase(&this->base);
// 析构基类
CBase::~CBase(this);
}
内存结构
派生类的虚表填充过程:
- 复制基类的虚表(函数顺序不变)。
- 如果派生类虚函数中有覆盖基类的虚函数(与基类的对应函数同名同参),使用派生类的虚函数地址覆盖对应表项。
- 如果派生类有新增的虚函数,将其放在虚表后面。
派生类的对象填充过程:
- 虚表指针指向派生类对应的虚表。
- 将派生类新增的成员放到基类的成员后面。
因此示例代码中的 CBase
和 CDerived
类的实例化的对象和虚表结构如下:
虚表函数重合是还原类继承关系的一个重要依据。
多重继承
这里使用如下代码来介绍多重继承:
#include <iostream>
class CBase1 {
public:
CBase1() {
m_nMember = 1;
puts(__FUNCTION__);
}
virtual ~CBase1() {
puts(__FUNCTION__);
}
virtual void fun1() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
virtual void fun3() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
class CBase2 {
public:
CBase2() {
m_nMember = 2;
puts(__FUNCTION__);
}
virtual ~CBase2() {
puts(__FUNCTION__);
}
virtual void fun1() {
puts(__FUNCTION__);
}
virtual void fun4() {
puts(__FUNCTION__);
}
virtual void fun5() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
class CDerived : public CBase1,public CBase2 {
public:
CDerived() {
m_nMember = 3;
puts(__FUNCTION__);
}
~CDerived() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
virtual void fun4() {
puts(__FUNCTION__);
}
virtual void fun6() {
puts(__FUNCTION__);
}
private:
int m_nMember;
CBase1 base1;
CBase2 base2;
};
int main() {
CDerived Derived;
return 0;
}
内存结构
下图是上面的示例代码的虚表结构:
多重继承对象的虚表结构规律:
- 将继承的对象的虚表各复制一份(顺序不变)。
- 如果派生类的函数重写了其中一个对象的函数(函数名同参数类型的虚函数)则将该函数地址覆盖到对应虚表的对应函数上。
- 如果派生类的虚函数没有重写任何一个父类的函数则将该函数追加到第一个虚表后面(例如调用虚表 2 的函数相当于把对象当
CBase2
解析了,如果是没有重写的函数就不会有问题,但是如果是重写的函数需要将对象按照CDerived
解析,为了提高效率就将新增的虚函数放到虚表 1 中)。 - 如果派生类的虚函数同时重写多个对象的函数则将所有重写的函数对应虚表的对应函数指针都覆盖为重新的函数。不过对于虚表指针不在对象开头的虚表该函数由于调用时传入的是
this + 8
因此需要一个代理函数将传入的地址转换为整个对象的地址。(示例代码就是同时重写了两个父类的析构函数) - 如果继承的两个父类有同样的虚函数,不会出现语法错误,例如示例代码中的
fun1()
。但是如果调用了这种函数,由于编译器无法确定到底调用了哪个函数,因此会报语法错误。
多重继承对象的结构规律:
- 按照继承顺序(例如
class A:piblic B, public C
的继承顺序是B
,C
)将父类的成员依次拷贝到对象中。 - 将派生类新增的成员放到基类的成员后面。
- 将所有父类的虚表指针指向对应复制并修改后的虚表,修改规则见多重继承对象的虚表结构规律。
构造析构顺序
构造顺序:
- 构造第一个基类
- 构造第二个基类
- 构造成员对象
- 构造自身
实例代码的构造函数如下:
void __thiscall CDerived::CDerived(CDerived *this)
{
// 构造第一个基类
CBase1::CBase1(this);
// 构造第二个基类
CBase2::CBase2(&this->CBase2);
this->CBase1::__vftable = (CDerived_vtbl *)CDerived::`vftable'{for `CBase1'};
this->CBase2::__vftable = (CBase2_vtbl *)CDerived::`vftable'{for `CBase2'};
// 构造成员对象
CBase1::CBase1(&this->base1);
CBase2::CBase2(&this->base2);
//构造自身
this->m_nMember = 3;
_puts("CDerived::CDerived");
}
析构顺序:
- 析构自身
- 析构成员对象
- 析构第二个基类
- 析构第一个基类
实例代码析构代码如下:
void __thiscall CDerived::~CDerived(CDerived *this)
{
this->CBase1::__vftable = (CDerived_vtbl *)CDerived::`vftable'{for `CBase1'};
this->CBase2::__vftable = (CBase2_vtbl *)CDerived::`vftable'{for `CBase2'};
// 析构自身
_puts("CDerived::~CDerived");
// 析构成员对象
CBase2::~CBase2(&this->base2);
CBase1::~CBase1(&this->base1);
// 析构第二个基类
CBase2::~CBase2(&this->CBase2);
// 析构第一个基类
CBase1::~CBase1(this);
}
因此在逆向过程中判断多重继承的依据如下:
- 在修改虚表指针之前多次调用构造函数(看编译器版本)。
- 同一个虚表指针在构造函数调用的构造函数中被填之后在构造函数中又被修改(如果是成员对象构造完之后虚表指针就不会被修改了)。
- 如果没有虚表无法判断,那么是继承还是成员对象已经无所谓了,因为没有多态。
抽象类
以下面这段代码为例:
#include <iostream>
class CBase {
public:
CBase() {
m_nMember = 1;
puts(__FUNCTION__);
}
~CBase() {
puts(__FUNCTION__);
}
virtual void fun1() = 0;
private:
int m_nMember;
};
class CDerived : public CBase {
public:
CDerived() {
m_nMember = 2;
puts(__FUNCTION__);
}
virtual void fun1() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
int main() {
CDerived Derived;
return 0;
}
CBase
为一个抽象类,因为该函数的成员函数 fun1
没有实现。因此如果 CDerived
想要继承 CBase
就一定要实现 fun1
函数。
CBase
函数的虚表中对应 fun3
的位置上是 _purecall
函数地址。
void (__cdecl *const ??_7CBase@@6B@[2])() =
{ &_purecall };
_purecall
函数实现如下,主要作用是终止程序。
extern "C" int __cdecl _purecall()
{
_purecall_handler const purecall_handler = _get_purecall_handler();
if (purecall_handler)
{
purecall_handler();
// The user-registered purecall handler should not return, but if it does,
// continue with the default termination behavior.
}
abort();
}
要想调到该函数我们只需要通过调用 CBase
的析构函数(这里需要定义 CBase
的析构函数,否则析构函数会被优化掉)将对象虚表指针指向 CBase
的虚表,之后触发多态调用虚表中的 fun1
即可。
int main() {
CDerived Derived;
((CBase *) &Derived)->~CBase();
(&Derived)->fun1();
return 0;
}
在逆向过程中识别抽象类的一个很重要的依据就是虚表中是否有 _purecall
函数地址。
菱形继承
这里使用如下代码来介绍多重继承:
#include <iostream>
class A {
public:
A() {
m_nMember = 1;
puts(__FUNCTION__);
}
~A() {
puts(__FUNCTION__);
}
virtual void fun1() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
virtual void fun3() {
puts(__FUNCTION__);
}
virtual void fun4() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
class B : virtual public A {
public:
B() {
m_nMember = 2;
puts(__FUNCTION__);
}
~B() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
virtual void fun4() {
puts(__FUNCTION__);
}
virtual void fun5() {
puts(__FUNCTION__);
}
virtual void fun6() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
class C : virtual public A {
public:
C() {
m_nMember = 3;
puts(__FUNCTION__);
}
~C() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
virtual void fun5() {
puts(__FUNCTION__);
}
virtual void fun7() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
class BC : public B, public C {
public:
BC() {
m_nMember = 4;
puts(__FUNCTION__);
}
~BC() {
puts(__FUNCTION__);
}
virtual void fun2() {
puts(__FUNCTION__);
}
virtual void fun3() {
puts(__FUNCTION__);
}
virtual void fun4() {
puts(__FUNCTION__);
}
virtual void fun5() {
puts(__FUNCTION__);
}
virtual void fun7() {
puts(__FUNCTION__);
}
virtual void fun8() {
puts(__FUNCTION__);
}
private:
int m_nMember;
};
int main() {
BC bc;
return 0;
}
内存结构
示例代码中各个类对应的对象的内存结构如下:
从中我们可以发现菱形重继承对象的虚表的结构规律:
- 所有与类
A
重复的虚函数一律覆盖到A
的虚表中。 - 所有与类
A
不重复但与B
或C
重复的函数一律覆盖到B
或C
的虚表中。特别的,如果与类B
和C
都重复的函数两个虚表的函数都覆盖,但是其中一个覆盖的是一个代理函数,例如实例代码中的BC::fun5()
。 BC
独有的虚函数追加到类开头的虚表指针指向的虚表中。BC::fun7()
函数由于优化问题在使用传入的this
指针访问成员的时候自动加了个偏移,因此没有通过代理函数对this
指针进行转换。
菱形重继承对象的结构规律:
- 虚继承的
A
放在最后,与前面的对象隔一个 4 字节的 0 填充(看编译器和编译参数,有的没有)。 - 虚表指针后面跟一个虚基址偏移表指针指向虚基址偏移表,该表的前 4 字节没有用到,后 4 字节记录虚基址偏移表指针到基类之间的距离。
构造析构顺序
示例代码对应的构造函数如下,调用构造函数的传参为 BC::BC(&bc, 1);
。
void __thiscall A::A(A *this)
{
this->__vftable = (A_vtbl *)A::`vftable';
this->m_nMember = 1;
puts("A::A");
}
void __thiscall B::B(B *this, int NotInit)
{
if ( NotInit )
{
this->__vbtable = (vbtable *)&C::`vbtable';
A::A((A *)&this[1].__vbtable);
}
this->__vftable = (B_vtbl *)B::`vftable'{for `B'};
*(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)B::`vftable'{for `A'};
*(B_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (B_vtbl *)(this->__vbtable->offset - 12);
this->m_nMember = 2;
puts("B::B");
}
void __thiscall C::C(C *this, int NotInit)
{
if ( NotInit )
{
this->__vbtable = (vbtable *)&C::`vbtable';
A::A((A *)&this[1].__vbtable);
}
this->__vftable = (C_vtbl *)C::`vftable'{for `C'};
*(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)C::`vftable'{for `A'};
*(C_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (C_vtbl *)(this->__vbtable->offset - 12);
this->m_nMember = 3;
puts("C::C");
}
void __thiscall BC::BC(BC *this, int NotInit)
{
if ( NotInit )
{
this->b.__vbtable = &BC::`vbtable'{for `B'};
this->c.__vbtable = &BC::`vbtable'{for `C'};
A::A(&this->a);
}
B::B(&this->b, 0);
C::C(&this->c, 0);
this->b.__vftable = (B_vtbl *)BC::`vftable'{for `B'};
this->c.__vftable = (C_vtbl *)BC::`vftable'{for `C'};
*(vbtable **)((char *)&this->b.__vbtable + this->b.__vbtable->offset) = (vbtable *)BC::`vftable'{for `A'};
*(B_vtbl **)((char *)&this->b.__vftable + this->b.__vbtable->offset) = (B_vtbl *)(this->b.__vbtable->offset - 28);
this->m_nMember = 4;
puts("BC::BC");
}
观察发现除了 A
的构造函数外都会传入一个参数 NotInit
,该函数的作用是标记 A
是否已经被初始化,从而确保基类 A
只被初始化一次。
实际上有些编译器还生成两个函数,一个初始化基类 A
的情况下调用,另一个在基类 A
未初始化下调用。
根据构造代码可知 BC
对象的构造过程如下:
析构调用的是 BC
析构函数的代理函数 BC::vbase_destructor(&bc);
,该函数先调用 BC
的析构函数然后调用基类 A
的析构函数(因为 A
一定是最后析构的,所以析构其它对象的时候都不会考虑是否析构 A
)。析构过程和构造过程顺序刚好相反。
void __thiscall B::~B(B *this)
{
this->__vftable = (B_vtbl *)B::`vftable'{for `B'};
*(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)B::`vftable'{for `A'};
*(B_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (B_vtbl *)(this->__vbtable->offset - 12);
puts("B::~B");
}
void __thiscall C::~C(C *this)
{
this->__vftable = (C_vtbl *)C::`vftable'{for `C'};
*(vbtable **)((char *)&this->__vbtable + this->__vbtable->offset) = (vbtable *)C::`vftable'{for `A'};
*(C_vtbl **)((char *)&this->__vftable + this->__vbtable->offset) = (C_vtbl *)(this->__vbtable->offset - 12);
puts("C::~C");
}
void __thiscall BC::~BC(BC *this)
{
this->b.__vftable = (B_vtbl *)BC::`vftable'{for `B'};
this->c.__vftable = (C_vtbl *)BC::`vftable'{for `C'};
*(vbtable **)((char *)&this->b.__vbtable + this->b.__vbtable->offset) = (vbtable *)BC::`vftable'{for `A'};
*(B_vtbl **)((char *)&this->b.__vftable + this->b.__vbtable->offset) = (B_vtbl *)(this->b.__vbtable->offset - 28);
puts("BC::~BC");
C::~C(&this->c);
B::~B(&this->b);
}
void __thiscall A::~A(A *this)
{
this->__vftable = (A_vtbl *)A::`vftable';
puts("A::~A");
}
void __thiscall BC::`vbase destructor'(BC *this)
{
BC::~BC(this);
A::~A(&this->a);
}
判断一个对象是菱形继承的依据是:
- 有偏移表。
- 根据构造函数参数决定是否调用基类构造。
异常
C++ 异常处理
在 windows pwn 基础知识中提到了 Visual C++ 中内置了一个异常处理 __try{...}__except(...){...}
,不过在 C++ 标准中也有一个异常处理的语法:try{...}catch(...){...}
。该异常处理语法的使用方式如下:
#include <iostream>
int main() {
try {
// 可能引发异常的代码
throw std::exception("Something went wrong.");
} catch (const std::exception &e) {
// 处理 std::exception 类型的异常
std::cout << "Caught std::exception: " << e.what() << std::endl;
} catch (...) {
// 处理任何类型的异常
std::cout << "Caught an unknown exception." << std::endl;
}
return 0;
}
实际上异常处理的 catch
是一种类似函数重载的基址,catch
的参数类型会与 throw
出来的异常类型进行匹配。另外如果两个 catch
参数都是同一个类型会优先匹配前一个 catch
。
#include <iostream>
#include<string>
int main() {
try {
int x;
std::cin >> x;
switch (x) {
case 1:
throw 1;
break;
case 2:
throw 2.0f;
break;
case 3:
throw '3';
break;
default:
throw std::exception(std::to_string(x).c_str());
}
} catch (int e) {
std::cout << "Caught int: " << e << std::endl;
} catch (float e) {
std::cout << "Caught float: " << e << std::endl;
} catch (char e) {
std::cout << "Caught char: " << e << std::endl;
} catch (const std::exception &e) {
std::cout << "Caught std::exception: " << e.what() << std::endl;
}
return 0;
}
C++ 标准的异常处理 try{...}catch(...){...}
与 Visual C++ 中内置了的异常处理 __try{...}__except(...){...}
相比有如下区别:
- 在 VC++ 中,可以使用
__try{}__except{}
来捕获和处理结构化异常,例如访问非法内存时引发的异常,这可以提供更底层的异常处理(例如我想访问一个可能被另一个线程修改的指针指向的内存,如果使用 C++ 标准异常处理需要手动判断,这就导致判断的时候可能指针指向有效内存,但是访问的时候会访问到无效地址,而使用 VC++ 自带异常处理就可以确保访问到异常地址的错误一定会被捕获。)。而 C++ 标准异常处理只能捕获throw
的异常,但可以使用 C++ 异常处理机制来处理其他类型的异常,例如逻辑错误或自定义异常。这提供了更高级别的异常处理。 - 在 VC++ 的自带的异常处理没有资源释放相关的处理,而 C++ 标准的异常处理会自动调用类的析构函数进行资源释放。
异常处理相关结构
C++ 异常处理相关结构如下:
ThrowInfo
ThrowInfo
的定义在 include\ehdata_forceinclude.h
中,具体定义如下:
typedef const struct _s_ThrowInfo {
unsigned int attributes; // Throw Info attributes (Bit field)
PMFN pmfnUnwind; // Destructor to call when exception has been handled or aborted
#if _EH_RELATIVE_TYPEINFO && !defined(BUILDING_C1XX_FORCEINCLUDE)
int pForwardCompat; // Image relative offset of Forward compatibility frame handler
int pCatchableTypeArray; // Image relative offset of CatchableTypeArray
#else
int (__cdecl * pForwardCompat)(...); // Forward compatibility frame handler
CatchableTypeArray* pCatchableTypeArray; // Pointer to list of pointers to types
#endif
} ThrowInfo;
attributes
: 抛出异常类型标记。pmfnUnwind
: 异常对象的析构函数地址。pCatchableTypeArray
:catch
块类型表,指向CatchTableTypeArray
表结构。
如果开启地址随机化则 pForwardCompat
和 pCatchableTypeArray
由绝对地址改为相对地址(后续的很多结构体都有这个机制),使用 int
类型再定义一遍是因为指针类型参与加减运算的基本单位是指针指向的结构的大小,不是真正意义上的加减运算。
CatchableTypeArray
CatchableTypeArray
的定义在 include\ehdata_forceinclude.h
中,具体定义如下:
typedef const struct _s_CatchableTypeArray {
int nCatchableTypes;
#if _EH_RELATIVE_TYPEINFO
int arrayOfCatchableTypes[]; // Image relative offset of Catchable Types
#else
CatchableType* arrayOfCatchableTypes[];
#endif
} CatchableTypeArray;
nCatchableTypes
:整数类型,表示可捕获类型的数量。例如throw
一个对象指针,实际上throw
的是对象指针和void*
,因此可捕获类型为 2 。arrayOfCatchableTypes
:指向CatchableType
类型的指针数组,数组中每一个成员代表着一种可以捕获的类型。
CatchableType
CatchableType
的定义在 include\ehdata_forceinclude.h
中,具体定义如下:
typedef struct PMD {
int mdisp; // Offset of intended data within base
int pdisp; // Displacement to virtual base pointer
int vdisp; // Index within vbTable to offset of base
} PMD;
typedef const struct _s_CatchableType {
unsigned int properties; // Catchable Type properties (Bit field)
#if _EH_RELATIVE_TYPEINFO
int pType; // Image relative offset of TypeDescriptor
#else
TypeDescriptor * pType; // Pointer to the type descriptor for this type
#endif
PMD thisDisplacement; // Pointer to instance of catch type within thrown object.
int sizeOrOffset; // Size of simple-type object or offset into
// buffer of 'this' pointer for catch object
PMFN copyFunction; // Copy constructor or CC-closure
} CatchableType;
properties
:用于表示 Catchable Type 的属性,通常是一个位字段(在枚举__CT_flags
中定义)。CT_IsSimpleType(1)
:简单类型。CT_ByReferenceOnly(2)
:指针类型。CT_HasVirtualBase(4)
:虚基类。CT_IsWinRTHandle(8)
:WinRT
句柄。CT_IsStdBadAlloc(16)
:标准库的std::bad_alloc
异常类型。
pType
:指向异常类型结构,TypeDescriptor
表结构。thisDisplacement
:基类信息。mdisp
:基类偏移。pdisp
:虚基类偏移。vdisp
:基类虚表偏移。
sizeOrOffset
:类的大小。copyFunction
:拷贝构造函数的指针。
TypeDescriptor
TypeDescriptor
的定义在 include\ehdata_forceinclude.h
中,具体定义如下:
typedef struct TypeDescriptor
{
#if defined(_WIN64) || defined(_RTTI) || defined(BUILDING_C1XX_FORCEINCLUDE)
const void * pVFTable; // Field overloaded by RTTI
#else
unsigned long hash; // Hash value computed from type's decorated name
#endif
void * spare; // reserved, possible for RTTI
char name[]; // The decorated name of the type; 0 terminated.
} TypeDescriptor;
hash
:类型名称的哈希值。spare
:保留,可用于 RTTI 的名称记录。name[]
:类型名称(符号修饰)。
FuncInfo
FuncInfo
的定义在 include\ehdata.h
中,具体定义如下:
typedef const struct _s_UnwindMapEntry {
__ehstate_t toState; // State this action takes us to
#if _EH_RELATIVE_FUNCINFO
int action; // Image relative offset of funclet
#else
void (__cdecl * action)(void); // Funclet to call to effect state change
#endif
} UnwindMapEntry;
typedef const struct _s_FuncInfo
{
unsigned int magicNumber:29; // Identifies version of compiler
unsigned int bbtFlags:3; // flags that may be set by BBT processing
__ehstate_t maxState; // Highest state number plus one (thus
// number of entries in unwind map)
#if _EH_RELATIVE_FUNCINFO
int dispUnwindMap; // Image relative offset of the unwind map
unsigned int nTryBlocks; // Number of 'try' blocks in this function
int dispTryBlockMap; // Image relative offset of the handler map
unsigned int nIPMapEntries; // # entries in the IP-to-state map. NYI (reserved)
int dispIPtoStateMap; // Image relative offset of the IP to state map
int dispUwindHelp; // Displacement of unwind helpers from base
int dispESTypeList; // Image relative list of types for exception specifications
#else
UnwindMapEntry* pUnwindMap; // Where the unwind map is
unsigned int nTryBlocks; // Number of 'try' blocks in this function
TryBlockMapEntry* pTryBlockMap; // Where the handler map is
unsigned int nIPMapEntries; // # entries in the IP-to-state map. NYI (reserved)
void* pIPtoStateMap; // An IP to state map. NYI (reserved).
ESTypeList* pESTypeList; // List of types for exception specifications
#endif
int EHFlags; // Flags for some features.
} FuncInfo;
magicNumber
:FuncInfo
结构体标志,通常填充为19930522h
。maxState
:最大栈展开数的下标值,也是pUnwindMap
数组中的成员个数。pUnwindMap
: 指向栈展开函数表的指针,指向UnwindMapEntry
表结构。toState
:栈展开数下标值。lpFunAction
:展开执行函数,主要是释放各种资源。
nTryBlocks
:函数中try
的数量,也是pTryBlockMap
数组中成员的个数。pTryBlockMap
:指向一个TryBlockMapEntry
类型的数组,记录了try
相关信息。
TryBlockMapEntry
TryBlockMapEntry
的定义在 include\ehdata.h
中,具体定义如下:
typedef const struct _s_TryBlockMapEntry {
__ehstate_t tryLow; // Lowest state index of try
__ehstate_t tryHigh; // Highest state index of try
__ehstate_t catchHigh; // Highest state index of any associated catch
int nCatches; // Number of entries in array
#if _EH_RELATIVE_FUNCINFO
int dispHandlerArray; // Image relative offset of list of handlers for this try
#else
HandlerType* pHandlerArray; // List of handlers for this try
#endif
} TryBlockMapEntry;
tryLow
,tryHigh
:表示try
块的最低(高)状态索引(try
嵌套),用于范围检查。catchHigh
:catch
块的最高状态索引,用于范围检查,通常是tryHigh + 1
。nCatches
:try
对应的catch
的数量。pHandlerArray
:指向HandlerType
类型的指针,指向此try
块的处理程序数组。
HandlerType
HandlerType
的定义在 include\ehdata.h
中,具体定义如下:
typedef const struct _s_HandlerType {
unsigned int adjectives; // Handler Type adjectives (bitfield)
#if _EH_RELATIVE_FUNCINFO
int dispType; // Image relative offset of the corresponding type descriptor
int dispCatchObj; // Displacement of catch object from base
int dispOfHandler; // Image relative offset of 'catch' code
#if defined(_WIN64) || defined(_CHPE_X86_ARM64_EH_)
int dispFrame; // displacement of address of function frame wrt establisher frame
#endif
#else // _EH_RELATIVE_FUNCINFO
TypeDescriptor* pType; // Pointer to the corresponding type descriptor
ptrdiff_t dispCatchObj; // Displacement of catch object from base
void * addressOfHandler; // Address of 'catch' code
#endif // _EH_RELATIVE_FUNCINFO
} HandlerType;
adjectives
:用于catch
块的匹配检查。pType
:指向TypeDescriptor
类型结构,用于描述catch
块要捕捉的类型。如果catch
匹配所有类型则这个值为 NULL 。dispCatchObj
:用于定位异常对象的偏移量。addressOfHandler
:catch
块代码的地址。
异常处理流程
针对下面这段示例代码讲解 C++ 异常处理流程。这里分析的是关闭全局优化的 Debug 版。
#include <stdio.h>
int main(int argc, char *argv[]) {
try {
throw 1;// 抛出异常
} catch (int e) {
printf(" 触发int异常\r\n");
} catch (float e) {
printf(" 触发float异常\r\n");
}
return 0;
}
异常注册
在 main
函数的开头程序想 SEH 链表插入了一个节点,其中处理函数为 __ehhandler$_main
,另外节点上还有一个 TryLevel
用来记录异常发生的位置,初始值 -1 表示不在 try
块中。
.text:0045A8C0 push ebp
.text:0045A8C1 mov ebp, esp
.text:0045A8C3 push 0FFFFFFFFh ; state
.text:0045A8C5 push offset __ehhandler$_main ; frameHandler
.text:0045A8CA mov eax, large fs:0
.text:0045A8D0 push eax ; pNext
.text:0045A8D1 mov large fs:0, esp
这个注册异常的节点实际上是 EHRegistrationNode
结构体,定义如下:
struct EHRegistrationNode
{
EHRegistrationNode *pNext;
void *frameHandler;
int state;
};
异常产生
在 main
函数中 throw
抛出异常时会调用 _CxxThrowException
函数并传入 pExceptionObject
和 pThrowInfo
两个参数,其中 pExceptionObject
存放异常值。
.text:0045A8F3 mov [ebp+pExceptionObject], 1
.text:0045A8FA push offset __TI1H ; pThrowInfo
.text:0045A8FF lea eax, [ebp+pExceptionObject]
.text:0045A902 push eax ; pExceptionObject
.text:0045A903 call j___CxxThrowException@8 ; _CxxThrowException(x,x)
_CxxThrowException
函数最终调用 RaiseException
产生异常,异常号为 0xE06D7363 。
void __stdcall __noreturn _CxxThrowException(_DWORD *pExceptionObject, const _s_ThrowInfo *pThrowInfo)
{
unsigned int parameters[3]; // [esp+0h] [ebp-24h] BYREF
void (__stdcall *PrepareThrow)(void *); // [esp+Ch] [ebp-18h]
unsigned int magicNumber; // [esp+10h] [ebp-14h]
unsigned int Target; // [esp+14h] [ebp-10h]
WinRTExceptionInfo *pWei; // [esp+18h] [ebp-Ch]
WinRTExceptionInfo **ppWei; // [esp+1Ch] [ebp-8h]
const _s_ThrowInfo *pTI; // [esp+20h] [ebp-4h]
pTI = pThrowInfo;
magicNumber = 0x19930520;
if ( pThrowInfo && (pTI->attributes & 0x10) != 0 )
{
ppWei = (WinRTExceptionInfo **)(*pExceptionObject - 4);
pWei = *ppWei;
pTI = pWei->throwInfo;
PrepareThrow = pWei->PrepareThrow;
Target = (unsigned int)PrepareThrow;
((void (__thiscall *)(void (__stdcall *)(void *), WinRTExceptionInfo **))PrepareThrow)(PrepareThrow, ppWei);
}
if ( pTI && (pTI->attributes & 8) != 0 )
magicNumber = 0x1994000;
parameters[0] = magicNumber;
parameters[1] = (unsigned int)pExceptionObject;
parameters[2] = (unsigned int)pTI;
RaiseException(0xE06D7363, 1u, 3u, parameters);
}
异常派发
__ehhandler$_main
函数将 eax 传入了 FuncInfo
结构体地址。之所以用 eax 是因为这个函数还要传其它参数,为了不影响函数传参这里使用 eax 额外传入 FuncInfo
结构体地址。
.text:005035E0 __ehhandler$_main proc near ; DATA XREF: _main+5↑o
.text:005035E0 nop
.text:005035E1 nop
.text:005035E2 mov eax, offset FuncInfo
.text:005035E7 jmp j____CxxFrameHandler3
.text:005035E7
.text:005035E7 __ehhandler$_main endp
FuncInfo
结构体内容如下,从中可以看出 UnwindMap
有两项,TryBlockMap
有 1 项(一个 try
)。
_s_FuncInfo FuncInfo =
{ 0x19930522, 0u, 2, &UnwindMap, 1u, &TryBlockMap, 0u, NULL, NULL, 1 };
__CxxFrameHandler3
函数定义在 crt\src\i386\trnsctrl.cpp
中,内容如下:
//
// __CxxFrameHandler3 - Real entry point to the runtime
// __CxxFrameHandler2 is an alias for __CxxFrameHandler3
// since they are compatible in VC version of CRT
// These function should be separated out if a change makes
// __CxxFrameHandler3 incompatible with __CxxFrameHandler2
//
extern "C" _VCRTIMP __declspec(naked) DECLSPEC_GUARD_SUPPRESS EXCEPTION_DISPOSITION __cdecl __CxxFrameHandler3(
/*
EAX=FuncInfo *pFuncInfo, // Static information for this frame
*/
EHExceptionRecord *pExcept, // Information for this exception
EHRegistrationNode *pRN, // Dynamic information for this frame
void *pContext, // Context info (we don't care what's in it)
DispatcherContext *pDC // More dynamic info for this frame (ignored on Intel)
) {
FuncInfo *pFuncInfo;
EXCEPTION_DISPOSITION result;
__asm {
//
// Standard function prolog
//
push ebp
mov ebp, esp
sub esp, __LOCAL_SIZE // 申请局部变量空间,__LOCAL_SIZE = 8。
push ebx
push esi
push edi
cld // A bit of paranoia -- Our code-gen assumes this
// 将 DF 位置 0,每次操作后,esi、edi 递增
//
// Save the extra parameter
//
mov pFuncInfo, eax // 传入的 FuncInfo 结构体地址
}
EHTRACE_FMT1("pRN = 0x%p", pRN);
result = __InternalCxxFrameHandlerWrapper<RENAME_EH_EXTERN(__FrameHandler3)>( pExcept, pRN, (PCONTEXT)pContext, pDC, pFuncInfo, 0, nullptr, FALSE );
EHTRACE_HANDLER_EXIT(result);
__asm {
pop edi
pop esi
pop ebx
mov eax, result
mov esp, ebp
pop ebp
ret 0
}
}
__InternalCxxFrameHandlerWrapper
函数调用了 __InternalCxxFrameHandler
函数,该函数没有源码,但从 IDA 反编译结果可以看到该函数的主要功能是完成异常类型的检查以及栈展开,并最终调用查找 try
块和 catch
块的核心函数 FindHandler___FrameHandler3_
。
void __cdecl __FrameHandler3::FrameUnwindToState(
EHRegistrationNode *pRN,
void *pDC,
const _s_FuncInfo *pFuncInfo,
int targetState)
{
__vcrt_ptd *v4; // eax
int nxtState; // [esp+14h] [ebp-24h]
int *p_ProcessingThrow; // [esp+18h] [ebp-20h]
int curState; // [esp+1Ch] [ebp-1Ch]
curState = __FrameHandler3::GetCurrentState(pRN, pDC, pFuncInfo);
p_ProcessingThrow = &j____vcrt_getptd()->_ProcessingThrow;
++*p_ProcessingThrow;
while ( curState != targetState )
{
if ( curState <= -1 || curState >= pFuncInfo->maxState )
j__abort();
nxtState = pFuncInfo->pUnwindMap[curState].toState;
if ( pFuncInfo->pUnwindMap[curState].action )
{
__FrameHandler3::SetState(pRN, pFuncInfo, nxtState);
_CallSettingFrame((unsigned int)pFuncInfo->pUnwindMap[curState].action, (unsigned int)pRN, 0x103u);
}
curState = nxtState;
}
if ( j____vcrt_getptd()->_ProcessingThrow > 0 )
{
v4 = j____vcrt_getptd();
--v4->_ProcessingThrow;
}
if ( curState != targetState )
j__abort();
__FrameHandler3::SetState(pRN, pFuncInfo, curState);
}
_EXCEPTION_DISPOSITION __cdecl __InternalCxxFrameHandler<__FrameHandler3>(
EHExceptionRecord *pExcept,
EHRegistrationNode *pRN,
_CONTEXT *pContext,
void *pDC,
const _s_FuncInfo *pFuncInfo,
int CatchDepth,
EHRegistrationNode *pMarkerRN,
unsigned __int8 recursive)
{
_EXCEPTION_DISPOSITION v8; // eax
__FrameHandler3::TryBlockMap tryBlockMap; // [esp+0h] [ebp-18h] BYREF
_EXCEPTION_DISPOSITION result; // [esp+8h] [ebp-10h]
int (*v11)(...); // [esp+Ch] [ebp-Ch]
unsigned int Target; // [esp+10h] [ebp-8h]
int (*pfn)(...); // [esp+14h] [ebp-4h]
j____except_validate_context_record(pContext);
if ( !j____vcrt_getptd()->_cxxReThrow
&& pExcept->ExceptionCode != 0xE06D7363
&& pExcept->ExceptionCode != 0x80000026
&& __FrameHandler3::getMagicNum(pFuncInfo) >= 0x19930522
&& __FrameHandler3::isEHs(pFuncInfo) )
{
return 1;
}
if ( (pExcept->ExceptionFlags & 0x66) != 0 )
{
if ( __FrameHandler3::GetMaxState(pDC, pFuncInfo) )
{
if ( !CatchDepth )
__FrameHandler3::FrameUnwindToEmptyState(pRN, pDC, pFuncInfo);
}
return 1;
}
else
{
__FrameHandler3::TryBlockMap::TryBlockMap(&tryBlockMap, pFuncInfo, 0);
if ( __FrameHandler3::TryBlockMap::getNumTryBlocks(&tryBlockMap)
|| __FrameHandler3::getMagicNum(pFuncInfo) >= 0x19930521 && __FrameHandler3::getESTypes(pFuncInfo)
|| __FrameHandler3::getMagicNum(pFuncInfo) >= 0x19930522 && __FrameHandler3::isNoExcept(pFuncInfo) )
{
if ( pExcept->ExceptionCode == 0xE06D7363
&& pExcept->NumberParameters >= 3
&& pExcept->params.magicNumber > 0x19930522 )
{
pfn = pExcept->params.pThrowInfo->pForwardCompat;
if ( pfn )
{
v11 = pfn;
Target = (unsigned int)pfn;
v8 = pfn(pExcept, pRN, pContext, pDC, pFuncInfo, CatchDepth, pMarkerRN, recursive);
result = v8;
return v8;
}
}
// 最终调用的函数
FindHandler___FrameHandler3_(pExcept, pRN, pContext, pDC, pFuncInfo, recursive, CatchDepth, pMarkerRN);
}
return 1;
}
}
FindHandler___FrameHandler3_
函数首先从传入的异常处理节点指针 EHRegistrationNode *pRN
中取出 state
,也就是记录异常发生的位置的值。之后判断这个值的范围是否合法(在 [-1, pFuncInfo->maxState]
范围内),如果不合法就终止程序。
curState = __FrameHandler3::GetCurrentState(pRN, pDC, pFuncInfo);
if ( curState < -1 || curState >= __FrameHandler3::GetMaxState(pDC, pFuncInfo) )
j__abort();
之后先处理 ThrowInfo
为空的情况。如果 ThrowInfo
为空则使用使用当前异常和上下文。如果 j____vcrt_getptd()->_curexception
为空则说明没有异常,直接返回,否则再次判断 ThrowInfo
是否为空,如果依然为空说明出现错误,需要终止程序。之后调用 IsInExceptionSpec
判断异常类型是否是能捕获的类型,如果不是会终止程序。
unsigned __int8 __cdecl IsInExceptionSpec(EHExceptionRecord *pExcept, const _s_ESTypeList *pESTypeList)
{
const _s_CatchableType *const *ppCatchable; // [esp+4h] [ebp-10h]
int catchables; // [esp+8h] [ebp-Ch]
int i; // [esp+Ch] [ebp-8h]
unsigned __int8 bFoundMatchingTypeInES; // [esp+13h] [ebp-1h]
if ( !pESTypeList )
j__abort();
bFoundMatchingTypeInES = 0;
for ( i = 0; i < pESTypeList->nCount; ++i )
{
ppCatchable = pExcept->params.pThrowInfo->pCatchableTypeArray->arrayOfCatchableTypes;
for ( catchables = pExcept->params.pThrowInfo->pCatchableTypeArray->nCatchableTypes; catchables > 0; --catchables )
{
if ( j____TypeMatch(&pESTypeList->pTypeArray[i], *ppCatchable, pExcept->params.pThrowInfo) )
{
bFoundMatchingTypeInES = 1;
break;
}
++ppCatchable;
}
}
return bFoundMatchingTypeInES;
}
unsigned __int8 __cdecl Is_bad_exception_allowed(const _s_ESTypeList *pESTypeList)
{
int i; // [esp+4h] [ebp-4h]
for ( i = 0; i < pESTypeList->nCount; ++i )
{
if ( type_info::operator==(
(type_info *)pESTypeList->pTypeArray[i].pType,
&std::bad_exception `RTTI Type Descriptor') )
{
return 1;
}
}
return 0;
}
if ( pExcept->ExceptionCode == 0xE06D7363
&& pExcept->NumberParameters == 3
&& (pExcept->params.magicNumber == 0x19930520
|| pExcept->params.magicNumber == 0x19930521
|| pExcept->params.magicNumber == 0x19930522)
&& !pExcept->params.pThrowInfo )
{
if ( !j____vcrt_getptd()->_curexception )
return;
pExcept = (EHExceptionRecord *)j____vcrt_getptd()->_curexception;
pContext = (_CONTEXT *)j____vcrt_getptd()->_curcontext;
IsRethrow = 1;
if ( !pExcept
|| pExcept->ExceptionCode == 0xE06D7363
&& pExcept->NumberParameters == 3
&& (pExcept->params.magicNumber == 0x19930520
|| pExcept->params.magicNumber == 0x19930521
|| pExcept->params.magicNumber == 0x19930522)
&& !pExcept->params.pThrowInfo )
{
j__abort();
}
if ( j____vcrt_getptd()->_curexcspec )
{
pCurrentFuncInfo = (const _s_ESTypeList *)j____vcrt_getptd()->_curexcspec;
j____vcrt_getptd()->_curexcspec = 0;
if ( !IsInExceptionSpec(pExcept, pCurrentFuncInfo) )
{
if ( Is_bad_exception_allowed(pCurrentFuncInfo) )
{
j____DestructExceptionObject(pExcept, 1u);
std::bad_exception::bad_exception(&pExceptionObject);
_CxxThrowException(&pExceptionObject, &_TI2_AVbad_exception_std__);
}
j__terminate();
}
}
}
构造一个 TryBlockMap
类记录 FuncInfo
地址。之后再判断异常类型,不过由于此时满足条件的异常的 pThrowInfo
一定不为空,所以不需要判断。
void __thiscall __FrameHandler3::TryBlockMap::TryBlockMap(
__FrameHandler3::TryBlockMap *this,
const _s_FuncInfo *pFuncInfo,
unsigned int imageBase)
{
this->_pFuncInfo = pFuncInfo;
this->_imageBase = imageBase;
}
__FrameHandler3::TryBlockMap::TryBlockMap(&tryBlockMap, pFuncInfo, 0);
if ( pExcept->ExceptionCode == 0xE06D7363
&& pExcept->NumberParameters == 3
&& (pExcept->params.magicNumber == 0x19930520
|| pExcept->params.magicNumber == 0x19930521
|| pExcept->params.magicNumber == 0x19930522) )
...
遍历 try
块,找到满足条件的 try
块调用 CatchIt___FrameHandler3_
函数。该函数两层循环,外层循环遍历 try
块,内层遍历 try
块对应的 catch
块。注意这里看似是遍历所有 try
块,但实际上 CatchIt___FrameHandler3_
函数不会返回(如果处理函数正常写),因此只会匹配一个处理函数。
if ( __FrameHandler3::TryBlockMap::getNumTryBlocks(&tryBlockMap) )
{
__FrameHandler3::GetRangeOfTrysToCheck(&startStop, &tryBlockMap, curState, pDC, pFuncInfo, CatchDepth);
for ( iter = startStop.first;
__FrameHandler3::TryBlockMap::iterator::operator<(&iter, &startStop.second);
__FrameHandler3::TryBlockMap::iterator::operator++(&iter) )
{
__FrameHandler3::TryBlockMap::iterator::operator*(&iter, &tryBlock);
if ( tryBlock.tryLow <= curState && curState <= tryBlock.tryHigh )
{
__FrameHandler3::HandlerMap::HandlerMap(&handlerMap, &tryBlock, 0, 0);
p_handlerMap = &handlerMap;
__FrameHandler3::HandlerMap::begin(&handlerMap, &result);
__FrameHandler3::HandlerMap::end(p_handlerMap, &other);
while ( __FrameHandler3::HandlerMap::iterator::operator!=(&result, &other) )
{
__FrameHandler3::HandlerMap::iterator::operator*(&result, &handler);
ppCatchable = pExcept->params.pThrowInfo->pCatchableTypeArray->arrayOfCatchableTypes;
catchables = pExcept->params.pThrowInfo->pCatchableTypeArray->nCatchableTypes;
while ( catchables > 0 )
{
pCatchable = *ppCatchable;
if ( __FrameHandler3::TypeMatch(&handler, pCatchable, pExcept->params.pThrowInfo) )
{
CatchIt___FrameHandler3_(
pExcept,
pRN,
pContext,
pDC,
pFuncInfo,
&handler,
pCatchable,
&tryBlock,
CatchDepth,
pMarkerRN);
goto LABEL_32;
}
--catchables;
++ppCatchable;
}
__FrameHandler3::HandlerMap::iterator::operator++(&result);
}
}
LABEL_32:
;
}
}
CatchIt___FrameHandler3_
函数通过调用 CallCatchBlock
函数来调用 try
块对应的异常处理函数,之后调用_JumpToContinuation
函数,之后调用 _JumpToContinuation
函数返回用户代码继续执行。
void __cdecl CatchIt___FrameHandler3_(
EHExceptionRecord *pExcept,
EHRegistrationNode *pRN,
_CONTEXT *pContext,
void *pDC,
const _s_FuncInfo *pFuncInfo,
const _s_HandlerType *pCatch,
const _s_CatchableType *pConv,
const _s_TryBlockMapEntry *pEntry,
int CatchDepth,
EHRegistrationNode *pMarkerRN)
{
void *continuationAddress; // [esp+0h] [ebp-8h]
if ( pConv )
__FrameHandler3::BuildCatchObject(pExcept, pRN, pCatch, pConv);
if ( pMarkerRN )
_UnwindNestedFrames(pMarkerRN, pExcept);
else
_UnwindNestedFrames(pRN, pExcept);
__FrameHandler3::FrameUnwindToState(pRN, pDC, pFuncInfo, pEntry->tryLow);
__FrameHandler3::SetState(pRN, pFuncInfo, pEntry->tryHigh + 1);
continuationAddress = CallCatchBlock(pExcept, pRN, pContext, pFuncInfo, pCatch->addressOfHandler, CatchDepth, 0x100u);
if ( continuationAddress )
_JumpToContinuation(continuationAddress, pRN);
}
CallCatchBlock
函数有如下调用链:
CallCatchBlock(pExcept, pRN, pContext, pFuncInfo, pCatch->addressOfHandler, CatchDepth, 0x100u);
_CallCatchBlock2(pRN, pFuncInfo, handlerAddress, CatchDepth, NLGCode);
_CallSettingFrame((unsigned int)handlerAddress, (unsigned int)pRN, NLGCode);
在 _CallSettingFrame
函数中有如下汇编代码,也就是说 _CallSettingFrame
函数会直接调用异常处理函数。而通过分析汇编可知,CallCatchBlock
函数的返回值为异常处理函数的返回值。
.text:00460AD1 mov eax, [ebp+handlerAddress]
...
.text:00460AE5 call eax
JumpToContinuation
函数只有一行有效汇编,也就是说 _JumpToContinuation
会跳转到异常处理函数的返回值继续执行。
void __stdcall _JumpToContinuation(void *target, EHRegistrationNode *pRN)
{
__asm { jmp eax }
}
异常处理
异常处理函数最好通过 FuncInfo->TryBlockMap->HandlerArray->addressOfHandler
来寻找,因为有些版本编译器不会把异常处理函数编译到对应函数中。或者 IDA 无法把异常处理函数归属到某个函数中。
示例代码 main
函数中有两个异常处理函数,分别是 __catch$_main$0
和 __catch$_main$1
。
.rdata:0052991C HandlerType _s_HandlerType <0, offset ??_R0H@8, 0FFFFFFECh, offset __catch$_main$0>
.rdata:0052992C _s_HandlerType <0, offset ??_R0M@8, 0FFFFFFE8h, offset __catch$_main$1>
以 __catch$_main$0
函数为例,汇编代码如下。异常处理函数在执行完核心逻辑后会在 eax 寄存器写恢复执行的地址,根据对异常派发的分析可知,在执行完异常处理函数后紧接着会跳转到 eax 也就是恢复执行的地址继续执行。
.text:0045A91B __catch$_main$1: ; DATA XREF: .rdata:0052992C↓o
.text:0045A91B push offset asc_506E64 ; " "
.text:0045A920 call j__printf
.text:0045A925 add esp, 4
.text:0045A928 mov eax, offset $LN10
.text:0045A92D retn
$LN10
在将 EHRegistrationNode.state
置为 -1 后跳转到 main
函数的结尾,卸载完 SEH 列表后返回。
.text:0045A937 $LN10:
.text:0045A937 mov [ebp+state], 0FFFFFFFFh
.text:0045A93E jmp short loc_45A949
...
.text:0045A949 loc_45A949:
.text:0045A949 xor eax, eax
.text:0045A94B mov ecx, [ebp+var_C]
.text:0045A94E mov large fs:0, ecx
.text:0045A955 pop edi
.text:0045A956 pop esi
.text:0045A957 pop ebx
.text:0045A958 mov esp, ebp
.text:0045A95A pop ebp
.text:0045A95B retn
其他
缺省参数
例如下面这段代码:
void foo(int x = 1) {}
int main() {
foo();
return 0;
}
在关闭优化后调用 foo
函数的汇编如下:
foo();
0045A863 push 1
0045A865 call foo (0455441h)
0045A86A add esp,4
因此得出结论:
- 缺省函数无还原依据
- 需要根据可读性还原
重载
函数重载
例如下面这段代码:
#include<stdio.h>
void foo(int x) {
printf("%d\n", x);
}
void foo(long long x) {
printf("%lld\n", x);
}
int main() {
foo(1);
foo(1LL);
return 0;
}
实际反编译出来的 main
函数中分别调了两个不同的函数。
int __cdecl main(int argc, const char **argv, const char **envp)
{
sub_401000(1);
sub_401020(1i64);
return 0;
}
但是如果我们将这两个函数导出:
#include<stdio.h>
__declspec(dllexport) void foo(int x) {
printf("%d\n", x);
}
__declspec(dllexport) void foo(long long x) {
printf("%lld\n", x);
}
int main() {
foo(1);
foo(1LL);
return 0;
}
因为导出表保存了导出符号信息,因此 IDA 能识别出函数名:
int __cdecl main(int argc, const char **argv, const char **envp)
{
foo(1);
foo(1i64);
return 0;
}
因此有如下结论:
- 如果是导出或导入函数可以还原。
- 否则没有还原依据,需要根据代码可读性还原。
运算符重载
重载赋值符号 =
:
class CObject {
public:
CObject &operator=(int x) {
this->x = x;
return *this;
}
private:
int x;
};
int main() {
CObject obj;
obj = 1;
return 0;
}
反编译代码如下,只能看出是(还原成)一个成员函数。
void __thiscall setX(CObject *this, int x)
{
this->x = x;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
CObject obj; // [esp+0h] [ebp-8h] BYREF
setX(&obj, 1);
return 0;
}
但是如果将类导出:
class __declspec(dllexport) CObject {
public:
CObject &operator=(int x) {
this->x = x;
return *this;
}
private:
int x;
};
int main() {
CObject obj;
obj = 1;
return 0;
}
那么成员函数就有符号可以识别。
CObject *__thiscall CObject::operator=(CObject *this, int x)
{
this->x = x;
return this;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
CObject obj; // [esp+0h] [ebp-8h] BYREF
CObject::operator=(&obj, 1);
return 0;
}
因此有如下结论:
- 如果是导入或导出类,则有还原依据。
- 否则无还原依据,需要根据可读性还原。
lambda 表达式
首先是函数内只使用参数:
int main() {
auto foo = [](int x, int y) {
return x + y;
};
foo(1, 2);
}
调用方式是 __thiscall
不过由于 this
指针在函数内部没有用到,因此本质相当于 __stdcall
的函数调用。
push 2
push 1
lea ecx, [ebp+var_5]
call sub_401000
当函数使用局部变量的时候:
int main() {
int x = 1, y = 2;
auto foo = [=]() {
return x + y;
};
foo();
}
本质是将参数赋值到 Args
对象中,然后将 Args
对象指针传入 foo
函数。
struct Args
{
int x;
int y;
};
Args *__thiscall Args::Args(Args *this, int *x, int *y)
{
this->x = x;
this->y = y;
return this;
}
int __thiscall foo(Args *this)
{
return this->y + this->x;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
Args this; // [esp+0h] [ebp-14h] BYREF
int x; // [esp+8h] [ebp-Ch] BYREF
int y; // [esp+Ch] [ebp-8h] BYREF
x = 1;
y = 2;
Args::Args(&this, &x, &y);
foo(&this);
return 0;
}
当函数使用和修改局部变量的时候:
int main() {
int x = 1, y = 2;
auto foo = [&]() {
x = 10, y = 20;
return x + y;
};
foo();
}
此时 Args
对象中存储的不再是变量的值而是变量的地址。
struct Args
{
int *x;
int *y;
};
Args *__thiscall Args::Args(Args *this, int *x, int *y)
{
this->x = x;
this->y = y;
return this;
}
int __thiscall foo(Args *this)
{
*this->x = 10;
*this->y = 20;
return *this->y + *this->x;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
Args this; // [esp+0h] [ebp-14h] BYREF
int x; // [esp+8h] [ebp-Ch] BYREF
int y; // [esp+Ch] [ebp-8h] BYREF
x = 1;
y = 2;
Args::Args(&this, &x, &y);
foo(&this);
return 0;
}
如果在使用修改局部变量的同时还传递参数:
int main() {
int x = 1, y = 2;
auto foo = [&](int a,int b) {
x = 10, y = 20;
return x + y;
};
foo(3, 4);
}
观察反编译代码发现实际上传入的参数作为 foo
函数 __thiscall
中 this
指针外的参数。
struct Args
{
int *x;
int *y;
};
Args *__thiscall Args::Args(Args *this, int *a2, int *a3)
{
this->x = a2;
this->y = a3;
return this;
}
int __thiscall foo(Args *this, int a, int b)
{
*this->x = 10;
*this->y = 20;
return *this->y + *this->x;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
Args this; // [esp+0h] [ebp-14h] BYREF
int x; // [esp+8h] [ebp-Ch] BYREF
int y; // [esp+Ch] [ebp-8h] BYREF
x = 1;
y = 2;
Args::Args(&this, &x, &y);
foo(&this, 3, 4);
return 0;
}
综上有如下结论:
- 可根据类 + 成员函数实现。
- 根据可读性还原。
模板
以如下代码为例:
template<typename T>
T max(T x, T y) {
return x > y ? x : y;
}
int main() {
max(1, 2);
max(1.0, 2.0);
return 0;
}
反编译代码如下:
int __cdecl MaxInt(int x, int y)
{
if ( x <= y )
return y;
else
return x;
}
double __cdecl MaxDouble(double x, double y)
{
if ( x <= y )
return y;
else
return x;
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
MaxInt(1, 2);
MaxDouble(1.0, 2.0);
return 0;
}
有如下结论:
- 无还原依据,需要根据代码可读性还原。
- 无法做
sig
文件。例如 STL 库在没有符号的时候 IDA 很难识别,因为根据模板传入的类型和值的不同,生成的汇编的特征不同。
STL
- 主要靠经验识别和还原。
- 熟悉数据结构的内存结构。
- 注意
xxx too long
等错误日志字符串,通过查找引用可以大致判断出 STL 代码。