UAF(Use After Free)
释放后重用,其实是一种指针未置空造成的漏洞。
首先介绍一下迷途指针的概念
在计算机编程领域中,迷途指针,或称悬空指针、野指针,指的是不指向任何合法的对象的指针。
当所指向的对象被释放或者收回,但是对该指针没有作任何的修改,以至于该指针仍旧指向已经回收的内存地址,此情况下该指针便称迷途指针。若操作系统将这部分已经释放的内存重新分配给另外一个进程,而原来的程序重新引用现在的迷途指针,则将产生无法预料的后果。因为此时迷途指针所指向的内存现在包含的已经完全是不同的数据。通常来说,若原来的程序继续往迷途指针所指向的内存地址写入数据,这些和原来程序不相关的数据将被损坏,进而导致不可预料的程序错误。这种类型的程序错误,不容易找到问题的原因,通常会导致存储器区块错误(Linux系统中)和一般保护错误(Windows系统中)。如果操作系统的内存分配器将已经被覆盖的数据区域再分配,就可能会影响系统的稳定性。
以上内容引用于维基百科。
然后看一下源码
#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;
}
首先这里面有三个类,一个基类Human
,两个派生类Man
和Woman
,他们继承了Human类
。Human类
中有两个虚函数give_shell()
和introduce()
,在Man类
和Woman类
中重新定义了虚函数introduce ()
。
下面介绍一下虚函数的工作原理:
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl
)。虚函数表中存储了为类对象进行声明的虚函数地址。
例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则需要在对象中添加一个地址成员,只是表的大小不同而已。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hFwoZ3A0-1603176838810)(https://i.loli.net/2019/06/22/5d0e1401a50cb64018.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YKSY1Bem-1603176838812)(https://i.loli.net/2019/06/22/5d0e14061eb0e69979.png)]
调用虚函数时,程序将查看存储在对象中的vtbl
地址,然后转向相应的虚函数地址表。如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。
然后看一下main函数
,main函数首先new
了两个对象,然后通过指针指向他们。然后接受一个操作码,根据操作码进行操作,各个操作的含义如下
- 调用
- 分配内存
- 释放内存
本题的大概思路就是通过3先释放内存,因为程序释放内存后没有将指针置空,所以指针仍然指向原来的内存。如果释放内存后又去申请相同大小的内存,操作系统会将刚刚释放掉的内存再次分配,我们对其进行精心构造后,再调用指针就可以让填充的数据使eip发生跳转。
可以看到给对象分配了24字节的空间,下面Man::Man()
是Man的构造函数,跟进去看看。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OVh7ZXn0-1603176838816)(https://i.loli.net/2019/06/22/5d0e20bef296862012.png)]
*a1
为申请的内存空间的指针,可以看到将a1
所指的前8个字节
的空间赋值为off_401570
,跟进去看看,发现这是指向虚函数表的指针。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yHenpJZk-1603176838817)(https://i.loli.net/2019/06/22/5d0e217c56e4a93227.png)]
可以看到Man的虚函数表,记录Human::give_shell()
的地址和记录Man::introduce()
的地址相差8个字节,我们每次调用Man::introduce()
都会用off_401570的值
+8
去寻找introduce()
函数,我们只要将off_401570的值-8
写入到off_401570
的位置,下次调用Man::introduce()
的时候就会调用Human::give_shell()
了。
至于怎么写呢,看源码中case2
,将argv[1]
转换为整数,去申请argv[1]
大小的内存空间,从argv[2]
指定的文件中读取argv[1]
大小的数据到data
中。通过上面的分析我们知道argv[1]应该为24,然后我们将off_401570的值-8
也就是0x401568
写入到一个文件,将文件目录作为arg[2]
。
然后3–>2–>2–>1,就能获取shell了,为什么是两次2呢?这里需要注意下释放的顺序是先释放的m,后释放的w,因此在分配内存的时候优先分配到后释放的w,因此需要先申请一次空间,将w分配出去,再开一次,就能分配到m了。通过一次UAF只能修改w指向的内存空间,而在引用的时候却先引用了m指向的内存空间。这时m指向的内存空间已经被释放,会造成段错误。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tmsSwkPa-1603176838818)(https://i.loli.net/2019/06/22/5d0e2abe73aa258919.png)]
最后需要注意的是这是个64位的程序,所以不要写成32位的地址格式。
如果觉得我写的不是很清楚,可以参考一下逢魔安全实验室的WP