• PWN——uaf漏洞学习


    PWN——uaf漏洞

    1.uaf漏洞原理

      在C语言中,我们通过malloc族函数进行堆块的分配,用free()函数进行堆块的释放。在释放堆块的过程中,如果没有将释放的堆块置空,这时候,就有可能出现use after free的情况。这里我写了一个demo

    #include<stdio.h>
    #include
    <stdlib.h> typedef struct demo { char *s; void(*func)(char *); }DEMO; void eval(char command[]) { system(command); } void echo(char content[]) { printf("%s",content); } int main() { DEMO*p1; DEMO*p2; p1=(DEMO*)malloc(sizeof(struct demo)); p1->s="I will tell you what's uaf."; printf("%s ",p1->s); printf("p1 malloc address:%p ",p1); p1->func=echo; p1->func("use after free! "); free(p1); p1->s="~Heihei~"; printf("%s ",p1->s); p2=(DEMO*)malloc(sizeof(struct demo)); p2->func=eval; // p1->func=eval; printf("p2 malloc address:%p ",p2); p2->func("whoami"); p1=NULL; printf("Now,I can't use address of p1."); p2->func("whoami"); return 0; }

    运行的结果如下图所示  

      可以看出,在free()堆块之后,没有将堆块置空,堆块处于悬空状态,导致被free掉的堆块依然可以被使用。同时,如果我们申请相同大小的堆块的话,由于ptmalloc的堆管理机制,重新分配的堆块的位置和我们释放的堆块地址是一样的。但是,在将p1置空之后,再次使用堆块的时候,就会报出段错误。

      其实,一般的uaf漏洞利用,和我们上图的demo也是类似的,都是定义一个结构体,结构体中有一个函数指针,然后free,将chunk添加进fastbin,再次分配相同大小的内存块,这时候分配的就是刚才free掉悬空的堆块,然后改写函数指针,劫持数据流,配合不同的函数指针,可以实现任意地址读,任意地址写,以及命令执行。

    注:这里第二次分配内存的时候,我们一般分配相同数据类型。比如上面的例子中,第一个释放的堆块p1的是DEMO类型的结构体,第二次分配的时候,我们们分配的p2也是一个DEMO类型的结构体,这种情况下,p2一定用的是之前p1的内存。但是在有些情况下,如果我们分配的数据类型不同,但是前后分配大小相同,也可能造成堆块的再次利用。

    2.例题

    这道例题,是pwnable.kr上的uaf这道题目,源码如下。

    #include <fcntl.h>
    #include <iostream> 
    #include <cstring>
    #include <cstdlib>
    #include <unistd.h>
    using namespace std;
    
    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
    2. after
    3. free
    ";
            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;    
    }

      之前没看过C++,花了点时间把C++的类与面向对象的一些知识学习了一下。

    ·   先看主函数,主函数中实现了分配堆块,释放堆块,以及从读入数据的功能。然后再看定义的几个类,Human是定义的一个基类,Man和Woman是定义的派生类,Human中public和protected定义的函数和变量可以在后面被子类继承和访问。但是定义的私有的give_shell虚函数,我们是无法在外部调用的,也无法通过子类来访问。只能通过Human类中定义的函数来调用。

    我们这里需要一些预备知识:C++的类中有序函数的时候,会生成一个虚表vtable,编译器会生成一个vptr指针指向vtable(不管基类中有多少虚函数,只有一个_vptr指针指向vtable;多态继承时,继承了多个父类,相应就会又多个vtable)。对于共有成员变量来说,虚函数可以被子类同名函数重新调用,子类中有同名函数的时候,子类的vatble中指向基类虚函数的函数指针变为指向子类同名函数。对于私有成员来说,他的函数指针会保留在子类的vtable中(但是子类不是继承私有成员)。所以对于上面的源码来讲,Man类和Woman类的vtable中,introduce函数指针指不同,但是give_shell函数指针是相同的。我们可以再深入思考一下:vtable保留在哪里?通过IDA我们可以看出,vtable是保留在rodata段的。vtable保留着类的虚函数指针,它应该在编译前就完成,相应的它肯定不在代码段,bss段和data段分别保留的是未初始化和初始化后的局部变量,全局变量以及静态变量,所以vtable也不会在bss段或者data段。这样看来,vtable和字符串常量一样,保留在全局数据段是合理的。

     

    这几张图片是从sakura师傅的博客取下来的,这里贴出来,大家可以看一下

           

      对于这道题而言,类比C程序中利用的步骤,我们应改首先创建堆块,然后通过case3释放堆块,但使堆指针悬空,最后通过case2,读入文件内容,覆写类的函数指针,劫持数据流。

      打开IDA,main函数的伪代码中可以得知,Man和Woman对象分配的堆的大小为0x18个字节。

      我们首先通过调试,找到相应的虚表函数,要找到虚表函数,就要先找到Man的构造函数。

    找到Man的构造函数之后进入,会发现调用Human的构造函数(因为Man作为子类要继承父类),所以这时候继续单步,可以在寄存器窗口看见调用give_shell函数的地址

     找vtable的话,进入IDA,到rodata段。

    Man类vtable表,give_shell的函数指针的地址为0x401570。现在要具体关注一下堆块内部的内存布局,以便在后面有效地覆盖函数指针。

    0x40117a是give_shell函数地址,0x4012d2是introduce函数地址。

    按照C++实例化类的内存分配的规则,类中有虚函数的时候,分配的对象的内存数据中,前8个字节是_vptr指针,这个指针指向vtable表,以后是成员变量的内存数据(这里应该只是成员变量的内存数据,不包括成员函数,所有函数都是定义在text段的)。

    vptr指向的地址,是vtable表的地址,vtable表上存储的是虚函数的地址。

    case2的时候填充的数据就应该是把前8个字节修改

    case1调用intreduce函数的代码如下

    v13和v14应该分别是Man对象和Woman对象的vptr指针,前面看到过,give_shell函数地址在0x401570地址处,我们向文件中写入0x401570-8的地址。

    这里要注意的是,再次分配地址的时候,由于最后delete的是Woman对象的地址,第一次写入的是之前Woman的地址,第二次才是写入到Man对象地址的vtable表上。

     

    终于是写完了这篇博客。。。

    C++和堆的很多知识我绝对理解的不是很到位,是要不断学习的,但是这道题是看着博客,跌跌撞撞做出来了。写出来是为了加深印象,如果哪里有说的不对的地方,还希望路过的师傅们能过及时指出来,谢过师傅们了。

  • 相关阅读:
    element-ui 多张图上传
    js json生取key,value 值
    iview DatePicker 回显验证报错
    iview的Select控制value为数字类型时表单验证无法通过
    iview 自定义树形
    tree 树形递归修改 key
    根据月份选择 生成这个月的每一天
    微信小程序超出隐藏省略号和自动换行
    uni-app picker select 取想要的值
    element-ui 表格fixed 样式修改
  • 原文地址:https://www.cnblogs.com/L0g4n-blog/p/12887869.html
Copyright © 2020-2023  润新知