内容简介:此文为原创文章作者:ret2nullptr@先知社区
此文为原创文章
作者:ret2nullptr@先知社区
恭喜作者获得
价值100元的天猫超市享淘卡一张
欢迎更多优质原创、翻译作者加入
ASRC文章奖励计划
欢迎多多投稿到先知社区
每天一篇优质技术好文
点滴积累促成质的飞跃
今天也要进步一点点呀
上一篇文章分析了 移动构造函数 ,这篇详细的分析一下 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打开,如下:
this指针
可以看到, base::
的每个函数都传入了一个参数 (base*)&v5
,正是类实例的 this指针
以下是普通成员函数 func()
的调用过程
rdi
作为第一个参数,存放 this
指针,而 windows
下是寄存器 rcx
this
指针是识别类成员函数的一个关键
如果看到C++生成的exe文件中,如果 rcx
寄存器还没有被初始化就直接使用,很可能是类的成员函数
构造、析构
考虑构造函数时的过程
其中 *this = off_400C18
,即先把类的虚表地址赋值给类实例的首字段
补充一些
注意虚表前还有一个 typeinfo
,在 g++
的实现中,真正的 typeinfo
信息在虚表之后,虚表的前一个字段存放了 typeinfo
的地址
typeinfo
是编译器生成的特殊类型信息,包括对象继承关系、对象本身的描述等
Aclass* ptra=new Bclass; int ** ptrvf=(int**)(ptra); RTTICompleteObjectLocator str= *((RTTICompleteObjectLocator*)(*((int*)ptrvf[0]-1))); //vptr-1
这段获取对象RTTI信息相关的代码也显示了这一点
回到构造和析构函数
在构造函数调用中,显然需要将虚表的地址赋值给类实例的虚表指针,从代码上来看也是这样
但是,我们观察base类的析构函数
析构时也首先重新赋值了虚表指针,看起来可能有点多此一举
但如果析构函数中调用了虚函数,此行为可以保证正确;至于如果不重新赋值会有错误行为的情况就不展开了
虚表指针的赋值是识别的一个关键,排除开发者故意伪造编译器生成的代码来误导分析,基本可以确定是 构造函数 或者 析构函数
同样的,找到了虚表,也就可以根据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[16]; memcpy(this->str, "hello", 12); } ~t() { cout << this->str << endl; } }; t ts[10]; int main(int argc, char** argv) { cout << "main" << endl; return 0; }
编译: visual studio 2019 x64 release
IDA打开,根据输出,下断点后发现t类全局变量构造函数输出信息调用于 initterm_0
函数
一段 initterm_0
的代码实现如下:
while (pfbegin < pfend) { //pfbegin == __xc_a , pfend == __xc_z if (*pfbegin != NULL) { (**pfbegin)(); //调用每一个初始化或构造代理函数 ++pfbegin(); } }
执行 (**pfbegin)()
后并不会进入全局对象的构造函数中,而是进入编译器提供的 构造代理函数
最简单的找到全局对象构造函数的方法:因为构造代理函数中会 注册析构函数 ,其注册方式是使用 atexit
,我们对 atexit
下断点,调试过程中很容易在附近找到全局对象构造的构造函数
如图所示, 10
即为对象数组的大小,并且最后一个参数传入了构造函数指针 t::t()
析构代理函数比较类似,就不多分析了,同样以 atexit
为切入点
t::_t
即为 t
类的析构函数
虚函数调用
代码中我们用 base*
指针指向了 new derived()
,在IDA里如下
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类
布局如图:
具体实现是在B、C类里不再保存A类的内容,而是保存一份 偏移地址 ,然后将A类的数据保存在一个公共位置处,降低数据冗余
为方便说明,使用 g++
编译并用IDA打开
main函数比较清晰,跟进D类的构造函数
虚表占8字节,int占4字节,考虑字节对齐,实际B、C类都占了16字节
接着用gdb跟进一下,断在 (**func)(func)
上
已经分析过,D类的首字段即存放了B类的虚表,也就是 RBX==0x614c20
是D类实例地址
IDA可以看到 0x400A90==A::vtable
,也就是先找到A类的虚表
而A类虚表实际存放的函数指针值,由于虚函数机制被 D::function()
覆盖,会实际调用到D类对应的函数
补充
关于如何让IDA里的分析更清晰,添加结构体、类的信息来帮助IDA的内容,网上已经有很多,这里不再多说了
推荐一本书《深度探索C++对象模型》,里面有很多类布局的历史实现,以及这些布局设计时对空间、时间效率的权衡
更
多
精
彩
请猛戳右边二维码
Twitter:AsrcSecurity
公众号ID
阿里安全响应中心
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 一文读懂监督学习、无监督学习、半监督学习、强化学习这四种深度学习方式
- 学习:人工智能-机器学习-深度学习概念的区别
- 统计学习,机器学习与深度学习概念的关联与区别
- 混合学习环境下基于学习行为数据的学习预警系统设计与实现
- 学习如何学习
- 深度学习的学习历程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C# 6.0本质论
[美] Mark Michaelis(马克·米凯利斯)、[美] Eric Lippert(埃里克·利珀特) / 周靖、庞燕 / 人民邮电出版社 / 2017-2-1 / 108
这是C#领域中一部广受好评的名作,作者用一种易于理解的方式详细介绍了C#语言的各个方面。全书共有21章和4个附录(其中哟2个附录从网上下载),介绍了C#语言的数据类型、操作符、方法、类、接口、异常处理等基本概念,深入讨论了泛型、迭代器、反射、线程和互操作性等高级主题,还介绍了LINQ技术,以及与其相关的扩展方法、分部方法、Lambda表达式、标准查询操作符和查询表达式等内容。每章开头的“思维导图”......一起来看看 《C# 6.0本质论》 这本书的介绍吧!