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\n",p1->s); printf("p1 malloc address:%p\n",p1); p1->func=echo; p1->func("use after free!\n"); free(p1); p1->s="~Heihei~"; printf("%s\n",p1->s); p2=(DEMO*)malloc(sizeof(struct demo)); p2->func=eval; // p1->func=eval; printf("p2 malloc address:%p\n",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\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; }
之前没看过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++和堆的很多知识我绝对理解的不是很到位,是要不断学习的,但是这道题是看着博客,跌跌撞撞做出来了。写出来是为了加深印象,如果哪里有说的不对的地方,还希望路过的师傅们能过及时指出来,谢过师傅们了。