欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]ctf pwn中的UAF及pwnable.kr UAF writeup

[原创]ctf pwn中的UAF及pwnable.kr UAF writeup

考察点

  • 虚函数的内存地址空间
  • UAF

前置知识1:虚函数的内存地址空间

在C++中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。
对于子类,最开始的内存数据记录着父类对象的拷贝(包括父类虚函数表指针和成员变量)。 之后是子类自己的成员变量数据。

源码

class Base 

{ 

    public: 

        virtual void f() { cout << "Base::f" << endl; } 

        virtual void g() { cout << "Base::g" << endl; } 

        virtual void h() { cout << "Base::h" << endl; } 

    int base; 

    protected: 

    private: 

}; 

//子类1,无虚函数重载 

class Child1 : public Base 

{ 

    public: 

        virtual void f1() { cout << "Child1::f1" << endl; } 

        virtual void g1() { cout << "Child1::g1" << endl; } 

        virtual void h1() { cout << "Child1::h1" << endl; } 

    int child1; 

    protected: 

    private: 

}; 

//子类2,有1个虚函数重载 

class Child2 : public Base 

{ 

    public: 

        virtual void f() { cout << "Child2::f" << endl; } 

        virtual void g2() { cout << "Child2::g2" << endl; } 

        virtual void h2() { cout << "Child2::h2" << endl; } 

    int child2; 

    protected: 

    private: 

};

单一继承,无虚函数重载

单一继承,重载了虚函数

多重继承

总结

  • 如果一个类中有虚函数,那么就会建立一张虚函数表vtable,子类继承父类vtable,若,父类的vtable中私有(private)虚函数,则子类vtable中同样有该私有(private)虚函数的地址。注意这并不是直接继承了私有(private)虚函数
  • 当子类重载父类虚函数时,修改vtable同名函数地址,改为指向子类的函数地址,若子类中有新的虚函数,在vtable尾部添加。
  • vptr每个对象都会有一个,而vptable是每个类有一个,vptr指向vtable,一个类中就算有多个虚函数,也只有一个vptr;做多重继承的时候,继承了多个父类,就会有多个vptr

前置知识2:Use-After-Free

Dangling pointer

Dangling pointer
Dangling pointer即指向被释放的内存的指针,通常是由于释放内存后,未将指针置为NULL。

UAF原理

对Dangling pointer所指向内存进行use,如指针解引用等。

利用思路

将Dangling pointer所指向的内存重新分配回来,且尽可能使该内存中的内容可控(如重新分配为字符串)

举例

typedef struct

{

    int id;

    char *name;

    int (*func)() //函数指针,可以理解为类里面的方法

};

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

  • 任意地址读:puts(p->name)--------------->puts(char*(addr2))
  • 任意地址写:strcpy(p->name,data);------>strcpy((char *)(addr2),data)
  • 控制流劫持:p->func()--------------------->call addr3

题目链接

http://pwnable.kr/play.php
https://github.com/eternalsakura/ctf_pwn/blob/master/pwnable.kr/uaf
https://github.com/eternalsakura/ctf_pwn/blob/master/pwnable.kr/uaf.cpp

源码

#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\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;

}

分析

先checksec
因为这是一道开源pwn,给了我们源码,而且代码也不复杂,没有什么逆向的必要,为了方便理解,我就直接从源码进行分析。

类的继承和虚表

可以看出Man和Woman都是继承了Human类,并且可以看出只要我们将控制流劫持到Human类的私有虚函数give_shell,就能getshell了。
Man和Woman都继承了Human类的vtable,可以通过调试,跟随子类的构造函数,找到vtable。

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;

        }

};

UAF

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;

                }

        }

可以看出程序给了我们3个选项

  • use 使用指针指向的函数
  • after 分配一段地址空间,我们可以用其将已经被free的内存,重新allocate
  • free 将指针指向的内存释放

组合起来就是UAF。

利用思路

  • 调试找到虚表中give_shell函数地址。
  • free后再allocate,得到一个可控的地址空间.
  • 为了在use,即m->introduce()时,将本来执行的introduce函数变成执行give_shell函数,在allocate的同时,改写虚表指针
  • 劫持控制流,执行give_shell

漏洞调试和利用

找到Man的构造函数,从而找到虚函数表

覆盖虚表指针


    可以看出在执行m->introduce()的时候,调用call [vptr+8]。
    为了执行give_shell,我们覆盖虚表指针,让它前移8个字节,这样call [vptr+8]的时候就调用give_shell了。
  • allocate
    从上图可以看出,原本Man对象分配的堆空间是0x18,即24字节,所以我们在再次分配的时候,也要分配24字节,保证自己拿到的是原先被free掉的地址空间。
Human* m = new Man("Jack", 25);

Human* w = new Woman("Jill", 21);

...

delete m;

delete w;

因为先free m再free w,所以为了再次拿到m所指向的空间,我们需要分配两次,第一次得到w所指向的空间,第二次才再次得到m所指向的空间

len = atoi(argv[1]);

data = new char[len];

read(open(argv[2], O_RDONLY), data, len);

在此题中,是通过从文件中读出内容覆盖原先的内容的,等同于之前写的strcpy(p->name,data),读取的长度是命令行的argv[1],打开的文件是argv[2]

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功