Written: Hume/冷雨
Email: humewen@21cn.com
QQ: 8709369
恰逢冲击波病毒肆虐,因此拜读了ISNO、ISLY、yuange、flashsky等高人关于堆溢出的文章,佩服之至。闲暇之余,亦对堆溢出产生了浓厚的兴趣,就是想知道为什么会这样以及Windows是如何控制堆对象的,这在上述的文章中均没有详细述及。简而言之,就是想搞懂Windows的堆管理器。这个问题实在是烦琐,因为我看不到Windows的源代码,只好反汇编之。要全部看懂这些代码要花不少时间,而最近上头逼的又紧,所以只能看懂多少算多少了,反正丑媳妇不怕见公婆吗。以前没怎么接触过安全方面的东西,一些幼稚的想法和推断,也列在其中,敬请指正。
堆的分配算法等细节随不同操作系统版本是有变化的,本文基于Windows 2000 PRO SP4版本。
1、Win32堆的价值
Win32堆(heap)是Windows负责为用户程序维护分配、校验、释放等细节的一块区域,可以将作为一个分配器(allocator)对象来对待。Win32堆是基于虚拟内存管理器的,分配和提交内存等都依赖于虚拟内存管理器,高级语言中也有其各自的堆管理,一般这些堆管理是基于Win32堆的。比如下面的流程:
应用程序-->C运行时堆
|
|
^
Win32堆管理
|
|
^
NT运行时库函数(NTDLL.DLL)--->windows虚拟内存管理器
事实上,win32环境子系统的堆管理函数大部分是对NTDLL.DLL中相关函数的直接向前(forward)引用。那么问题是我们为什么不直接调用虚拟内存函数自己来管理内存呢?我们自己绝对可以这样干,但要付出的代价肯定也是巨大的:
1.如何处理多线程访问内存的同步;
2.如何在需要多块、频繁的内存时释放、分配时减少内存碎片并保证内存分配和使用的效率;
3......
重新发明一个轮子是可行的,不过我们完全没有必要这样做,不仅是因为我们懒惰,更是为了配合软件工程学的一些基本原则。于是,各种高级语言编译器中都采用了堆管理,很多for Win编译器都封装了Win32堆函数,我们也就开始使用摆在那里的好用的堆函数。
2.Windows的堆管理
堆管理器采用的分配算法在不同的Win版本上是不同的,微软也一直在拿广大用户的计算机做试验,试图在效率和资源占用两方面取得一个合理的折衷。堆的管理和回收是个很大的主题,在网上这样的多如牛毛。在关于堆溢出的文章中我看到的有几点:一是分配内存块的管理结构,一是堆分配和释放的链表管理,但都没有详细说明。本文试图进行一点详细说明,以便能够吃到鱼,还能知道为什么能吃到鱼。
在NTDLL中windows为堆管理提供了两套API,一套是用于正常管理分配的,一套是用于调试的。堆调试API为探测堆溢出、验证堆的有效性等提供了便利。在使用某些ring3调试器调试的时候,Windows会创建调试堆,因此要对正常的堆分配进行跟踪,最好使用ring0调试器(如SICE)。对于堆调试API,不作分析。下面的说明及分析均基于正常的堆分配。
3.堆的细节
创建进程时会创建一个默认堆,可以使用GetProcessHeap获取,这个函数很简单,下面是伪码:
//
//lc:KERNLE32.DLL 获取进程默认堆的句柄,从PEB中取得进程默认堆句柄
//
HANDLE GetProcessHeap(VOID)
{
PPEB pPeb;
__asm
{
mov eax,fs:[0x18] //pTeb linear addr
mov eax,[eax+0x30] //pPeb
mov pPeb,eax
}
return pPeb->ProcessHeap; //*(pPeb+0x18)
}
RtlGetProcessHeaps是类似的,以下是伪码:
DWORD RtlGetProcessHeaps(
DWORD NumberOfHeaps, // maximum number of heap handles
PHANDLE ProcessHeaps // buffer for heap handles
){
__try{
PPEB pPeb;
DWORD NumEnum;
__asm{
mov eax,fs:[18]
mov eax,[eax+30]
mov pPeb,eax //get PEB pointer
}
RtlEnterCriticalSection(&_RtlpProcessHeapsListLock);
NumEnum=NumberOfHeaps;
if (NumberOfHeaps>pPeb->NumberOfHeaps)
{
NumEnum=pPeb->NumberOfHeaps;
}
memmov(ProcessHeaps,pPeb->ProcessHeapsList,NumEnum);
RtlLeaveCriticalSection(&_RtlpProcessHeapsListLock);
//
//调试相关
//
if (_RtlpDebugPageHeap)
{
_RtlpDebugPageHeapGetProcessHeaps(NumberOfHeaps,ProcessHeaps);
}
}
__finally
{
RtlLeaveCriticalSection(&_RtlpProcessHeapsListLock);
}
return true;
}
堆句柄实际是指向堆头部结构的一个指针。
一些堆信息都是从PEB中获取的,堆的行为还会受到NT GLOBAL FLAG的影响,这些标志大约有8个,控制着进行堆操作时是否进行参数检查、合并相邻的空闲块等行为,具体参见MSDN的GLOBAL FLAGS refrence。
在一些堆溢出文章中提到了8个字节的管理结构,这8字节的管理结构位于分配的堆内存块的前面,整个堆大约是这样子的:
|堆的头部结构|堆管理结构|用户分配的堆块 .....|堆管理结构|用户分配的堆块|堆末尾管理结构|链表节点
这8字节的管理结构究竟是什么样的呢?经过反汇编研究,发现是这样的。
typedef struct
{
/*0x0*/ USHORT curBlockSizeDiv8;
/*0x2*/ USHORT prevBlockSizeDiv8;
/*0x4*/ UCHAR index;
/*0x5*/ UCHAR flags;
/*0x6*/ USHORT sizeMng;
} MNG_STRUCT,*PMNG_STRUCT;
curBlockSizeDiv8和prevBlockSizeDiv8分别是当前分配堆块和其紧临其前的分配堆块的大小,计算方法是堆块的实际字节数大小除以8。index是指向头结构0x58开始的大小为0x40的一个双字数组的索引,含义还不是很清楚,推测用于管理非常多的堆块的扩充,也许类似目录的作用,该值一般为0,也就是说一般只有一个表项,很显然不能大于0x40。flags是堆块的标志,为1是正常分配的堆块,为0x10表示末尾链表节点之前的堆块。sizeMng实际是
(要求分配的字节数按照8字节对齐后+8字节的管理区的大小)-要求分配的字节数
用这个值可以计算出请求分配的内存的大小。堆管理的RtlSizeHeap就是这样计算的,下面是伪码:
//location:NTDLL.DLL 获取堆块的大小 Kernel32.DLL.HeapSize->forward
DWORD RtlSizeHeap(
HANDLE hHeap, // handle to the heap
DWORD dwFlags, // heap size control flags
LPCVOID lpMem // pointer to memory to return size for
){
ULONG HeapSizeByBytes=0;
PVOID ptrHeap= (void *)hHeap;
DWORD flag1=[ptrHeap+0x10];
if (flag1&0x69020000)
{
//堆调试处理省略之
debug_do_something;
return;
}
PMNG_STRUCT pMng=(unsigned char *)lpMem-8;
BYTE flag=pMng->Flags;
if (flag2!=1)
{
invalid_param();
return;
}
return (pMng->curBlockSizeDiv8 * 8 - pMng->sizeMng);
}
对管理结构的了解有助于构造溢出数据,利用堆溢出避免在Validate等操作后检查出数据错误,甚至可以在堆溢出获取控制权后重构堆数据避免一些问题。
夜深,有时间再继续。。。。
Good.
> 这8字节的管理结构究竟是什么样的呢?经过反汇编研究,发现是这样的。
这是微软自己的定义
+0x000 Size : Uint2B
+0x002 PreviousSize : Uint2B
+0x000 SubSegment : Ptr32 to Void
+0x004 SegmentIndex : UChar
+0x005 Flags : UChar
+0x006 UnusedBytes : UChar
+0x007 SmallTagIndex : UChar
小四哥,不会是查看了Windows的源代码了吧?知道这个我就不费劲了。。。。
不过看起来象是Windbg的定义。。。。
+0x006 UnusedBytes : UChar
这个好像确实有用,不是UnusedBytes
严格说,这里只涉及到堆块管理结构的内容,要说WINDOWS下的堆的分析,还有很多内容的.我前一段时间仔细分析了一些WINDOWS的对内容,大致也写了40来页的WINDOWS堆结构的分析的东西.
关于堆块管理结构的内容,SCZ给出是很正确的说明,,其实充分利用堆操作对这些字段的操作的各种特性,可以获得利用堆的非常具备实用的技巧,如最近的MESSENGER的堆益处,利用堆的这些特性,我们能够写出非常有效的利用程序来,即使在2K SP4这种TOP SEH带0X14的系统上也能获得非常好的成功率,也能简单处理堆异常引发的一些导致API不能成功调用的问题.
另外关于+0x006 UnusedBytes : UChar,我这里给出我的详细的此字段的分析吧:
#############################################
2.2.5 FreeSize字段
长度:1字节
偏移:6
含义:非正常使用的堆块空间大小,单位为字节
说明:
此字段的大小包含了8字节的管理头结构和因对齐或重分配导致的浪费的空间大小。
通过这个字段可以算出实际申请分配的堆块空间大小。也就是8*ThisChunkSize-FreeSize
由于堆块的大小是取8字节对齐的,因此在分配的时候存在因对齐而空余的几个字节。。
另外由于堆的重分配操作,也会改变此字段的大小,甚至有可能会使得此字段大于等于0X10(非重分配的正常分此字段必然大于等于8小于0X10)
此字段只在堆块为已使用状态下有意义。
但是对于堆本身管理结构所使用的堆块,一般固定为0。
################################################
我想hume的这段代码:
if (pPeb->NumberOfHeaps>NumberOfHeaps)
{
NumEnum=pPeb->NumberOfHeaps;
}
是不是应该是这样的:
if (pPeb->NumberOfHeaps
NumEnum=pPeb->NumberOfHeaps;
}
因为我认为这儿的NumEnum代表的是将要分配的内存块数,而NumberOfHeaps是所需的最大块数,所以在pPeb->NumberOfHeaps>NumberOfHeaps的条件下
NumEnum应该等于NumberOfHeaps而不是pPeb->NumberOfHeaps。不知我理解可否正确?请指正。
---
还有一个BYTE flag=pMng->Flags
应为BYTE flag2=pMng->Flags
仔细看了看,SICE看的眼花,上面文章中已经更正谢楼上
hume 编辑于 2003-11-07 17:52
没有评论:
发表评论