前言
这道题目反应了uaf的基本原理,pwn入门必做的题目,如果这一块了解的不够透彻,直接去打现在涉及各种奇技淫巧的pwn,肯定会被绕晕掉。所以为了照顾萌新(其实是我自己菜)把这道题目单独拿出来写一下。这是一道uaf的题目,把二进制文件拉到本地来研究
在分析前,先简单说一下c++的虚函数和uaf的前置知识
在c++中,如果类中有虚函数,那么这个类就会有一个虚函数表的指针vfptr,而子类会继承。
uaf的原理:在释放内存后未将指向原内存的指针置为null,use after free的意思就是在释放以后进行use。
举个简单的例子:
原来的p指针指向一个结构体,当结构体没释放之后没有将p置为null,如果我们重新分配原结构体大小的空间,则指针p可以继续使用。但是再次使用时,因为p指针指向的内存包含的数据不是原来的数据了,此时的引用对于正常程序来说是有风险的,不过对于黑客来说,反而方便其进行控制。比如可以进行这些攻击:
任意地址读:puts(p->name)—————>puts(char*(addr2)) 任意地址写:strcpy(p->name,data);——>strcpy((char *)(addr2),data) 控制流劫持:p->func()———————>call addr3
这次的uaf题目基本相当于任意地址写。先看源码
uaf@pwnable:~$ cat uaf.cpp #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; }
human父类,man和woman两个子类继承自human父类
human父类存在虚函数,会创建有虚表指针指向的一个虚表。Man和woman子类会继承,子类的虚表中会继承父类的所有项(并且当子类存在同名虚函数时,会修改vtable表项,指向自己的函数的地址。如果父类有私有函数,但是这个私有函数是虚函数,那么子类的vtable中同样会有这个函数的表项。每个虚表只有一个vptr,就算有多个虚函数也一样。但是当多重继承的时候,就会有多个vptr。)
再看main
可以知道程序运行后供我们输入
1会分配内存,2会写内存,3会释放内存。
根据uaf的原理,程序运行后会自动分配内存,我们需要先释放内存,然后将exp写入data,这样当输入1时就会被我们劫持了。
这里需要注意几点:
1. 输入2,也就是case2时,需要确定要分配多少内存给我们写,我们知道原程序申请了int age为4字节,string name为16字节,加上一个虚函数指针4字节,共24字节。
2. 这里程序自动申请分配给了man,woman,以24字节为单位。而case3是先delete m,在delete w,所以我们这里需要分配两个24字节的内存,即按两次2才能得到m所指向的空间
3. case2需要制定24字节的长度,以及要从哪个文件中读内容来覆盖原先分配的空间,这个文件可以随意指定,关键是文件的内容是什么,这就是我们接下来要研究的地方
注意到在我们use after free的use步骤,也就是输入case1的时候,按照程序逻辑而言执行的是introduce
其实这里调用的是父类human::introduce,而我们想要的是giveshell。
由前面虚函数的知识我们可以知道,这两个虚函数是在一张表上的,那么我们只要在调用human::introduce之前将其地址改为giveshell的,这样在输入case1的时候就可以拿到shell了。
虚表里面一共就两项,第一项是giveshell,第二项是introduce,关键就是找到两者间的偏移,以及虚表指针
可以看到introduce和giveshell差了8
上图是case1的汇编
可以看到执行了add rax,8后会执行introduce,那么我们给rax的值-8,这样执行了该指令后就会执行giveshell
虚表原地址我们知道是0x401570
所以我们现在要把它覆盖成0x401570-8=0x401568
也就是说我们case2,在分配24字节,写0x401568来覆盖原内容,根据内存布局,其实就是相当于覆盖了虚表指针vfptr
所以pwn的步骤就很简单了,如图所示
实验推荐--CTF-PWN练习之函数指针改写(学习使用objdump来查找二进制程序中函数的地址信息,并通过修改函数指针变量的值为指定函数的地址来改写程序执行流程。)