Reverse中虚函数/虚表浅析
this指针
在C++的类中,需要研究一个非常重要并且特殊的指针就是this指针:
一个简单的demo来测试一下this指针:
1
2
3
4
5
6
7
8
9
10
|
int main() {
struct Test{
int a;
int b;
int c;
};
Test* test = new Test;
test->a = 100;
printf("%d", *test);
}
|
可以看到,直接取test的值就是a的值,这里的test就是this指针本身,而this指针的特性就是指向类的首地址,在对类进行内存分配时,this指针会默认指向该类的首地址:

关于类和结构体:其底层实现是一样的,所以demo中用struct和class的结果相同

demo2:
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
|
#include <iostream>
using namespace std;
struct Test {
int a;
int b;
int c;
virtual void func1();
virtual void func2();
virtual void func3();
};
void Test::func1() {
printf("func1");
}
void Test::func2() {
printf("func2");
}
void Test::func3() {
printf("func3");
}
int main() {
Test* test = new Test;
test->a = 100;
test->func1();
printf("%d", *test);
}
|
这里我定义了一个结构体,带着三个虚函数,虚函数需要定义,通过结构体名::方法名的形式进行定义,重点在实例化对象调用函数的位置,调试来到反汇编窗口,红框中标注的为实际调用函数的位置,这里我们在声明结构体时从上到下的声明为func1
、func2
、func3
,我们注意到这里call的函数内容来自[edx]
、[edx+4]
、[edx+8]

VS的监视器中已经帮我们标注出来了,存在一个__vfptr,这里指针就是指向虚表的指针,对于一个类or结构体中,如果其虚函数的数量大于1,那么编译器在初始化时会自动创建一个虚表,用来存储函数的地址,通过代码的编写顺序进行函数索引的映射,虚表中存放的就是函数定义的地址:

但是在VS中默认开启了增量链接,所以此时虚表中指向的还是一个跳转地址,跳转地址之后才是函数真正的位置:


IDA加载该程序
直接来到main函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
_DWORD *v4; // [esp+Ch] [ebp-DCh]
void *v5; // [esp+14h] [ebp-D4h]
_DWORD *v6; // [esp+E0h] [ebp-8h]
__CheckForDebuggerJustMyCode(&unk_41C066);
v5 = operator new(0x10u);
if ( v5 )
v4 = (_DWORD *)sub_411460(v5);
else
v4 = 0;
v6 = v4;
v4[1] = 100;
(*(void (__thiscall **)(_DWORD *))*v6)(v6);
(*(void (__thiscall **)(_DWORD *))(*v6 + 4))(v6);
(*(void (__thiscall **)(_DWORD *))(*v6 + 8))(v6);
sub_411442("%d", *v4);
return 0;
}
|
根据源码,这一部分就是虚函数调用在IDA中的反编译结果:
1
2
3
|
(*(void (__thiscall **)(_DWORD *))*v6)(v6);
(*(void (__thiscall **)(_DWORD *))(*v6 + 4))(v6);
(*(void (__thiscall **)(_DWORD *))(*v6 + 8))(v6);
|
这里v6是指向v4,来到v4的初始化,可以看到存在一个virtual function table
也就是虚函数表:
1
2
3
4
5
|
_DWORD *__thiscall sub_411880(_DWORD *this)
{
*this = &Test::`vftable';
return this;
}
|
根据前面VS中的调试可以知道这个表中存放的是增量链接:

基本上关于虚函数在IDA中的体现就介绍完了
一道逆向题
SWPUCTF2019 easyRE,同样来到main函数,本文重点在虚函数的创建以及体现,其他部分不做分析:
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
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
_DWORD v4[28]; // [esp-6Ch] [ebp-F8h] BYREF
_DWORD *v5; // [esp+4h] [ebp-88h]
_DWORD *v6; // [esp+8h] [ebp-84h]
int v7; // [esp+Ch] [ebp-80h]
char v8[108]; // [esp+10h] [ebp-7Ch] BYREF
int v9; // [esp+88h] [ebp-4h]
if ( sub_40EF90() )
return 1;
sub_4026C0(0x6Cu);
sub_401FE0(v4[27], v5);
v9 = 0;
v6 = v4;
sub_40F360(v8);
sub_40F080(v4[0], v4[1]);
v5 = v4;
sub_40F360(v8);
sub_40F150(argc, (int)argv);
v7 = 0;
v9 = -1;
sub_4021C0(v8);
return v7;
}
|
进入sub_401FE0:
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
|
_DWORD *__thiscall sub_401FE0(_DWORD *this)
{
int i; // [esp+4h] [ebp-14h]
*this = &EASYRE::`vftable';
this[1] = 0;
*((_BYTE *)this + 52) = 8;
*((_BYTE *)this + 53) = -22;
*((_BYTE *)this + 54) = 88;
*((_BYTE *)this + 55) = -34;
*((_BYTE *)this + 56) = -108;
*((_BYTE *)this + 57) = -48;
*((_BYTE *)this + 58) = 59;
*((_BYTE *)this + 59) = -66;
*((_BYTE *)this + 60) = -120;
*((_BYTE *)this + 61) = -44;
*((_BYTE *)this + 62) = 50;
*((_BYTE *)this + 63) = -74;
*((_BYTE *)this + 64) = 20;
*((_BYTE *)this + 65) = -126;
*((_BYTE *)this + 66) = -73;
*((_BYTE *)this + 67) = -81;
*((_BYTE *)this + 68) = 20;
*((_BYTE *)this + 69) = 84;
*((_BYTE *)this + 70) = 127;
*((_BYTE *)this + 71) = -49;
qmemcpy(this + 18, " 03\"3 0 203\" $ ", 20);
sub_4030A0(this + 23);
sub_402DE0(this + 26);
for ( i = 0; i < 40; ++i )
*((_BYTE *)this + i + 12) = 0;
return this;
}
|
该题目中不存在增量链接,所以这里的虚函数表中指向的就是函数真正的地址:

来到校验flag的函数,可以发现存在虚函数的特征:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
BOOL __thiscall sub_4024B0(_DWORD *this, int flag)
{
BOOL result; // eax
this[2] = flag;
result = 0;
if ( (*(int (__thiscall **)(_DWORD *))(*this + 0xC))(this) )
{
(*(void (__thiscall **)(_DWORD *))(*this + 0x18))(this);
if ( (*(int (__thiscall **)(_DWORD *))(*this + 0x28))(this) )
result = 1;
}
return result;
}
|
这里this指针指向的首地址是0x4124E4
,根据偏移可以映射:
1
2
3
|
(*(int (__thiscall **)(_DWORD *))(*this + 0xC))(this) -> 0x4124F0 -> sub_402500
(*(void (__thiscall **)(_DWORD *))(*this + 0x18))(this) -> 0x4124FC -> sub_4026E0
(*(int (__thiscall **)(_DWORD *))(*this + 0x28))(this) -> 0x41250C -> sub_402A00
|
映射:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
BOOL __thiscall sub_4024B0(_DWORD *this, int flag)
{
BOOL result; // eax
this[2] = flag;
result = 0;
if ( sub_402500() )
{
sub_4026E0();
if ( sub_402A00() )
result = 1;
}
return result;
}
|
End