安全矩阵

 找回密码
 立即注册
搜索
查看: 2260|回复: 0

逆向角度看C++部分特性

[复制链接]

249

主题

299

帖子

1391

积分

金牌会员

Rank: 6Rank: 6

积分
1391
发表于 2022-4-3 12:09:05 | 显示全部楼层 |阅读模式
本帖最后由 sandalwood 于 2022-4-3 12:19 编辑



单/多继承

单继承

测试源码:

  1. #define MAIN __attribute__((constructor))
  2. #define NOINLINE __attribute__((__noinline__))

  3. class BaseClass{
  4. public:
  5.     int a,b;
  6.     BaseClass(int mA=1,int mB=2,int mC=3,int mD=4){
  7.         this->a = mA;
  8.         this->b = mB;
  9.         this->c = mC;
  10.         this->d = mD;
  11.     }
  12. private:
  13.     int c;
  14. protected:
  15.     int d;
  16. };

  17. class ChildClass: public BaseClass{
  18. public:
  19.     int m,n;
  20.     ChildClass(int mM=5,int mN=6){
  21.         this->m = mM;
  22.         this->n = mN;
  23.     }
  24. };

  25. MAIN
  26. void test0(){
  27.     auto* baseClass = new BaseClass();
  28.     LOGD("baseClass   : %p sizeof: %d ",baseClass,sizeof(*baseClass));

  29.     auto* child1 = new ChildClass(10,20);
  30.     LOGD("child1  : %p sizeof: %d", child1, sizeof(*child1));
  31. }
复制代码



LOG日志:

内存情况:

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

多继承

  1. // 新增一个BaseNewClass,让ChildClass:BaseClass继承这两个Class
  2. class BaseNewClass{
  3. public:
  4.     int p,q;
  5.     BaseNewClass(int mP=10,int mQ=11){
  6.         this->p = mP;
  7.         this->q = mQ;
  8.     }
  9. };

  10. class ChildClass:BaseClass,BaseNewClass{
  11. public:
  12.     int m,n;
  13.     ChildClass(int mM=5,int mN=6){
  14.         this->m = mM;
  15.         this->n = mN;
  16.     }
  17. };
复制代码

  1. // LOG()日志
  2. D/ZZZ: baseClass   : 0xf216dd70 sizeof: 16
  3. D/ZZZ: childClass  : 0xea17e280 sizeof: 32

  4. // 内存情况
  5. [Pixel XL::XXX]-> seeHexA(0xea17e280,32)
  6.            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
  7. ea17e280  01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
  8. ea17e290  0a 00 00 00 0b 00 00 00 05 00 00 00 06 00 00 00  ................
复制代码


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

虚函数

  1. // 测试源码
  2. class BaseClass{
  3. public:
  4.     int a,b;
  5.     BaseClass(int mA=1,int mB=2,int mC=3,int mD=4){
  6.         this->a = mA;
  7.         this->b = mB;
  8.         this->c = mC;
  9.         this->d = mD;
  10.     }
  11.     virtual void showLOG(){
  12.         LOGD("Called BaseClass showLOG");
  13.     }
  14.     virtual void showLOG1(){
  15.         LOGD("Called BaseClass showLOG1");
  16.     }
  17. private:
  18.     int c;
  19. protected:
  20.     int d;
  21. };

  22. class ChildClass:BaseClass{
  23. public:
  24.     int m,n;
  25.     ChildClass(int mM=5,int mN=6){
  26.         this->m = mM;
  27.         this->n = mN;
  28.     }
  29.     virtual void showLOG(){
  30.         LOGD("Called ChildClass showLOG");
  31.     }
  32.     virtual void showLOG1(){
  33.         LOGD("Called ChildClass showLOG1");
  34.     }
  35.     virtual void showLOG2(){
  36.         LOGD("Called ChildClass showLOG2");
  37.     }
  38. };

  39. MAIN
  40. void test0(){
  41.     auto* baseClass = new BaseClass();
  42.     LOGD("baseClass   : %p sizeof: %d",baseClass,sizeof(*baseClass));

  43.     auto* childClass = new ChildClass();
  44.     LOGD("childClass  : %p sizeof: %d",childClass,sizeof(*childClass));
  45. }
