这道题目是非常典型的user-after-free漏洞
同时也涉及到了关于fastbin的一些知识
首先我们来了解一下在linux的堆中,glib是如何来分配小于64kb的内存的。
在linux的堆中,内存是以chunk的形式进行分配的
而每个chunk的结构是这样表示的

其中chunk head中,prev_size是,如果前一个chunk处于空闲状态,则存放它的大小,而size的低三位bit为flag,其中最低位指示前一个chunk是否处于空闲状态。
如果本chunk正在被使用,那么后面的memory部分都是数据了。
如果本chunk处于空闲状态,那么memory的前8字节用于存放fd和bk指针,分别指向后一个未使用的chunk和前一个未使用的chunk
由于malloc需要负责回收已经free掉的chunk,它就需要进行chunk的合并,这里就就涉及到unlink的操作了,也是会引发漏洞的
当然,我们这次的主题不是它。
为了快速的维护较小内存块的分配,malloc会把这一系列较小的chunk通过他们的fd(指向后一个未使用的chunk)指针组成一个单链表,因为这里bk并没有使用,所以最后进入fastbin的chunk反而会被最先分配出去
这个单链表就被称为fastbin,它里面的内存块可以是8kb,16kb,24kb...80kb
当我们试图使用malloc去分配一块较小的内存(<=64 Bytes)时,它会首先检查对应大小的fastbin中是否包含未被使用的chunk,如果存在则直接将其从fastbin中移除并返回
其实是有一点像stack的,当然这个实际上就是一个单链表了
了解了fastbin的工作方式后,我们就来看看这道题目
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; } }; 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; } }; 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; }很明显的,由于这些类的size都较小,通过了fastbin来进行分配对象的内存
这里如果我们先选择了3,去free掉这两个对象,而在通过1来调用的话,就形成了uaf的利用
但是这里我们想一想,怎样才能通过子类调用到父类的指针呢?
在C++里,对于存放类的虚表vtable,子类会继承父类的vtable,而如果需要调用虚函数
则这些调用语句是根据vtable的地址来进行相对偏移的。
然后子类根据自己是否重写或新增了某个方法,修改虚函数的指针地址,或者是新增一条虚函数指针
但是,父类的虚函数指针,即使子类没有使用,也仍然存在于它们的vtable里
而在对象内存的开头处就存放着vtable的地址
所以这下思路就很明显了,我们需要做这样的事情。
1.两个对象在被free后,以先man后women的顺序先后加入fastbin
2.在申请同样大小的内存(24)时,会从fastbin来进行内存的分配
所以,data = new char[len]语句得到了之前woman的内存所在3.在read(open(argv2, O_RDONLY), data, len) 产生了堆溢出
4.由于case 1时,仍然可以使用被free掉的对象,产生了uaf漏洞
因此,通过这个分析,最关键的部分就在于我们需要通过这个堆溢出去覆盖掉vtable的地址
首先使用objdump -D uaf来分析这个文件

可以看到,子类man的虚表地址正是0x401570
发现这地址存放着第一条虚函数的指针,很明显的,它就是give_shell()原因如下
vtable指向的是虚表的开始指针。其实vtable是虚表的地址,虚表的第一项是第一个虚函数的指针。
由于不可能通过覆盖对象的内存直接去覆盖虚函数指针
因此,我们需要通过覆盖man的vtable,并让它减少一定量,使调用man的introduce方法时
调用到我们的give_shell()方法
这里每个指针的长度是8,所以我们要让man的vtable-8,来实现我们的目的
由于第一次after分配到的是woman的内存,因此我们需要两次after来得到man的内存
因此构造如下的payload
python -c "print '\x68\x15\x40\x00\x00\x00\x00\x00'" >> /tmp/mathias
./uaf 24 /tmp/mathias
然后选择 free-after-after-use

得到了flag yay_f1ag_aft3r_pwning