pwnable.kr Toddler's Bottle_pt8 UAF - Blog of Mathias
Blog of Mathias Web Securtiy&Deep Learning
pwnable.kr Toddler's Bottle_pt8 UAF
发表于: | 分类: UAF | 评论:0 | 阅读:1,001

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

1.jpg

其中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来分析这个文件

2.jpg

可以看到,子类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

4.jpg

得到了flag yay_f1ag_aft3r_pwning

还不快抢沙发

添加新评论