首页
社区
课程
招聘
12
[原创]逆向角度看C++部分特性
发表于: 2022-3-10 16:20 22286

[原创]逆向角度看C++部分特性

2022-3-10 16:20
22286

单/多继承

  • 单继承
    测试源码
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
#define MAIN __attribute__((constructor))
#define NOINLINE __attribute__((__noinline__))
 
class BaseClass{
public:
    int a,b;
    BaseClass(int mA=1,int mB=2,int mC=3,int mD=4){
        this->a = mA;
        this->b = mB;
        this->c = mC;
        this->d = mD;
    }
private:
    int c;
protected:
    int d;
};
 
class ChildClass: public BaseClass{
public:
    int m,n;
    ChildClass(int mM=5,int mN=6){
        this->m = mM;
        this->n = mN;
    }
};
 
MAIN
void test0(){
    auto* baseClass = new BaseClass();
    LOGD("baseClass   : %p sizeof: %d ",baseClass,sizeof(*baseClass));
 
    auto* child1 = new ChildClass(10,20);
    LOGD("child1  : %p sizeof: %d", child1, sizeof(*child1));
}

LOG日志
LOG日志

 

内存情况

 

可以看到实际上单继承就是把 baseClass 的成员变量完全copy了一份放在了我们childClass的前面

  • 多继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 新增一个BaseNewClass,让ChildClass:BaseClass继承这两个Class
class BaseNewClass{
public:
    int p,q;
    BaseNewClass(int mP=10,int mQ=11){
        this->p = mP;
        this->q = mQ;
    }
};
 
class ChildClass:BaseClass,BaseNewClass{
public:
    int m,n;
    ChildClass(int mM=5,int mN=6){
        this->m = mM;
        this->n = mN;
    }
};
1
2
3
4
5
6
7
8
9
// LOG()日志
D/ZZZ: baseClass   : 0xf216dd70 sizeof: 16
D/ZZZ: childClass  : 0xea17e280 sizeof: 32
 
// 内存情况
[Pixel XL::XXX]-> seeHexA(0xea17e280,32)
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
ea17e280  01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
ea17e290  0a 00 00 00 0b 00 00 00 05 00 00 00 06 00 00 00  ................

其实也都是成员变量按顺序往后排就完事

虚函数

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
39
40
41
42
43
44
45
46
47
48
// 测试源码
class BaseClass{
public:
    int a,b;
    BaseClass(int mA=1,int mB=2,int mC=3,int mD=4){
        this->a = mA;
        this->b = mB;
        this->c = mC;
        this->d = mD;
    }
    virtual void showLOG(){
        LOGD("Called BaseClass showLOG");
    }
    virtual void showLOG1(){
        LOGD("Called BaseClass showLOG1");
    }
private:
    int c;
protected:
    int d;
};
 
class ChildClass:BaseClass{
public:
    int m,n;
    ChildClass(int mM=5,int mN=6){
        this->m = mM;
        this->n = mN;
    }
    virtual void showLOG(){
        LOGD("Called ChildClass showLOG");
    }
    virtual void showLOG1(){
        LOGD("Called ChildClass showLOG1");
    }
    virtual void showLOG2(){
        LOGD("Called ChildClass showLOG2");
    }
};
 
