先贴上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;
}
又是学新东西的一天
这道题主要会用到的知识点:
1.c++多态性中,通过虚函数表实现的动态多态(或者叫运行时多态)
2.uaf(use after free)漏洞的基本原理
3.c++对象在内存中的布局(不多,这题只需要知道vptr在起始处)
1. 虚函数表和动态多态
主要读了https://leehao.me/C-%E8%99%9A%E5%87%BD%E6%95%B0%E8%A1%A8%E5%89%96%E6%9E%90/这篇博客
按照自己的理解简单复述如下:
不同于通过模板和函数重载实现的、在编译时完成的静态多态,动态多态主要指的是,对于实现了同名虚函数且存在继承关系的几个类,在程序执行时根据对象指针所指的不同对象,正确地调用到目标虚函数的过程。
实现这个过程的核心在于虚函数表。每个声明了虚函数的类都会拥有一个虚函数表,虚函数表是一个指针数组,指针指向当前类所实现的虚函数的地址。而每一个由含虚函数的类实例化而来的对象,实际上除了本身的数据成员外,还会包含一个指针*__vptr,称为虚表指针,该指针指向自己所属的类的虚函数表。
于是,当通过对象指针调用虚函数时,实际上先根据对象指针找到对象所在的地址,然后读取对象中虚表指针的内容,找到对应的虚函数表,再根据虚函数表中所维护的指针找到虚函数真正的入口地址。
2.UAF的基本原理
UAF,use after free,属于堆漏洞,
由于malloc等内存申请函数机制上的问题,在free掉一片由malloc函数申请的内存后,在一定条件下并不会把内存块立刻释放回内存,而是标记成空闲状态,以期下次申请内存时使用。如果在free一个指针后,立刻再次malloc申请内存,分配到的内存块可能会和上次相同。
用一个简单的场景来直观描述uaf的一种利用效果:
1.存在一个指针p1,指向堆中的一片内存。
2.函数A会根据p1指向的内容来决定执行的是人畜无害的操作还是高危操作,
并且正常执行流程下函数A只会进行人畜无害的操作
3.函数B会声明一个指针p2,用malloc申请内存,相应的地址保存在p2里;然后对p2所在的内存进行读写
4.函数C会把p1所指的内存free掉,但不会把指针p1本身置为null
5.于是问题出现了,
先调用函数C,于是p1所指的内存被释放了;
接着调用函数B分配到的内存实际上就是此前p1所指的地方,此时函数B通过指针p2对该片内存进行写入;
最后调用函数A,虽然此时p1所指的内存已经经历了释放、申请的过程,并且被函数B修改了内容,但是函数A对此一无所知,依然根据p1所指的内容进行判断,从而破坏正常的执行流程,转入高危操作
3.c++对象在内存中的存储方式
主要读了这篇博客https://www.cnblogs.com/QG-whz/p/4909359.html
还有这篇https://zhuanlan.zhihu.com/p/94152624
摘几个关键点下来:
1.虚函数指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。当vprt位于对象内存最前面时,对象的地址即为虚函数指针地址。
2.虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成。
3.简单地说,当出现多继承时,对象也会含有多个vptr,并且内存布局上总体按照继承的先后顺序进行摆放,出现在最后的是当前类自己声明的成员。
4.看看题吧
先看源代码,
基类human里面实现了两个虚函数give_shell和introduce,
两个protected的数据成员name和age,
man和woman类都继承了human类,
并且重写了introduce方法,
为单继承,
那么地址的起始处就应该是vptr,指向的虚函数表有两项,
在main函数中,
一开始就分别new了man和woman的对象,分别用指针m,w指向它们所在的内存,
然后开始while(1)的死循环,
通过cin输入数字来选择switch到不同的分支,
case1会依次调用man和woman对象的introduce方法,
case2会用new申请一片内存,大小取决于在命令行中执行程序时的第一个参数;
然后把调用时的第二个参数当作文件名,把文件中读取到的内容写到刚刚申请的内存中,
case3会依次delete掉m和w指针指向的内存;
那么思路就有了,
先执行case3把内存释放掉,
然后执行case2申请一片内存将会得到刚刚释放掉的内存,于是就可以控制vptr指针,
设法让它调用m->introduce时实际上调用到give_shell,
最后执行case1,
scp -P端口号 用户名@主机名:文件目录 本地目录
下载文件
扔进ida里看看,
main函数的前面这一块,通过24行和31行的new,能够确定man和woman的对象都是24个字节
双击25行的man::man,跳转到构造函数man::man
看到构造函数先是调用了父类的构造函数human::human,
然后把一个奇怪的地址401570写到了a1所指的地址,
最后似乎对运算符=做了一些事情
跳到401570,
发现是.rodata段
ida的注释已经写的明明白白了,
vtable for man,
那么这个地方应该就是man类的虚函数表,
下面还有vtable for human,
往上翻也有vtable for woman
从401570开始的八个字节指向human::give_shell
从401578开始的八个字节指向man::introduce
再回去看反编译的main函数,
从61行往下的部分会调用析构函数之后delete,应该对应源代码中case3的部分;
41-52行这个代码块则是源代码中switch语句外面的一块和case2的缝合体;
那么56-59的这两句应该就对应case1中分别调用man.introduce()和woman.introduce()的那两行
先看57行,
先取了指针v13所指的值,
加上8,
把结果当成函数指针来调用函数,
那么v13是个啥,
往前找找
看到v13来自v3,
而v3实际上就是指向new出来的man对象的指针,
再看回57行,
把v13当作qword即8字节长的指针用,
取到的内容实际上是man的虚函数表的地址401570,
加上8得到401578,
而从401578开始的8个字节存储了man::introduce的入口地址,
于是就可以确定57行这里对应源代码中case1中的m->introduce()
另外也明确了利用方法,
通过case2控制最开始的八个字节为401568,
这样执行到57行处就会取到401568,
加上8得到401570,
然后就会调用到human::give_shell
把地址401568表示成小端法下的十六进制,
写到tmp目录下的某个文件里,
然后开始执行uaf,参数为24(即man或woman对象的大小,保证能申请到同一片内存),以及刚刚的文件路径,
先执行case3,把两个对象指针都free掉,
然后执行第一次case2,申请到原先woman对象所在的内存;
再执行第二次case2,申请到原先man对象所在的内存;
最后进入case1,在调用man::introduce的时候会转而调用human::give_shell,
从而getshell