IE漏洞学习笔记(二):UAF释放后重用

 

UAF基础概念

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

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

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

实验源码如下

// UAFv1.cpp : 定义控制台应用程序的入口点。
//
#include<iostream>
using namespace std;
#include "stdafx.h"
#include<stdio.h>
#include<Windows.h>
#define size 32


class Base
{
public :
    int base;
    virtual void f(){ cout<<"Base::f()"<<endl;}
    virtual void g(){cout<<"Base::g()"<<endl;}
    virtual void h(){cout<<"Base::h()"<<endl;}
};
class Child:public Base
{
public:
    int child;
    void f(){cout<<"Child::f()"<<endl;}
    void g1(){cout<<"Child::g1()"<<endl;}
    void h1(){cout<<"Child::h1()"<<endl;}
};

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.1悬挂指针(Dangling pointer)

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

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

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

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

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

1.2占坑

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

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

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

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

1.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 <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. 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;    

}

2.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.2UAF利用流程

(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文件

 

小结

UAF在浏览器漏洞中多为对C++对象(虚函数)的修改,悬挂指针是UAF利用的关键,调用被Free的函数,如果这个函数的位置已经被别的对象占坑,进行了修改,那么调用悬挂指针就可能能够造成任意代码执行。结合Heap Spray会产生很好的效果。

(完)