默小白 发表于 2019-5-28 12:20

【转帖】C++逆向学习(四) 类

转自:https://xz.aliyun.com/t/5242

上一篇文章分析了**移动构造函数**,这篇详细的分析一下`C++类`的逆向相关内容

已经有很多书和文章分析的比较清楚了,本文尽可能展现一些有新意的内容

# 测试代码

基类`base`,派生类`derived`,分别有成员变量、成员函数、虚函数

```
#include<stdio.h>
#include<stdlib.h>

class base {
public:
    int a;
    double b;
    base() {
      this->a = 1;
      this->b = 2.3;
      printf("base constructor\n");
    }
    void func() {
      printf("%d%lf\n", a, b);
    }
    virtual void v_func() {
      printf("base v_func()\n");
    }
    ~base() {
      printf("base destructor\n");
    }
};

class derived :public base {
public:
    derived() {
      printf("derived constructor\n");
    }
    virtual void v_func() {
      printf("derived v_func()");
    }
    ~derived() {
      printf("derived destructor\n");
    }
};


int main(int argc, char** argv) {
    base a;
    a.func();
    a.v_func();

    base* b = (base*)new derived();
    b->func();
    b->v_func();
    return 0;
}
```

编译:`g++ test.cpp -o test`

# IDA视角

IDA打开,如下:

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133653-9af22c0e-7c53-1.png)

# this指针

可以看到,`base::`的每个函数都传入了一个参数`(base*)&v5`,正是类实例的`this指针`

以下是普通成员函数`func()`的调用过程

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133709-a442cf8e-7c53-1.png)

```
rdi`作为第一个参数,存放`this`指针,而`windows`下是寄存器`rcx
```

> `this`指针是识别类成员函数的一个关键
>
> 如果看到C++生成的exe文件中,如果`rcx`寄存器还没有被初始化就直接使用,很可能是类的成员函数

# 构造、析构

考虑构造函数时的过程

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133715-a8162f02-7c53-1.png)

其中`*this = off_400C18`,即先把类的虚表地址赋值给类实例的首字段

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133721-aba011f6-7c53-1.png)

> 补充一些

注意虚表前还有一个`typeinfo`,在`g++`的实现中,真正的`typeinfo`信息在虚表之后,虚表的前一个字段存放了`typeinfo`的地址

> `typeinfo`是编译器生成的特殊类型信息,包括对象继承关系、对象本身的描述等

```
Aclass* ptra=new Bclass;
int ** ptrvf=(int**)(ptra);
RTTICompleteObjectLocator str=
*((RTTICompleteObjectLocator*)(*((int*)ptrvf-1)));   //vptr-1
```

这段获取对象RTTI信息相关的代码也显示了这一点

> 回到构造和析构函数

在构造函数调用中,显然需要将虚表的地址赋值给类实例的虚表指针,从代码上来看也是这样

但是,我们观察base类的析构函数

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133729-b04687bc-7c53-1.png)

**析构时也首先重新赋值了虚表指针**,看起来可能有点多此一举

但如果析构函数中调用了虚函数,此行为可以保证正确;至于如果不重新赋值会有错误行为的情况就不展开了

> 虚表指针的赋值是识别的一个关键,排除开发者故意伪造编译器生成的代码来误导分析,基本可以确定是**构造函数**或者**析构函数**

同样的,找到了虚表,也就可以根据IDA的交叉引用,找到对应的**构造函数**和**析构函数**

## 构造、析构代{过}{滤}理函数

全局对象和静态对象的构造时机相同,可以说是被隐藏了起来,在main函数之前由构造代{过}{滤}理函数统一构造

测试代码:

```
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>

using namespace std;

class t {
public:
    char* str;
    t() {
      cout << "constructor" << endl;
      this->str = new char;
      memcpy(this->str, "hello", 12);
    }
    ~t() {
      cout << this->str << endl;
    }
};

t ts;

int main(int argc, char** argv) {
    cout << "main" << endl;
    return 0;
}
```

编译:`visual studio 2019 x64 release`

IDA打开,根据输出,下断点后发现t类全局变量构造函数输出信息调用于`initterm_0`函数

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133736-b4a84c50-7c53-1.png)

一段`initterm_0`的代码实现如下:

```
while (pfbegin < pfend) {
    //pfbegin == __xc_a , pfend == __xc_z
    if (*pfbegin != NULL) {
      (**pfbegin)(); //调用每一个初始化或构造代{过}{滤}理函数
      ++pfbegin();
    }
}
```

