SEEDLab-堆与UAF漏洞
实验介绍
本实验旨在深入理解UAF(Use-After-Free)漏洞的成因、利用原理及其在实际程序中的表现形式。通过分析两个典型场景——普通结构体中的函数指针UAF和C++对象中虚函数表(vtable)相关的UAF,掌握堆内存分配与释放的基本机制,熟悉攻击者如何通过精心构造输入覆盖已释放内存中的关键数据(如函数指针或虚表指针),从而劫持程序控制流并执行任意代码。同时,借助GDB等调试工具,观察内存布局变化,验证漏洞触发过程,提升对内存安全问题的分析与防护意识。
实验内容
任务一 Use After Free漏洞
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <stdio.h> typedef struct s { int id; char name[20 ]; void (*clean)(void *); }VULNSTRUCT;void *cleanMemory (void *mem) { free (mem); }int main (int argc, char *argv[]) { void *ptr1; VULNSTRUCT *vuln=malloc (256 ); fflush(stdin ); printf ("Enter id num: " ); scanf ("%d" , &vuln->id); printf ("Enter your name: " ); scanf ("%s" , vuln->name); vuln->clean=cleanMemory; if (vuln->id>100 ){ vuln->clean(vuln); } ptr1=malloc (256 ); strcpy (ptr1, argv[1 ]); free (ptr1); vuln->clean(vuln); return 0 ; }
首先,观察程序代码,发现总体上调用了两次vuln结构体中的clean函数,clean函数执行free操作,因此程序中存在可利用的UAF漏洞,在if条件中调用clean视为free,在此之后,又为ptr1分配相同大小的内存块,可以猜想这次分配得到的块与被释放内存块相同。下面通过GDB调试验证这一点。
如图1,反汇编程序,在调用malloc函数之后,返回值(分配内存的地址)存放在eax寄存器中,在此设置断点。如图二,运行后查看eax中的内容即可获取申请内存的位置。
如图3,查看分配内存块的内容,可以看到写入到ptr1所指区域的“AAAA”确实写入到了被释放的vuln区域中,可以确定是存在UAF漏洞。如图5,编写程序输入,,将shellcode放到ptr1的后半部分,将clean函数指针覆盖为shellcode的地址,考虑到误差,采用slide方式,使得程序可以正确运行到shellcode。
如图6,运行漏洞程序,攻击成功。
任务二 UAF和VPTR
创建badfile,你可以用下面的框架来创建。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include <fcntl.h> #include <iostream> #include <cstring> #include <cstdlib> #include <unistd.h> using namespace std ; class Human { private: virtual void give_shell () { setuid(geteuid()); 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 ; }
观察程序代码,这是一个菜单程序,其中选项可以选择释放内存、申请内存或者调用函数,存在UAF漏洞,同时类中存在虚函数,即可利用虚函数表进行攻击。
如图7所示,为了观察内存中堆的情况,在程序中调用函数的部分和read读取文件的部分以及释放内存的部分设置断点。
如图8,运行程序到free之前可以查看分配区域的内容,先查看区域地址,再查看堆中的内容,可以看到w和m对象的内容,其中就有虚函数表地址。
如图9,先选择free后再选择after申请内存,输入AAAA,可以看到虽然申请的内存在之前释放的区域,但并非是m的区域而是w的区域,但由于在use选项中先调用的是m中的函数,因此这么覆盖无法攻击成功。
如图9,由于先释放m再释放w,所以会先占用w,再申请一次就会占用m的空间,用这种方式就可以覆盖m的虚函数表地址。
如图11和图12,使用GDB查看程序中各个函数的地址,再查看虚函数表的内容,可以确定giveshell地址在函数表中的位置,下面只要将虚函数表指针覆盖成giveshell的位置,再选择use选项即可运行giveshell,攻击成功。
如图13,编写程序输入,将虚函数表地址覆盖为giveshell的地址的地址。如图14,攻击成功。
任务三 问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <cstring> #include <unistd.h> class Number { public: Number(int x) : number(x) { } void setAnnotation (char *a) { memcpy (annotation, a, strlen (a)); } virtual int operator+(Number &r){ return number + r.number; } private: char annotation[100 ]; int number; };int main (int argc, char **argv) { if (argc < 2 ) _exit(1 ); Number *x = new Number(5 ); Number *y = new Number(6 ); Number &five = *x, &six = *y; five.setAnnotation(argv[1 ]); return six + five; }
首先观察程序代码,发现类中存在虚函数调用,同时存在无限制的缓冲区复制,可以造成溢出,也有溢出的条件。可以将shellcode放在five的annotation+4的位置,将shellcode的地址放在annotation,再将six的虚函数指针覆盖为annotaion。
首先对程序进行调试,首先反编译main(),可以发现setAnnotation方法的调用,在调用该方法时,寄存器eax中存放的应当是对象five的地址。
查看对象five中的内容,可以看到两个一模一样的地址值,可以确定这就是虚函数地址只需将对象six的虚函数指针覆盖为shellcode地址的地址即可。因此构造payload,在five的annotation中写入shellcode的地址,并将six的虚函数指针覆盖为five的annotation。如图12所示,攻击成功。