复制代码

  1. // 日志
  2. D/ZZZ: baseClass   : 0xe75a7648 sizeof: 20
  3. D/ZZZ: childClass  : 0xe75d8d00 sizeof: 28

  4. // 内存情况
  5. [Pixel XL::XXX]-> seeHexA(0xe75a7648,20)
  6.            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
  7. e75a7648  24 59 5f d2 01 00 00 00 02 00 00 00 03 00 00 00  $Y_.............
  8. e75a7658  04 00 00 00                                      ....
  9. [Pixel XL::XXX]-> seeHexA(0xe75d8d00,28)
  10.            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
  11. e75d8d00  3c 59 5f d2 01 00 00 00 02 00 00 00 03 00 00 00  <Y_.............
  12. e75d8d10  04 00 00 00 05 00 00 00 06 00 00 00              ............
复制代码


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

  1. //读取vptr指向的位置
  2. [Pixel XL::XXX]-> seeHexA(ptr(0xe75a7648).readPointer(),0x20)
  3.            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
  4. d25f5924  b1 fa 5a d2 c9 fa 5a d2 d4 60 5f d2 39 29 5f d2  ..Z...Z..`_.9)_.
  5. d25f5934  00 00 00 00 48 59 5f d2 e1 fa 5a d2 f9 fa 5a d2  ....HY_...Z...Z.

  6. [Pixel XL::XXX]-> Module.findBaseAddress("libdynamic.so")
  7. "0xd2593000"
  8. [Pixel XL::XXX]-> ptr(0xd25f5924).readPointer().sub(0xd2593000)
  9. "0x1cab1"
  10. [Pixel XL::XXX]-> ptr(0xd25f5928).readPointer().sub(0xd2593000)
  11. "0x1cac9"
  12. [Pixel XL::XXX]-> ptr(0xd25f592c).readPointer().sub(0xd2593000)
  13. "0x630d4"
  14. [Pixel XL::XXX]-> ptr(0xd25f5930).readPointer().sub(0xd2593000)
  15. "0x5f939"
复制代码


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

// IDA查看地址:


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

  1. //
  2. [Pixel XL::XXX]-> seeHexA(ptr(0xe75d8d00).readPointer(),0x20)
  3.            0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
  4. d25f593c  e1 fa 5a d2 f9 fa 5a d2 11 fb 5a d2 30 61 5f d2  ..Z...Z...Z.0a_.
  5. d25f594c  44 29 5f d2 00 00 00 00 01 00 00 00 2c 59 5f d2  D)_.........,Y_.

  6. [Pixel XL::XXX]-> ptr(0xd25f593c).readPointer().sub(0xd2593000)
  7. "0x1cae1"
  8. [Pixel XL::XXX]-> ptr(0xd25f5940).readPointer().sub(0xd2593000)
  9. "0x1caf9"
  10. [Pixel XL::XXX]-> ptr(0xd25f5944).readPointer().sub(0xd2593000)
  11. "0x1cb11"
  12. [Pixel XL::XXX]-> ptr(0xd25f5948).readPointer().sub(0xd2593000)
  13. "0x63130"
  14. [Pixel XL::XXX]-> ptr(0xd25f594c).readPointer().sub(0xd2593000)
  15. "0x5f944"
复制代码



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

// IDA查看地址:


简单归纳一下:

① 继承这种操作其实就是得到了一个父类数据结构的副本,他们的vptr和属于子类部分的数据结构都是独有的。

② 继承后父类的虚函数表也会被子类完全继承。若无覆盖时,子类的虚函数表会完全拷贝一份父类的虚函数表项,并将自己子类的虚函数表项拼接在上表后面。

③ 如果子类覆盖了父类的某一个虚函数,虚函数表项值改变顺序不变。

这里简单的提及了一下,更详细的关于虚函数的介绍可以查看 这篇文章(https://blog.csdn.net/smartgps2008/article/details/90745271)。

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

① Base1 b1 = new Derive(); 将子类的指针转为一个父类指针,只是在c++语法上限制了其对部分操作的可能性。 "子类中的未覆盖父类的成员函数" ,对它的理解应该是:它本是是什么还是什么,语法上的限制完全可以使用指针操作来实现一定程度和语法的背道而驰。他提出的第二点 *"访问non-public的虚函数" 其实和上述这一点也差不多的意思。

② 补充一点:其实对于继承中的成员变量也有同样类似的效果,父类不管把成员的访问权限设置为什么,其实子类都有一个完整的拷贝,同样可以通过指针操作绕过c++语法的禁止,去访问并修改父类非公开成员变量。

拷贝构造

源码以及汇编情况:

  1. NOINLINE
  2. void test1(ChildClass* cls){
  3.     cls->showLOG();
  4. }

  5. NOINLINE
  6. void test2(ChildClass &cls){
  7.     cls.showLOG();
  8. }

  9. NOINLINE
  10. void test3(ChildClass cls){
  11.     cls.showLOG1();
  12. }

  13. NOINLINE
  14. ChildClass test4(ChildClass cls){
  15.     return cls;
  16. }

  17. MAIN
  18. void test0(){
  19.     auto* baseClass = new BaseClass();
  20.     LOGD("baseClass   : %p sizeof: %d  typeid: %s",baseClass,sizeof(*baseClass),typeid(baseClass).name());

  21.     auto* child1 = new ChildClass(10,20);
  22.     LOGD("child1  : %p sizeof: %d  typeid: %d", child1, sizeof(*child1), typeid(child1).hash_code());

  23.     auto* child2 = new ChildClass(*child1);
  24.     child2->showLOG();

  25.     test1(child2);
  26.     test2(*child2);
  27.     test3(*child2);
  28.     test4(*child2);
  29. }
复制代码



// 全局视图:

列举出以下的几种情况:函数参数值传递(值传递和引用传递)

参见 test1 test2 可见:
值传递对于基础数据类型会直接mov出一个副本,值传递对象(class/struct)的话会调用对象的拷贝构造函数得到一个新的副本,所以对于类对象太大的情况建议使用指针传递或者使用引用传递(指针传递和引用传递在汇编层面其实是一样的都是传递了一个指针[见上图])。

参见 test3 可见:
test3进行了值传递,在进入函数前先对ChildClass调用了一次拷贝构造函数,将栈上拷贝出来的该类传递进了 test3。

函数返回值

参见 test4 可见:
test4 和 test3 同样在调用前都先调用了一次拷贝构造函数,但是test4的第一个参数是在栈上提前申请好预留给test4返回的空间,第二个参数为拷贝好的指向副本的类指针,进入test4后也会发现在内部在调用了一次拷贝构造函数,也就是说值传递加上返回值这种写法相比直接引用传递会多调用两次拷贝构造函数。

从一个类创建另一个类

参见 test1(v4) 上面的两句:其实也是调用的拷贝构造函数,v4指向的拷贝好的类在栈上的首地址,第一个代表读取vptr,第二个代表读取vtable的第一个函数(child2->showLOG();就是ChildClass的第一个虚函数),然后再把自己(v4)当成this传递给这个虚函数调用。

拷贝构造拷贝父类

详见下图:// 由编译器为我们生成的拷贝构造函数



  1. ChildClass(const ChildClass &child){
  2.     this->m = child.m;
  3.     this->n = 12;
  4.     LOGD("called ChildClass拷贝构造函数");
  5. }
复制代码

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

// 虚函数表:


由此可见调用子类的拷贝构造函数会先调用父类的构造函数,然后在调用当前类的拷贝构造,这里的off_85600就是 vptr ,从虚函数表中也可以看见,子类覆盖了父类的虚函数就会指向子类的虚函数。

若没有覆盖,表项中依旧是指向父类的函数地址,而且顺序是按照父类的虚函数表顺序排列,子类中父类没有的虚函数会按顺序继续排在后面,不同类的虚函数表其实都是在编译期就已经确定了的,不同类的虚函数表处于临近的内存区域。

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

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


类继承权限

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

类型的强转

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

  1. BaseClass* baseTmp = dynamic_cast<BaseClass*>(child1);
  2. if (baseTmp!= nullptr){
  3.     baseTmp->showLOG1();
  4. }

  5. BaseClass* baseTmp1 = static_cast<BaseClass*>(child1);
  6. if (baseTmp1!= nullptr){
  7.     baseTmp1->showLOG1();
  8. }

  9. ChildClass* baseTmp2 = dynamic_cast<ChildClass*>(baseTmp);
  10. if (baseTmp2!= nullptr){
  11.     baseTmp2->showLOG1();
  12. }
复制代码





// 向下转型

由上两图可见对于类 向上转型 dynamic_cast 和 static_cast 本质是一样的,没有做任何处理。

dynamic_cast 向下转型的时候是借助了 RTTI 机制,就是我们前面图中看到的vptr->vtable 除了虚函数以后的指针标识该类的类型用于动态类型转换,同样也是typeid这个操作符的信息来源,具体可以参考 这篇文章(http://c.biancheng.net/view/2343.html)。

其实虚表什么的都是在编译期间就已经完全确定了,之前还误解以为动态类型转换中的向上转型可以让该子对象调用已经被子对象覆盖的父对象的方法,想多了想多了... 但是如果真想实现这样的"向上转型"也不是不行,借助指针去操作虚函数表即可 ↓

// 实现所谓的"向上转型"



// 效果图



最后两条日志可见,我们对同一对象调用 showLOG() 一个是父函数,一个是子函数,对应代码 815 和 819 行。

简介 lambda

细节介绍参考
这个(https://en.cppreference.com/w/cpp/language/lambda) 和
这篇文章(https://zhuanlan.zhihu.com/p/384314474

引用传递和值传递


关于lambda表达式分为三部分解析

1、表达式位于类中 (为了看到最原始的实现,不要开编译器优化)

  1. #define xASM(x) __asm __volatile__ (x)
  2. using namespace std;

  3. class testA{
  4. private:
  5.     int tempInt0 = 123;
  6.     int tempInt1 = 321;
  7.     int tempInt2 = 110;
  8. public:
  9.     testA(){
  10.         testFunction();
  11.     }

  12.     NOINLINE
  13.     void testFunction(){
  14.         [&]()-> void {
  15.             LOGD("testA anonymous_0 -> tempInt2:%d",this->tempInt2);
  16.         }();

  17.         []()-> void {
  18.             xASM("MOV r4, r0":::"r4");
  19.             int** tmp = nullptr;
  20.             xASM("MOV %0, r4":"=r"(tmp)::);
  21.             LOGD("testA anonymous_1 -> tempInt0:%d",**(tmp + 0x1));
  22.         }();

  23.         []()-> void {
  24.             xASM("MOV r4, r0":::"r4");
  25.             int tmp = 0;
  26.             xASM("LDR r4, [r4, #0x8]":::"r4");
  27.             xASM("LDR r4, [r4, #0x4]":::"r4");
  28.             xASM("MOV %0, r4":"=r"(tmp)::"r4");
  29.             LOGD("testA anonymous_2 -> tempInt1:%d",tmp);
  30.         }();
  31.     }
  32. };
复制代码



构造以及调用testFunction

testFunction汇编也可以明显看到被IDA识别为了lambda表达式。

从这里我们可以看到虽然源码后面两个lambda表达式虽然没有捕获参数,但是依旧有一个栈地址的传递(可以理解为一个空 this)。

传递栈上地址逐个相差一个指针长度。
0x4*6 = 24(0x18)  sp+0x4 sp+0x8 sp+0xc

上述源代码中没有表现出来,即便是空 lambda 实现,编译器依旧会传递一个栈上地址过去,这里也不做展示了。

然后后面两个 lambda 实现主要是为了实践,即使不捕获任何的参数,依旧可以拿到类实例,以及去读取类成员变量。

在类里面的 lambda 可以理解为对 () 的重载(被IDA也是识别为重载 operator())。

读取类成员变量日志


2、表达式位于类外无捕获参数

  1. NOINLINE
  2. void testB(){
  3.     auto testNoCatch = [](int s) -> int {
  4.         LOGD("testB called lambda function : %d", ++s);
  5.         return s;
  6.     };

  7.     LOGD("testB testNoCatch ---> %p",*testNoCatch);

  8. //    using TypeFunc = int(*)(int);
  9.     auto testNoCatchNew = *testNoCatch;
  10.     testNoCatchNew(5);
  11.     testNoCatch(10);
  12. }
复制代码



表达式位于类外无捕获参数


中间函数用来返回lambda函数真实的地址

中间跳板函数


lambda函数的实现,和普通函数没有啥差别。

类外 lambda 函数,*lambda 都会生产这样的一个跳转逻辑。
类内 lambda 函数不管有没有捕获参数都是直接理解为匿名类重载 () 运算符。

testB 日志


3、表达式位于类外有捕获参数

  1. NOINLINE
  2. void testC(){
  3.     int a = 10;
  4.     int* b = new int(11);
  5.     auto c = make_unique<int>(12);
  6.     auto* d = new float(13);

  7.     auto testNoCatch = [](int s,int t) -> void {
  8.         LOGD("testC called lambda function : %d", s+t);
  9.     };

  10.     auto testCatch = [=,&c,&d](int s) -> void {
  11.         LOGD("testC called lambda function : {%d} | {%d,%d,%d,%f}", ++s,a,*b,*c,*d);
  12.     };

  13. //    LOGD("testC  testNoCatch typeid:%s | testCatch typeid:%s",typeid(testNoCatch).name(),typeid(testCatch).name());
  14.     LOGD("testC ------ ");
  15.     testNoCatch(3,4);
  16.     testCatch(2);

  17.     // &testCatch 拿到的是他准备好的参数在栈区的首地址
  18.     LOGD("testC &testCatch %p", &testCatch);
  19.     LOGD("testC &testNoCatch %p", &testNoCatch);

  20.     void* funcAddress = nullptr;
  21.     auto testCatchNew= [=,&c,&d,&funcAddress](int s) -> void {
  22. //        xASM("MOV %0, lr":"=r"(funcAddress)::);
  23. //        [&funcAddress]()-> void{
  24. //            if (funcAddress == nullptr) xASM("MOV %0, lr":"=r"(funcAddress)::);
  25. //        }();
  26.         LOGD("testC called lambda function : {%d} | {%d,%d,%d,%f}", ++s,a,*b,*c,*d);
  27.     };

  28.     testCatchNew(3);
  29.     LOGD(" %p ", funcAddress);
  30.     free(b);
  31. }
复制代码



IDA反汇编

testC 日志
从 sub_319EC(v7, 3, 4) → testNoCatch(3, 4)
可以看出 [] 捕获的参数其实都在第一个参数,lambda的传参在 第二个参数往后,结合把lambda理解为一个重载 () 运算符的类也是自洽的。

从 sub_31A4C(v6, 2) → testCatch(2);
再去对应看v6的参数,也就可以更加理解,lambda 表达式引用传递和值传递的区别,源码中的 c为一个类 (理解为→ 构造 : sub_3198C(v10, &v9); | 析构 : sub_31BB4(v10);),栈传参的时候源码中的引用传递放在最前面,其次按顺序传递参数。

从 sub_3198C(v10, &v9); 和 sub_31BB4(v10)
sub_3198C(v10, &v9) → auto c = make_unique<int>(12);
sub_31BB4(v10) → 作用域结束,对unique指针的析构。

从 上图 29 30 行可见:对带捕获参数的 lambda 表达式取地址得到的只是 匿名类(分配在栈上)的首地址,其实从栈的角度看也是待传参数数组的首地址。

没有带捕获参数的 lambda 表达式 基本上可以等价于一个普通函数,函数地址通过 来获得(编译器针对表达式特殊处理的);带捕获参数的 lambda 表达式 不能使用 ,如果使用 & 只能获得该匿名类首地址,而且 匿名lambda类的构造函数可以理解为inline构造。

lambda 表达式可以使用 [=] / [&] 捕获外部 值传递 / 引用传递,编译器只会把使用到的变量按照对应传递方式传递给匿名lambda类,没用到的变量不会被拷贝。

由此上结论我们可以将 dobby hook 稍微封装一下。



registerHook的重载第三个参数(Callback)本来是想用模板的但是好像不太行。

得到一个类似于java函数回调一样的写法。
这里srcCall可以用一个变长参数简写一下代码。


函数返回对象​​​​​​​
class testClass {public:    int a = 10;    int b = 11;    int c = 12;    int d = 13;    testClass(){        LOGD("testClass");    }     void callTest() {        LOGD("callTest");    }}; NOINLINEtestClass getTestClass() {    auto tmp = testClass();    LOGD("getTestClass %p %d",&tmp,tmp.c);    return tmp;} void testRetClass() {    testClass tt = getTestClass();    LOGD("testRetClass b -> %d",tt.d);}
“SUB SP, SP, #24” 4*6 总计开辟了 6 个位置,最下面的那个位置(fp-0x4) 用于存放栈检查的fp或者说成sp。

正常情况下 class() 构造出来的类就在当前函数栈中,但是这里有一个特例:对于在 函数getTestClass 中 创建在栈上的class(tmp),实际上他真实存在的位置是在 函数testRetClass 的栈上 位置 {fp-0x8,fp-0x14},最顶上的那个位置(sp)是空的(在这里分配栈最小差值0x8),结合上述描述再去看地址 0x4F5FC 就是logd中的第三个参数 “tt.d”。
ps:如果这里的 class 只有三个成员变量,这里的 “SUB SP, SP, #0x18” 将会变成 “SUB SP, SP, #0x10”,刚好用满栈的四个位置。

对于地址 0x4F5E8 这里的这个函数调用就是对 类testClass 的初始化,传递了第一个地址(函数testRetClass栈地址)进去 对 int a,b,c,d 的初始化就放在 “LOGD("testClass");” 之前。

模板类的实现

模板类的实现


实现


  •         它的上一层是一个 plt 跳转。
  •         使用模板对应生成了多个实现方法。





回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-30 14:35 , Processed in 0.015086 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表