夜间模式暗黑模式
字体
阴影
滤镜
圆角
C++虚函数和UAF

0x01 前置知识

虚函数

在C++里面,如果类里面有虚函数,它就会有一个虚函数表的指针__vfptr(virtual function pointer),存放在类对象最开始的内存数据中,在这之后就是类中成员变量的内存数据。对于其子类来说,最开始的内存数据记录着父类对象的拷贝,包括父类虚函数表指针和成员变量,紧接其后的是子类自己的成员变量数据。

BaseClass

  +-------------+
  |             |
  | __vfptr   | <------+ when virtual
  |             |         function exists
  +-------------+
  |             |
  | Base data | +-------------+
  |             |               |
  +-------------+               | (same as)
                                |
Child                           v

  +-------------+         +------------+
  |             |         | __vfptr   |
  | base copy | +------> +------------+
  |             |         | base data |
  +-------------+         +------------+
  |             |
  | child data |
  |             |
  +-------------+

有虚函数重载和无虚函数重载的区别:

image-20200517162029340
image-20200517162054502

多重继承的例子:

image-20200517162803399

如果类里面有虚函数,首先会建立一张虚函数表,子类首先继承父类vtable,如果父类的vtable中有私有的虚函数,子类vtable中同样有该私有虚函数的地址。当子类重载父类虚函数的时候,修改vtable同名的函数地址并改为指向子类的函数地址。如果子类中有新的虚函数,添加在vtable尾部。

UAF

UAF全称是Use-After-Free,就是对悬垂指针所指向的内存进行利用。如将悬垂指针所指向的内存重新分配回来,且尽可能地使该内存中的内容可控,比如重新分配为字符串。

image-20200517165717674

比如有一个结构体指针p,在释放掉p之后,没有将p置NULL,所以p变成Dangling pointer,再通过重新分配,再次拿到p之前指向的这段地址空间。之后,通过strcpy(p2,”addr”),或者其他方式,向这段地址空间写入新数据。然后当我们通过其他函数,再次使用p指针,就会造成无法预料的后果,因为此时p指针指向的内存包含的已经是完全不同的数据。

0x02 pwnable.kr uaf

class Human{
   private:
  virtual void give_shell(){
           system("/bin/sh");
      }
  protected:
  int age;
  string name;
   public:
  virtual void introduce(){
           cout << "My name is " << name << endl;
           cout << "I am " << age << " years old" << endl;
      }
};

第一个类Human中有虚函数,那么类Human具有一个vtable,这个vtable中记录了类中所有虚函数的函数指针,即包括give_shell和introduce两个函数的函数指针。在vtable后面是类的数据部分。

class Man: public Human{
   public:
  Man(string name, int age){
           this->name = name;
           this->age = age;
      }
   virtual void introduce(){
       Human::introduce();
       cout << "I am a nice guy!" << endl;
  }
};

class Woman: public Human{
   public:
  Woman(string name, int age){
           this->name = name;
           this->age = age;
      }
  virtual void introduce(){
           Human::introduce();
           cout << "I am a cute girl!" << endl;
      }
}

上面创建了一个男人类和一个女人类,都是继承了Human类,并且都实现了各自的introduce方法。这两个类都会继承父类的vtable,并且vtable里面的introduce函数指针将会被替换成各自的函数地址。

int main(int argc, char* argv[]){
   Human* m = new Man("Jack", 25);
   Human* w = new Woman("Jill", 21);
   
   size_t len;
   char * data;
   unsigned int op;
   while (1){
       cout << "1. use\n2. after\n3. free\n";
       cin >> op;
       
       switch (op){
           case 1:
               m->introduce();
               w->introduce();
               break;
           case 2:
               len = atoi(argv[1]);
               data = new char[len];
               read(open(argv[2], O_RDONLY), data, len);
               cout << "your data is allocated" << endl;
               break;
        case 3;
               delete m;
               delete w;
               break;
           default:
               break;
      }
  }
   return 0;
   
}

查看一下main函数,存在switch分支:

  • 1 -> 调用两个类的函数
  • 2 -> 分配data空间,从文件名为argv[2]中读取长度为argv[1]的字符到data部分。
  • 3 -> 释放对象

容易设计出的攻击链是:首先执行case3,把对象空间释放,指针置NULL;执行case2,因为data在分配空间的时候是分配到刚释放的空间里去的,可以打成修改introduce为give_shell,最后调用case1完成攻击。这里需要注意下free的顺序是先free的m,后free的w,因此在分配内存的时候优先分配到后释放的w,因此需要先申请一次空间,将w分配出去,再开一次,就能分配到m了。执行的顺序是3221。然后找到对应的虚表地址改成give_shell的地址就可以了。(可能在本机上编译会出现地址出现偏移的情况,初判断是编译的原因,请具体分析)

函数的执行是根据虚表偏移来找函数执行的,因为give_shell在introduce的前面,所以只要调整vptr为vptr-8就可以。

评论

  1. 赤道企鹅
    Windows Edge 84.0.522.40

    学习了.😁

    2 months前
    2020-7-20 23:28:24

发送评论 编辑评论


				
上一篇
下一篇