UAF (Use After Free)漏洞分析及利用

因为大作业的需求要调试一个浏览器的UAF漏洞,首先必须对UAF漏洞有个整体的了解,本篇文章主要讲解UAF造成的原因以及利用方法,这里结合2016年HCTF fheap
题目分析起来还是有点耐人寻味。

0x01 UAF 原理

这里首先放一段简单的c代码,让大家更容易理解(linux 环境)

#include <stdio.h>
#include <cstdlib>
#include <string.h>
int main()
{
    char *p1;
    p1 = (char *) malloc(sizeof(char)*10);//申请内存空间
    memcpy(p1,"hello",10);
    printf("p1 addr:%x,%s\n",p1,p1);
    free(p1);//释放内存空间
    char *p2;
    p2 = (char *)malloc(sizeof(char)*10);//二次申请内存空间,与第一次大小相同,申请到了同一块内存
    memcpy(p1,"world",10);//对内存进行修改
    printf("p2 addr:%x,%s\n",p2,p1);//验证
    return 0;
}

如上代码所示

1.指针p1申请内存,打印其地址值
2.然后释放p1
3.指针p2申请同样大小的内存,打印p2的地址,p1指针指向的值

Gcc编译,运行结果如下:
这里写图片描述
p1与p2地址相同,p1指针释放后,p2申请相同的大小的内存,操作系统会将之前给p1的地址分配给p2,修改p2的值,p1也被修改了。

重温程序,看注释


根本原因

应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放会内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。

简单讲就是第一次申请的内存空间在释放过后没有进行内存回收,导致下次申请内存的时候再次使用该内存块,使得以前的内存指针可以访问修改过的内存。

0x02 漏洞的简单利用

还是先放一段程序(linux x86)

