发新话题
打印

[转载]Win2K SP4堆研究

[转载]Win2K SP4堆研究

Win2K SP4堆研究
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等操作后检查出数据错误,甚至可以在堆溢出获取控制权后重构堆数据避免一些问题。

夜深,有时间再继续。。。。

TOP

发新话题