Pwnable-UAF
这道题主要考察的是虚函数的内存地址空间以及UAF的使用
所需知识:
1.虚函数的内存地址空间:
在C 中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。 对于子类,最开始的内存数据记录着父类对象的拷贝(包括父类虚函数表指针和成员变量)。 之后是子类自己的成员变量数据。
单继承,无虚函数重载:
单继承,有虚函数重载:
总结
- 如果一个类中有虚函数,那么就会建立一张虚函数表vtable,子类继承父类vtable,若,父类的vtable中私有(private)虚函数,则子类vtable中同样有该私有(private)虚函数的地址。注意这并不是直接继承了私有(private)虚函数
- 当子类重载父类虚函数时,修改vtable同名函数地址,改为指向子类的函数地址,若子类中有新的虚函数,在vtable尾部添加。
- vptr每个对象都会有一个,而vptable是每个类有一个,vptr指向vtable,一个类中就算有多个虚函数,也只有一个vptr;做多重继承的时候,继承了多个父类,就会有多个vptr
详情知识请移步:https://bbs.pediy.com/thread-224651.htm
2.Use-After-Free
Dangling pointer:
指向被释放的内存的指针。
成因:释放掉后没有将指针重置为NULL.简单来说就是因为分配的内存释放后,指针没有因为内存释放而变为NULL,而是继续指向已经释放的内存.
UAF:
对上面所说的指针进行利用,引用到自己想引用的函数上等等。
3.SLUB:
SLUB:系统内存分配机制。
对对象类型没有限制,两个对象只要大小差不多就可以重用同一块内存,而不在乎类型是否相同样的话,同一个笼子既可以放鸡,又可以放鸭。也就是说我们释放掉sock对象A以后马上再创建对象B,只要A和B大小相同(不在乎B的类型),那么B就极有可能重用A的内存。SLAB差不多,只不过要求类型也要相同。
既然B可以为任意对象类型,那我们当然希望选择一个用起来顺手的对象类型。至少要符合以下2个条件:
1.用户可以控制该对象的大小
2.用户空间可以对该对象写入数据
Pwnable-UAF详解:
源代码:
快速浏览一遍过后我们可以观察到主要的Human和Man、Woman三个类,Human是父类,其余是继承了的子类,并且两个子类都重写了父类中的introduce函数,我们还注意到了父类中的getshell私有函数,所以我们之后肯定会用到它。由前者的知识点我们可以明白,三个类中都有虚函数,所以每个类都有一个vtable表来存储虚函数,并且两个子类都继承了父类的vtable表,并且也有父类私有虚函数的getshell虚函数。
所以我们可以想到利用子类的构造函数,来跟随找出vtable,再利用getshell虚函数地址来继续。
main函数中after那一段的作用是分配一段地址空间,我们可以利用已经被free的内存重新allocate一个可控的地址空间。
所以我们的思路是:
1.找到getshell虚表的地址
2.找到vtable的地址
3.重写覆盖vptr指针指向地址
4.free后再allocate得到可控地址
1.getshell虚表地址
由于我是本地自己重新把平台上的cpp文件编译了一遍,所以地址和平台上环境地址会不一样。(后来我才明白是因为自己编译cpp文件的时候所使用的参数不同的原因,比如gcc -g uaf.cpp -o uaf和不加-g是有区别的) 以上可以看见getshell虚函数在vtable中的地址为0x4012ea,也可以在gdb中调试,来查看getshell地址。
2.vtable地址
找到man的构造函数
在0x401084处下断点,用gdb调试
p /x $ebx的作用是打印出实例化man对象的地址,而后查看man对象的内存地址空间,因为虚表指针在首部,所以我们找到了虚表的地址是0x401668
3.重写覆盖
我们首先得需要找到虚表指针引用introduce函数时候的偏移量:
我们可以大致推测出v12和v13是同一个vptr指针,偏移 8后刚好是getshell地址 8后的introduce函数地址,所以我们可以开始利用,把vtable表的地址-8,即把vptr指针指向的地址-8时,就可以在程序运行use段时引用introduce函数的时候实则引用的是getshell函数。
4.allocate
可以看到原来man对象分配到的空间是0x30,即48字节,所以当我们再次分配的时候也要分配48字节,保证自己拿到的是原先被free掉的地址空间。
利用:
由于先free掉的是m,所以当我们分配第一次的时候得到的是w所指向的空间,所以我们需要分配两次得到m所指向的空间再来利用。 因为这题是从文件中读出内容覆盖,所以我们可以使用python -c来写入转变成不可见字符(由于我试过直接在文档里面写十六进制的地址没法被读取,所以才明白要转变成不可见的字符)。
程序在case2中读取数据的填充到data空间的时候,开始的八字节就是vtable。之后是类的数据。(因为geshell表 8字节后就是introduce表,所以推测读取的地址为8字节一个段)
0x401668-0x8=0x401660 python -c ' print "x60x16x40x00x00x00x00x00" ' > /tmp/exp ./uaf 48 /tmp/exp