#include <stdio.h>
#include <stdlib.h>
typedef void (*func_ptr)(char *);
void evil_fuc(char command[])
{
system(command);
}
void echo(char content[])
{
printf("%s",content);
}
int main()
{
    func_ptr *p1=(func_ptr*)malloc(4*sizeof(int));
    printf("malloc addr: %p\n",p1);
    p1[3]=echo;
    p1[3]("hello world\n");
    free(p1); //在这里free了p1,但并未将p1置空,导致后续可以再使用p1指针
    p1[3]("hello again\n"); //p1指针未被置空,虽然free了,但仍可使用
<think>UAFUse-After-Free漏洞是一种内存安全漏洞,指的是在程序释放了某个内存块后,仍然继续使用指向该内存块的指针。攻击者可以通过精心设计的内存操作,在释放的内存中填入恶意数据,从而在程序使用已释放指针时控制程序执行流程。 下面是一个简单的UAF漏洞利用示例,使用C语言编写。这个示例程序存在UAF漏洞,我们将通过这个漏洞执行一个简单的攻击:修改虚函数表指针,使其指向我们构造的虚假虚函数表,从而控制程序流。 注意:这个示例仅用于教育目的,实际漏洞利用需要根据具体环境调整。 ### 漏洞程序 ```c #include <iostream> #include <cstring> #include <cstdlib> class Base { public: virtual void show() { std::cout << "Base::show()" << std::endl; } virtual ~Base() {} }; class Derived : public Base { public: void show() override { std::cout << "Derived::show()" << std::endl; } }; int main() { Base *a = new Derived(); a->show(); // 正常调用 delete a; // 释放内存 // 分配一个与Base大小相同的对象,尝试覆盖被释放的内存 char *b = new char[sizeof(uintptr_t) * 2]; // 通常虚函数表指针占用一个指针大小,但为了覆盖整个对象,我们分配足够空间 uintptr_t *vtable_ptr = reinterpret_cast<uintptr_t*>(b); // 构造一个假的虚函数表指针,指向我们控制的地址(这里假设我们有一个恶意函数地址) // 注意:实际中我们需要知道一个可执行的地址,这里用0x41414141模拟 vtable_ptr[0] = 0x41414141; // 覆盖虚函数表指针 // 使用已释放的指针 a->show(); // UAF发生,调用虚函数时,会跳转到0x41414141 delete[] b; return 0; } ``` ### 漏洞利用说明 1. 程序创建了一个`Derived`对象,并通过基类指针`a`指向它。 2. 然后释放了`a`指向的内存。 3. 接着分配了一个字符数组`b`,大小与`Base`对象相同(通常包含一个指向虚函数表的指针)。在释放的内存上重新分配,这样`b`就覆盖了原来`a`指向的内存。 4. 在`b`中,我们构造了一个假的虚函数表指针,将其指向一个我们控制的地址(这里用`0x41414141`模拟,实际中需要替换为有效的函数指针)。 5. 当再次通过`a`调用虚函数`show()`时,程序会去假的虚函数表中查找函数地址,并跳转到`0x41414141`执行,从而控制程序流程。 ### 漏洞利用的关键点 - 需要知道释放后重新分配的内存大小,确保分配相同大小的内存块。 - 需要控制重新分配的内存内容,写入伪造的虚函数表指针。 - 需要知道一个可执行的地址(例如,攻击者注入的shellcode地址),这里用0x41414141模拟,实际运行会导致崩溃,但证明控制流被劫持。 ### 实际利用中的挑战 - 现代操作系统有ASLR(地址空间布局随机化),使得地址难以预测。 - DEP(数据执行保护)使得注入的代码无法执行。 - 堆分配器的行为可能复杂(例如,不同大小的分配可能使用不同的缓存)。 ### 缓解措施 - 使用智能指针(如`std::shared_ptr`,`std::unique_ptr`)自动管理内存。 - 释放指针后立即置空,避免悬垂指针。 - 使用内存安全语言(如Rust)或内存安全工具(如AddressSanitizer)检测UAF。 ### 相关安全技术 - 利用UAF通常需要结合堆喷(Heap Spraying)来在内存中布置大量恶意数据,增加跳转地址命中的概率。 - 结合信息泄露绕过ASLR:先泄露某个对象的地址,计算出基地址,再构造虚假虚函数表。 ### 注意事项 上述代码在编译时可能会因为现代编译器的保护(如虚函数表保护)而无法直接触发,且运行时会崩溃。但在没有保护的环境中(如关闭安全机制),可以演示UAF的基本原理。 ### 编译运行 在Linux下编译运行(关闭DEP,但ASLR可能仍然存在): ```bash g++ -fno-stack-protector -z execstack -o uaf uaf.cpp ``` ### 实际漏洞利用代码示例(概念性) 由于实际漏洞利用需要结合具体环境和目标,以下是一个更实际的利用步骤(伪代码): 1. 释放对象A。 2. 分配一个可控对象B,占据A释放的内存。 3. 在对象B中伪造虚函数表指针,指向一个恶意构造的虚函数表(其中包含恶意函数指针)。 4. 触发虚函数调用(如调用原对象的虚函数),跳转到恶意代码。 ### 防御建议 - 使用AddressSanitizer(ASan)检测UAF:编译时加上`-fsanitize=address`。 - 使用智能指针。 §§相关问题§§: 1. 如何检测UAF漏洞? 2. 现代操作系统有哪些安全机制可以缓解UAF漏洞利用? 3. 在C++中,如何安全地管理内存以避免UAF? 4. 请解释堆喷(Heap Spraying)技术及其在漏洞利用中的作用。</think>### UAF漏洞利用代码示例 UAFUse-After-Free漏洞发生在程序释放内存后仍继续使用该内存指针。下面是一个经典的C++ UAF漏洞利用示例,通过虚函数表劫持实现代码执行: ```cpp #include <iostream> #include <cstring> class VulnerableClass { public: virtual void execute() { std::cout << "Legitimate function" << std::endl; } virtual ~VulnerableClass() {} }; class MaliciousClass { public: virtual void payload() { std::cout << "Malicious payload executed!" << std::endl; // 实际攻击中这里会执行shellcode } }; int main() { // 分配原始对象 VulnerableClass* obj = new VulnerableClass(); // 释放内存 delete obj; // UAF漏洞点:内存被释放但指针未置空 // 分配恶意对象(占用相同内存位置) size_t objSize = sizeof(VulnerableClass); char* fakeVtable = new char[objSize]; memset(fakeVtable, 0, objSize); // 构造恶意虚函数表 void (**maliciousVtable)() = new void(*[2])(); maliciousVtable[0] = reinterpret_cast<void(*)()>( static_cast<void (MaliciousClass::*)()>(&MaliciousClass::payload) ); // 将恶意虚表指针写入内存 *reinterpret_cast<void***>(fakeVtable) = reinterpret_cast<void**>(maliciousVtable); // 触发UAF - 调用已释放对象的虚函数 obj->execute(); // 实际执行MaliciousClass::payload() delete[] fakeVtable; delete[] maliciousVtable; return 0; } ``` ### 代码解释 1. **漏洞创建**: - 创建`VulnerableClass`对象并立即删除,但保留悬垂指针`obj` - 典型漏洞模式:释放后未置空指针 2. **内存占用**: - 分配相同大小的内存`fakeVtable`占据被释放的内存空间 - 使用`memset`清零确保内存控制 3. **虚表劫持**: ```cpp void (**maliciousVtable)() = new void(*[2])(); maliciousVtable[0] = reinterpret_cast<void(*)()>( static_cast<void (MaliciousClass::*)()>(&MaliciousClass::payload) ); ``` - 创建伪造虚函数表 - 将第一个虚表项指向恶意函数`payload()` - 使用双重转换绕过C++类型检查 4. **内存布局控制**: ```cpp *reinterpret_cast<void***>(fakeVtable) = reinterpret_cast<void**>(maliciousVtable); ``` - 在伪造对象头部写入恶意虚表指针 - 对象内存布局:`[vptr][成员变量...]` 5. **触发利用**: ```cpp obj->execute(); // 实际调用maliciousVtable[0] ``` - 通过悬垂指针调用虚函数 - CPU从虚表加载函数指针并执行恶意代码 ### 防护措施 1. **释放后置空**: ```cpp delete obj; obj = nullptr; // 防止UAF ``` 2. **智能指针**: ```cpp std::unique_ptr<VulnerableClass> obj = std::make_unique<VulnerableClass>(); ``` 3. **内存消毒工具**: ```bash # 使用AddressSanitizer编译 g++ -fsanitize=address -g uaf.cpp -o uaf ``` 4. **安全分配器**: - 使用jemalloc或tcmalloc增强堆安全性
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值