MAIN
void test0(){
    auto* baseClass = new BaseClass();
    LOGD("baseClass   : %p sizeof: %d",baseClass,sizeof(*baseClass));
 
    auto* childClass = new ChildClass();
    LOGD("childClass  : %p sizeof: %d",childClass,sizeof(*childClass));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 日志
D/ZZZ: baseClass   : 0xe75a7648 sizeof: 20
D/ZZZ: childClass  : 0xe75d8d00 sizeof: 28
 
// 内存情况
[Pixel XL::XXX]-> seeHexA(0xe75a7648,20)
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
e75a7648  24 59 5f d2 01 00 00 00 02 00 00 00 03 00 00 00  $Y_.............
e75a7658  04 00 00 00                                      ....
[Pixel XL::XXX]-> seeHexA(0xe75d8d00,28)
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
e75d8d00  3c 59 5f d2 01 00 00 00 02 00 00 00 03 00 00 00 
e75d8d10  04 00 00 00 05 00 00 00 06 00 00 00              ............

由上我们可以看到这两个Class的地址的开始位置都多了一个指针,指针后面的才是我们真实的结构体值,这第一个指针就是 vptr(虚函数指针),指向了虚函数表,然后再去读一下这个指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//读取vptr指向的位置
[Pixel XL::XXX]-> seeHexA(ptr(0xe75a7648).readPointer(),0x20)
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
d25f5924  b1 fa 5a d2 c9 fa 5a d2 d4 60 5f d2 39 29 5f d2  ..Z...Z..`_.9)_.
d25f5934  00 00 00 00 48 59 5f d2 e1 fa 5a d2 f9 fa 5a d2  ....HY_...Z...Z.
 
[Pixel XL::XXX]-> Module.findBaseAddress("libdynamic.so")
"0xd2593000"
[Pixel XL::XXX]-> ptr(0xd25f5924).readPointer().sub(0xd2593000)
"0x1cab1"
[Pixel XL::XXX]-> ptr(0xd25f5928).readPointer().sub(0xd2593000)
"0x1cac9"
[Pixel XL::XXX]-> ptr(0xd25f592c).readPointer().sub(0xd2593000)
"0x630d4"
[Pixel XL::XXX]-> ptr(0xd25f5930).readPointer().sub(0xd2593000)
"0x5f939"

此时打开IDA验证一下这前两个地址就是真实的函数地址
// IDA查看地址


 

同理我们去看看另一个childClass类也会得到类似的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
[Pixel XL::XXX]-> seeHexA(ptr(0xe75d8d00).readPointer(),0x20)
           0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
d25f593c  e1 fa 5a d2 f9 fa 5a d2 11 fb 5a d2 30 61 5f d2  ..Z...Z...Z.0a_.
d25f594c  44 29 5f d2 00 00 00 00 01 00 00 00 2c 59 5f d2  D)_.........,Y_.
 
[Pixel XL::XXX]-> ptr(0xd25f593c).readPointer().sub(0xd2593000)
"0x1cae1"
[Pixel XL::XXX]-> ptr(0xd25f5940).readPointer().sub(0xd2593000)
"0x1caf9"
[Pixel XL::XXX]-> ptr(0xd25f5944).readPointer().sub(0xd2593000)
"0x1cb11"
[Pixel XL::XXX]-> ptr(0xd25f5948).readPointer().sub(0xd2593000)
"0x63130"
[Pixel XL::XXX]-> ptr(0xd25f594c).readPointer().sub(0xd2593000)
"0x5f944"

第一二三个:明显就是对应的虚函数具体的函数地址
第四五个:应该是和 C++中的RTTI机制 相关

 

// IDA查看地址

 

简单归纳一下:

  1. 继承这种操作其实就是得到了一个父类数据结构的副本,他们的vptr和属于子类部分的数据结构都是独有的
  2. 继承后父类的虚函数表也会被子类完全继承。若无覆盖时,子类的虚函数表会完全拷贝一份父类的虚函数表项,并将自己子类的虚函数表项拼接在上表后面
  3. 如果子类覆盖了父类的某一个虚函数,虚函数表项值改变顺序不变

这里简单的提及了一下,更详细的关于虚函数的介绍可以查看 这篇文章

 

至于里面提到的关于 安全性 的反思:

  1. Base1 b1 = new Derive(); 将子类的指针转为一个父类指针,只是在c++语法上限制了其对部分操作的可能性。 "子类中的未覆盖父类的成员函数" ,对它的理解应该是:它本是是什么还是什么,语法上的限制完全可以使用指针操作来实现一定程度和语法的背道而驰。他提出的第二点 *"访问non-public的虚函数" 其实和上述这一点也差不多的意思
  2. 补充一点:其实对于继承中的成员变量也有同样类似的效果,父类不管把成员的访问权限设置为什么,其实子类都有一个完整的拷贝,同样可以通过指针操作绕过c++语法的禁止,去访问并修改父类非公开成员变量

拷贝构造

  • 源码以及汇编情况
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
NOINLINE
void test1(ChildClass* cls){
    cls->showLOG();
}
 
NOINLINE
void test2(ChildClass &cls){
    cls.showLOG();
}
 
NOINLINE
void test3(ChildClass cls){
    cls.showLOG1();
}
 
NOINLINE
ChildClass test4(ChildClass cls){
    return cls;
}
 
MAIN
void test0(){
    auto* baseClass = new BaseClass();
    LOGD("baseClass   : %p sizeof: %d  typeid: %s",baseClass,sizeof(*baseClass),typeid(baseClass).name());
 
    auto* child1 = new ChildClass(10,20);
    LOGD("child1  : %p sizeof: %d  typeid: %d", child1, sizeof(*child1), typeid(child1).hash_code());
 
    auto* child2 = new ChildClass(*child1);
    child2->showLOG();
 
    test1(child2);
    test2(*child2);
    test3(*child2);
    test4(*child2);
}

// 全局视图

  • 列举出以下的几种情况

    1. 函数参数值传递(值传递和引用传递)
    • 参见 test1 test2 可见:
      值传递对于基础数据类型会直接mov出一个副本,值传递对象(class/struct)的话会调用对象的拷贝构造函数得到一个新的副本,所以对于类对象太大的情况建议使用指针传递或者使用引用传递(指针传递和引用传递在汇编层面其实是一样的都是传递了一个指针[见上图])
    • 参见 test3 可见:
      test3进行了值传递,在进入函数前先对ChildClass调用了一次拷贝构造函数,将栈上拷贝出来的该类传递进了 test3
    1. 函数返回值
    • 参见 test4 可见:
      test4 和 test3 同样在调用前都先调用了一次拷贝构造函数,但是test4的第一个参数是在栈上提前申请好预留给test4返回的空间,第二个参数为拷贝好的指向副本的类指针,进入test4后也会发现在内部在调用了一次拷贝构造函数,也就是说值传递加上返回值这种写法相比直接引用传递会多调用两次拷贝构造函数
    1. 从一个类创建另一个类
    • 参见 test1(v4) 上面的两句:
      其实也是调用的拷贝构造函数,v4指向的拷贝好的类在栈上的首地址,第一个代表读取vptr,第二个代表读取vtable的第一个函数(child2->showLOG();就是ChildClass的第一个虚函数),然后再把自己(v4)当成this传递给这个虚函数调用
      4.拷贝构造拷贝父类
      • 详见下图
        // 由编译器为我们生成的拷贝构造函数
1
2
3
4
5
ChildClass(const ChildClass &child){
    this->m = child.m;
    this->n = 12;
    LOGD("called ChildClass拷贝构造函数");
}

// 由我们自己编写的拷贝构造函数

 

// 虚函数表

 

由此可见调用子类的拷贝构造函数会先调用父类的构造函数,然后在调用当前类的拷贝构造,这里的off_85600就是 vptr ,从虚函数表中也可以看见,子类覆盖了父类的虚函数就会指向子类的虚函数,若没有覆盖,表项中依旧是指向父类的函数地址,而且顺序是按照父类的虚函数表顺序排列,子类中父类没有的虚函数会按顺序继续排在后面,不同类的虚函数表其实都是在编译期就已经确定了的,不同类的虚函数表处于临近的内存区域

类的 构造/析构 函数调用时机

  • 详见下图(ChildClass中新增了一个析构函数)
    // 新增析构函数

类继承权限

  • 类的继承权限并不会影响子类继承父类子类所拥有的父类的成员变量个数,换句话说,不管父类的成员变量是什么权限,之类都完全拥有一份父类的成员变量的拷贝(这里就不展示)

类型的强转

  • 主要是针对 dynamic_cast 向下转型的情况

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    BaseClass* baseTmp = dynamic_cast*>(child1);
    if (baseTmp!= nullptr){
        baseTmp->showLOG1();
    }
     
    BaseClass* baseTmp1 = static_cast*>(child1);
    if (baseTmp1!= nullptr){
        baseTmp1->showLOG1();
    }
     
    ChildClass* baseTmp2 = dynamic_cast*>(baseTmp);
    if (baseTmp2!= nullptr){
        baseTmp2->showLOG1();
    }

// 向下转型

  1. 由上两图可见对于类 向上转型 dynamic_cast 和 static_cast 本质是一样的,没有做任何处理
  2. dynamic_cast 向下转型的时候是借助了 RTTI 机制,就是我们前面图中看到的vptr->vtable 除了虚函数以后的指针标识该类的类型用于动态类型转换,同样也是typeid这个操作符的信息来源,具体可以参考 这篇文章
  3. 其实虚表什么的都是在编译期间就已经完全确定了,之前还误解以为动态类型转换中的向上转型可以让该子对象调用已经被子对象覆盖的父对象的方法,想多了想多了... 但是如果真想实现这样的"向上转型"也不是不行,借助指针去操作虚函数表即可 ↓
    // 实现所谓的"向上转型"

// 效果图

 

[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

最后于 2022-3-30 11:52 被唱过阡陌编辑 ,原因: 完善文章
收藏
免费 12
支持
分享
赞赏记录
参与人
雪币
留言
时间
東陽不列山
感谢你的贡献,论坛因你而更加精彩!
2024-11-4 03:15
归来何去
为你点赞~
2024-4-16 12:29
0xEA
为你点赞~
2023-4-12 16:01
伟叔叔
为你点赞~
2023-3-18 03:46
PLEBFE
为你点赞~
2022-7-27 23:45
云天逵
为你点赞~
2022-3-17 10:04
无造
为你点赞~
2022-3-13 02:45
阿碧
为你点赞~
2022-3-11 21:43
yxylwt
为你点赞~
2022-3-11 16:13
GitRoy
为你点赞~
2022-3-11 13:09
lanoche
为你点赞~
2022-3-11 11:14
卧勒个槽
为你点赞~
2022-3-10 18:43
打赏 + 50.00雪花
打赏次数 1 雪花 + 50.00
收起 
赞赏  Editor   +50.00 2022/04/02 恭喜您获得“雪花”奖励,安全圈有你而精彩!
最新回复 (7)
雪    币: 136
活跃值: (1212)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
支持一下
2022-3-11 11:21
0
雪    币: 5308
活跃值: (5624)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
3
真的好,花了20分钟仔细看完了。希望大佬能多写一些基础的,复习或者补充遗漏的知识
2022-3-11 11:40
0
雪    币: 2042
活跃值: (606)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
真的好
2022-3-11 16:13
0
雪    币: 959
活跃值: (66)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
请问Pixel XL::XXX 是什么工具,开源的还是楼主自己写的,谢谢
2022-3-12 11:39
0
雪    币: 3268
活跃值: (3209)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
6
ybhdgggset 请问Pixel XL::XXX 是什么工具,开源的还是楼主自己写的,谢谢
Frida连接Android手机。其实也可以使用调试器实现相同效果。
2022-3-13 09:58
0
雪    币: 959
活跃值: (66)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
xhyeax Frida连接Android手机。其实也可以使用调试器实现相同效果。
谢谢
2022-3-20 17:17
0
雪    币: 70
活跃值: (2183)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
细 并且耐心 
2023-6-21 18:31
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册