> 执行`(**pfbegin)()`后并不会进入全局对象的构造函数中,而是进入编译器提供的**构造代{过}{滤}理函数**

最简单的找到全局对象构造函数的方法:因为构造代{过}{滤}理函数中会**注册析构函数**,其注册方式是使用`atexit`,我们对`atexit`下断点,调试过程中很容易在附近找到全局对象构造的构造函数

如图所示,`10`即为对象数组的大小,并且最后一个参数传入了构造函数指针`t::t()`

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133742-b8178306-7c53-1.png)

**析构代{过}{滤}理函数**比较类似,就不多分析了,同样以`atexit`为切入点

> `t::_t`即为`t`类的析构函数

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133747-bb08e7ee-7c53-1.png)

# 虚函数调用

代码中我们用`base*`指针指向了`new derived()`,在IDA里如下

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133753-bea6f6ca-7c53-1.png)

v3作为derived类实例的地址,存放的正好是虚表指针,而`v_func()`正好在虚表的第一个位置,参数`v3`则是例行传入`this`指针

> 已经有很多文章讲过虚函数调用过程了,这里就只是简单说一下

# 虚基类继承

主要分析一下**菱形继承**的内存布局,代码如下:

```
#include<stdio.h>
#include<stdlib.h>

//间接基类
class A {
public:
    virtual void function() {
      printf("A virtual function\n");
    }
    int a;
};

//直接基类
class B :virtual public A { //虚继承
public:
    virtual void func() {
      printf("B virtual func()\n");
    }
    int b;
};

//直接基类
class C :virtual public A { //虚继承
public:
    virtual void func() {
      printf("C virtual func()");
    }
    int c;
};

//派生类
class D :public B, public C {
public:
    virtual void function() {
      printf("D virtual function()");
    }
    int d;
};


int main(int argc, char** argv) {
    A* A_ptr = (A*)new D();
    A_ptr->function();

    return 0;
}
```

编译:`visual studio 2019 x64 release`

B、C类都虚继承了A类,然后D类多重继承于B、C类

布局如图:

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133841-db13d27e-7c53-1.png)

> 具体实现是在B、C类里不再保存A类的内容,而是保存一份**偏移地址**,然后将A类的数据保存在一个公共位置处,降低数据冗余

为方便说明,使用`g++`编译并用IDA打开

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133846-de15d170-7c53-1.png)

main函数比较清晰,跟进D类的构造函数

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133851-e14cf5b2-7c53-1.png)

> 虚表占8字节,int占4字节,考虑字节对齐,实际B、C类都占了16字节

接着用gdb跟进一下,断在`(**func)(func)`上

!(https://xzfile.aliyuncs.com/media/upload/picture/20190522133857-e4973570-7c53-1.png)

已经分析过,D类的首字段即存放了B类的虚表,也就是`RBX==0x614c20`是D类实例地址

IDA可以看到`0x400A90==A::vtable`,也就是先找到A类的虚表

而A类虚表实际存放的函数指针值,由于虚函数机制被`D::function()`覆盖,会实际调用到D类对应的函数

# 补充

关于如何让IDA里的分析更清晰,添加结构体、类的信息来帮助IDA的内容,网上已经有很多,这里不再多说了

推荐一本书《深度探索C++对象模型》,里面有很多类布局的历史实现,以及这些布局设计时对空间、时间效率的权衡

JerryLia 发表于 2019-5-28 12:55

g 感谢分享,下载看

cat95f 发表于 2019-5-28 13:15

虽然不懂,以后慢慢学

muzb 发表于 2019-5-28 13:47

shangziq 发表于 2019-5-28 15:56

学习了,谢谢楼主分享!

a123037583 发表于 2019-5-28 16:31


学习了,谢谢楼主分享

wojciech 发表于 2019-5-28 17:14

我感觉有C的影子,头文件和输出语句典型的C形式,C++不应该是iostream和cout吗?

zxy453326 发表于 2019-5-28 18:54

大佬大佬。。。。

xie83544109 发表于 2019-5-28 19:57

{:1_918:}
楼主,请问有视频教材不

hac11az 发表于 2019-5-28 20:29

学习了,感谢楼主分享~~~~~
页: [1] 2 3
查看完整版本: 【转帖】C++逆向学习(四) 类