freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

听说你还不会UAF?
2020-05-26 14:29:56
所属地 湖南省

作者:萌新 合天智汇

前言:

这道题目反应了uaf的基本原理,pwn入门必做的题目,如果这一块了解的不够透彻,直接去打现在涉及各种奇技淫巧的pwn,肯定会被绕晕掉。所以为了照顾萌新(其实是我自己菜)把这道题目单独拿出来写一下。

这是一道uaf的题目,把二进制文件拉到本地来研究

v2-48dd0ac8d09498869c25ccc3cc8aeda3_720w

在分析前,先简单说一下c++的虚函数和uaf的前置知识

在c++中,如果类中有虚函数,那么这个类就会有一个虚函数表的指针vfptr,而子类会继承。

v2-d9fa9bd59b9a57469e64467c62be457e_720w

uaf的原理:

在释放内存后未将指向原内存的指针置为null,use after free的意思就是在释放以后进行use。

举个简单的例子:

v2-13571ee6421ceaf759c5dc1755e26cf5_720w

原来的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;
}

v2-62b2659f479d368e67922c970e656fd7_720w

human父类,man和woman两个子类继承自human父类

human父类存在虚函数,会创建有虚表指针指向的一个虚表。Man和woman子类会继承,子类的虚表中会继承父类的所有项(并且当子类存在同名虚函数时,会修改vtable表项,指向自己的函数的地址。如果父类有私有函数,但是这个私有函数是虚函数,那么子类的vtable中同样会有这个函数的表项。每个虚表只有一个vptr,就算有多个虚函数也一样。但是当多重继承的时候,就会有多个vptr。

再看main

v2-f211644ad08f7eb241455da0413e58d1_720w

可以知道程序运行后供我们输入

v2-f211644ad08f7eb241455da0413e58d1_720w

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,关键就是找到两者间的偏移,以及虚表指针

v2-2fab095118890ba57e6c45051010b108_720w

可以看到introduce和giveshell差了8

v2-a66b8a86fba7d22867fee93dde266423_720w

上图是case1的汇编

可以看到执行了add rax,8后会执行introduce,那么我们给rax的值-8,这样执行了该指令后就会执行giveshell

虚表原地址我们知道是0x401570

所以我们现在要把它覆盖成0x401570-8=0x401568

也就是说我们case2,在分配24字节,写0x401568来覆盖原内容,根据内存布局,其实就是相当于覆盖了虚表指针vfptr

所以pwn的步骤就很简单了,如图所示

v2-b28dacb3d0a067ebcdbbf505d40c380e_720w

CTF-PWN练习之函数指针改写

http://www.hetianlab.com/expc.do?w=exp_ass&ec=ECID172.19.104.182014110409162800001

(学习使用objdump来查找二进制程序中函数的地址信息,并通过修改函数指针变量的值为指定函数的地址来改写程序执行流程。)

声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!


# UAF
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者