C++ PWN

最近看了几道C++的pwn题,头都大了,太难逆向了,所以想学习一下C++的一些知识。

C++基础知识

从C到C++

封装、继承、多态是C++耳熟能详的几个特点。

C++的封装可以理解成C语言中把函数定义在结构体内部。如下面的代码,在C语言中Test是个结构体类型,在C++中就叫成Test类了,test就叫做对象,定义在这个结构体里面的都叫做成员,写在结构体里面的函数就叫做成员函数

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 <stdio.h>

struct Test
{
int a;
int b;
void func()
{
printf("hello world!\n");
}
};

struct Test2
{
int a;
int b;
void func()
{
int c = this->a + this->b;
printf("%d\n", c);
}
};
int main()
{
Test test;
Test2 test2;
test2.a = 1;
test2.b = 2;
test.func();
test2.func();
printf("%d\n", sizeof(test));
return 0;
}

定义在结构体或者类里面的函数不占大小,上面代码中除了定义了两个int型变量还定义了一个函数,但这个结构体的大小依然是2个int大小8。

成员函数在被调用时,会自动传递一个this指针,这个指针是结构体的首地址。上述代码的func函数没有参数,但在实际看程序的反汇编时,可以看到编译器自己传了一个参数(test对象的地址)到rcx中,这个在C++中就叫做this指针。

类与结构体的访问控制

在编写源程序的时候类与结构体的最显著的区别在于类默认的访问控制为private,而结构体则可以看作默认public,类又可以设置成员变量访问控制权限为public,private,protected

但是所有访问控制的检查都是在编译期进行的,也就是说在逆向的时候,结构体和类是没有访问控制的区别的

类的大小

一般情况下类的大小即各成员变量大小之和,而其中也有一些特殊情况

  • gcc编译器C语言空结构体的大小是0,C++空结构体大小为1,写个代码测试一下就晓得了。我的vs2019在C语言中没法定义空结构体

  • 内存对齐:一般成员变量的地址是依次排列在类中的,但是对于类中的不同数据类型编译器会按照一定规则填充字节让内存完成8字节对齐、4字节对齐或2字节对齐等

  • 静态数据成员:类中的静态数据成员存放的位置和全局变量一样位于bss段,只是编译器增加了作用域检查,使其在作用域之外不可见,即只能被同类对象共同享有

构造函数和析构函数

  • 构造函数不可定义返回值,调用构造函数后会返回对象首地址,也就是this指针
  • 对象生成时会自动调用构造函数,找到了定义对象的地方就找到了构造函数的调用时机
  • 在o2选项优化编译之后,某些结构简单的类会被转化为几个连续定义的变量,故不是所有类都有默认的构造函数
  • 在需要调用复制构造函数(拷贝构造函数)的时候,如果没有定义复制构造函数则会直接对副本对象中的成员变量进行复制,也就是进行浅拷贝;否则直接调用定义好的复制构造函数,在定义的复制构造函数中需要处理好分配的堆地址等资源数据,也就是进行深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Person			
{
int age;
int level;
Person()
{
printf("Person对象创建了\n");
}
Person(int age,int level)
{
this->age = age;
this->level = level;
}
void Print()
{
printf("%d-%d\n",age,level);
}
};

构造函数的特点:

  1. 与类同名

  2. 没有返回值

  3. 创建对象的时候执行

  4. 主要用于初始化

  5. 可以有多个(最好有一个无参的),称为重载 其他函数也可以重载

  6. 编译器不要求必须提供

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
struct Person			
{
int age;
int level;
char* arr;
Person()
{
printf("无参构造函数执行了...");
}
Person(int age,int level)
{
printf("有参构造函数执行了...");
this->age = age;
this->level = level;
arr = (char*)malloc(1024);
}
~Person()
{
printf("析构函数执行了...");
free(arr);
arr = NULL;
}
void Print()
{
printf("%d-%d\n",age,level);
}
};

析构函数的特点:

  1. 只能有一个析构函数,不能重载
  2. 不能带任何参数
  3. 不能带返回值
  4. 主要用于清理工作
  5. 编译器不要求必须提供

继承与权限控制

继承的本质就是数据的赋值

可以用父类指针指向子类的对象.

public修饰的成员与普通的成员没有区别 只是编译器会检测.

private修饰的成员只要自己的其他成员才能访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base
{
int x;
int y;
};

int main(int argc, char* argv[])
{
Base base;

base.x = 10;
base.y = 20;

return 0;
}

编译器默认class中的成员为private,而struct中的成员为public

父类中的程序继承后变成private属性,下面Sub类继承Base类之后就无法改变x,y成员的属性了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base				
{
public:
int x;
int y;
};
class Sub:Base
{
public:
int a;
int b;
};
int main(int argc, char* argv[])
{
Sub sub;

sub.x = 1; //无法访问
sub.y = 2; //无法访问
sub.a = 3;
sub.b = 4;

return 0;
}

如果希望可以改变成员的属性,则在继承的时候加上public

1
2
3
4
5
6
class Sub:public Base	
{
public:
int a;
int b;
};

那么private类型的成员是否被继承呢

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
class Base			
{
public:
Base()
{
x = 11;
y = 12;
}
private:
int x;
int y;
};
class Sub:Base
{
public:
int a;
int b;
};

int main(int argc, char* argv[])
{
Sub sub;
sub.a = 1;
sub.b = 2;

int* p = (int*)&sub;


printf("%d\n",sizeof(sub));
printf("%d\n",*(p+0));
printf("%d\n",*(p+1));
printf("%d\n",*(p+2));
printf("%d\n",*(p+3));

return 0;
}

虚函数表

示例代码

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
#include <stdio.h>

class Base
{
public:
void Function_1()
{
printf("Function_1...\n");
}
virtual void Function_2()
{
printf("Function_2...\n");
}
};


void TestMethod()
{
Base base;
base.Function_1();
base.Function_2();

Base * pb = &base;
pb->Function_1();
pb->Function_2();
}

int main()
{
TestMethod();
return 0;
}

  1. 通过对象调用时,virtual函数与普通函数都是E8 Call

  2. 通过指针调用时,virtual函数是FF Call,也就是间接Call

  3. 类中若存在虚函数时,会多出一个属性,32位4个字节,64位8个字节

  4. 这多出来的数据位于对象的首地址处,指向一张表,里面存储了所有虚函数的地址

再看示例代码

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
#include <stdio.h>

class Base
{
public:
int x;
int y;
Base()
{
x = 1;
y = 2;
}
virtual void Function_1()
{
printf("Function_1...\n");
}
virtual void Function_2()
{
printf("Function_2...\n");
}
virtual void Function_3()
{
printf("Function_3...\n");
}
};

void TestMethod()
{
Base base;
printf("base的虚函数表地址: %p\n", *(int*)(&base));

}

int main()
{
TestMethod();
return 0;
}

把反汇编复制出来

1
2
3
4
5
6
7
8
Base::Function_2:
00C81307 E9 84 05 00 00 jmp Base::Function_2 (0C81890h)

Base::Function_1:
00C813E3 E9 38 04 00 00 jmp Base::Function_1 (0C81820h)

Base::Function_3:
00C813E8 E9 13 05 00 00 jmp Base::Function_3 (0C81900h)

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!