windows下堆结构调试笔记

前言

最近调试windows下堆的cve的时候发现对windows下的堆管理理解不够,对javascript堆分配和利用基础不够,由于windows下没有源码可以看,只能通过网上的博客和调试器自己学习。windows堆管理在不断更新,博客内容会有所偏差,接下来的笔记是windows10上的堆结构。

堆函数

HeapCreate

这个函数创建一个只有调用进程才能访问的私有堆。进程从虚拟地址空间里保留出一个连续的块并且为这个块特定的初始部分分配物理空间。

HANDLE HeapCreate(DWORD flOptions , DWORD dwInitialSize , DWORD dwMaxmumSize);

参数:

flOptions:堆的可选属性。这些标记影响以后对这个堆的函数操作,函数有:HeapAlloc , HeapFree , HeapReAlloc , HeapSize .

下面给_出在此可以指定的标记:

HEAP_NO_SERIALIAZE:指定当函数从堆里分配和释放空间时不互斥(不使用互斥锁)。当不指定该标记时默认为使用互斥。序列化允许多个线程操作同一个堆而不会错误。这个标记是可忽略的。

HEAP_SHARED_READONLY:这个标记指定这个堆只能由创建它的进程进行写操作,对其他进程是只读的。如果调用者不是可靠的,调用将会失败,错误代码ERROR_ACCESS_DENIDE 。

注解:为了使用标记为HEAP_SHARED_READONLY的堆,运行在kernel mode(核心状态)是必须的。

dwInitialSize:堆的初始大小,单位为Bytes。这个值决定了分配给堆的初始物理空间大小。这个值将向上舍入知道下个page boundary(页界)。若需得到主机的页大小,使用GetSystemInfo 函数。

dwMaxmumSize:如果该参数是一个非零的值,它指定了这个堆的最大大小,单位为Bytes。该函数会向上舍入该值直到下个页界,然后为这个堆在进程的虚拟地址里保留舍入后大小的块。如果函数 HeapAlloc 和 HeapReAlloc 要求分配的空间超过参数 dwInitialSize 指定的大小,系统会分配额外的空间给该堆直到这个堆的最大大小。If dwMaximumSize is nonzero, the heap cannot grow and an absolute limitation arises where all allocations are fulfilled within the specified heap unless there is not enough free space. (如果该参数非零,除非没有足够的空间,这个堆总可以增长到该大小)。如果该参数为零,那么该堆大小的唯一限制是可用的内存空间。分配大小超过 0x0018000 Bytes的空间总会失败,因为获得这么大的空间需要系统调用 VirtualAlloc 函数。需要使用大空间的应用应该把该参数设置为零。

返回值:

成功:一个指向新创建的堆的指针。

失败:NULL

调用函数 GetLastError 获得更多的错误信息。

附注:

这个函数在调用进程里创建一个私有堆,进程可调用 HeapAlloc 函数分配内存空间。这些页在进程的虚拟空间内创建了一个块,在那里堆可以增长。

如果 HeapAlloc 函数请求的空间超过了现有的页大小,如果物理空间足够的话,额外的空间将会从已保留的空间里附加。

只有创建私有堆的进程可以访问私有堆。

如果一个DLL(动态链接库)创建了一个私有堆,那么这么私有堆是在调用该DLL的进程的地址空间内,且仅该进程可访问。

系统会使用私有堆的一部分空间去储存堆的结构信息,所以,不是所有的堆内空间对进程来说是可用的。例如:HeapAlloc函数从一个最大大小为 64KB 的堆里申请 64KB 的空间,由于系统占用的一部分空间,这个请求通常会失败。

HeapAlloc

LPVOID HeapAlloc(
HANDLE hHeap,
DWORD dwFlags,
SIZE_T dwBytes,
);

hHeap
要分配堆的句柄,可以通过HeapCreate()函数或GetProcessHeap()函数获得。

dwFlags
堆分配时的可选参数,其值可以为以下的一种或多种:
HEAP_GENERATE_EXCEPTIONS
如果分配错误将会抛出异常,而不是返回NULL。异常值可能是STATUS_NO_MEMORY, 表示获得的内存容量不足,或是STATUS_ACCESS_VIOLATION,表示存取不合法。
HEAP_NO_SERIALIZE
不使用连续存取。
HEAP_ZERO_MEMORY
将分配的内存全部清零。

dwBytes
要分配堆的字节数。

HeapFree

BOOL HeapFree(
HANDLE hHeap,
DWORD dwFlags,
LPVOID lpMem
);

Heap Entry

Heap Entry类似于linux下的chunk。
Heap Entry前八个字节保存结构信息,类似chunk头,但是windows为了安全性,对前八个字节进行了加密,加密方式为与HEAP结构(ps:后续后说,先理解为arena)0x50偏移处八个字节异或。可以有效防止堆溢出。
由于被加密,windbg显示混乱

于是通过x32dbg进行的调试过程(菜鸡就这个用的熟)

先把解密密钥提取出来为56 8B 12 F9 5B 64 00 00。
下面进行一次alloc堆的分配观察Heap Entry结构。

char p = (char )HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);
提取头部信息,异或解密得到eb 03 07 ef 90 00 00 18
分配的大小为8000字节,得到头部信息大小为0x3eb,大小以8字节为单位,所以真实大小应该为0x3eb8=8024字节。大小包含头部信息及最后16位的填充。

分配出来的堆块地址为0x019C0480

相邻下一堆块地址为分配出来的堆块地址为0x019C023d8
验证发现0x019C023d8-0x019C0480=8024.
此时确定前两字节为size
通过查询第三字节为flag
flag标志位
0×01 该块处于占用状态
0×02 该块存在额外描述
0×04 使用固定模式填充堆块
0×08 虚拟分配
0×10 该段最后一个堆块
此处分配的flag为07,表示该块处于占用状态,该块存在额外描述,使用固定模式填充堆块
下一处空闲的堆块flag为0x4,使用固定模式填充堆块
第四字节用于检测堆有效性的cookie
提取相邻堆块解密头部为81 01 04 84 eb 03
第5-6字节为pre_size,上一堆块的大小。验证,分配堆块的pre_size=0x90,0x90
8=0x480。0x019C0480-0x480=HeapCreate返回地基址。
第7字节 堆块所在段的序号,未验证
第8字节为UnusedBytes,未用到的字节,如分配出来的堆块头部及最后的16字节填充未使用故,第八字节为0x18.
相比较linux的堆漏洞利用,windows要多出一步信息泄露,下面是UAF的一个演示通过堆的读和写得到加密密钥
int main()
{
HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 10000, 40000);

char * p = (char *)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);
bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);

HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
void * pp = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);
DWORD z = *(((DWORD *)p) + 0x112)^ 0xE20404E2;
DWORD z1 = *(((DWORD *)p) + 0x113)^ 0x45;

return 0;

}

HEAP_SEGMENT

一个heap结构有多个heap_segment,当堆的大小不足以分配时,创建新的堆段分配新的空间。堆段通过链表进行连接。

heap_segment分配在堆里,所以整体也算是一个HEAP_ENTRY,前8个字节为HEAP_ENTRY头部。
堆段的签名为0Xffeeffee
偏移为0x10为堆段的链表。
0x18为堆段隶属的heap。
0x20为堆块分配的页的数目。当需要分配的空间大小大于这个数目*页大小时会创建一个新的heap_segment并链入链表中。
FirstEntry,LastValidEntry 分别为堆段中第一个,及最后一个HEAP_ENTRY结构。
堆段中主要保存,堆段的起始及范围。堆段隶属的heap,堆段的链表。

HEAP 结构

每个HEAP有一个HEAP结构,一个heap结构有多个heap_segment

heap结构{
heap_segment
heap
}

heap结构在每个堆的起始地址,由每个堆的0号堆段和自己的结构拼接而成,由图可见堆段大小为0x40,前0x40字节属于0号堆段。
0x40之后的heap结构用来保存堆的资产及必要信息。
+0x040 Flags : 2
+0x044 ForceFlags : 0
+0x048 CompatibilityFlags : 0
+0x04c EncodeFlagMask : 0x100000
+0x050 Encoding : _HEAP_ENTRY
+0x058 Interceptor : 0
+0x05c VirtualMemoryThreshold : 0xfe00
+0x060 Signature : 0xeeffeeff
+0x064 SegmentReserve : 0x1fd0000
+0x068 SegmentCommit : 0x2000
+0x06c DeCommitFreeBlockThreshold : 0x800
+0x070 DeCommitTotalFreeThreshold : 0x2000
+0x074 TotalFreeSize : 0x3c440
+0x078 MaximumAllocationSize : 0xfffdefff
+0x07c ProcessHeapsListIndex : 1
+0x07e HeaderValidateLength : 0x248
+0x080 HeaderValidateCopy : (null)
+0x084 NextAvailableTagIndex : 0
+0x086 MaximumTagIndex : 0
+0x088 TagEntries : (null)
+0x08c UCRList : _LIST_ENTRY [ 0x5a4ffe8 - 0x5a4ffe8 ]
+0x094 AlignRound : 0xf
+0x098 AlignMask : 0xfffffff8
+0x09c VirtualAllocdBlocks : _LIST_ENTRY [ 0x71009c - 0x71009c ]
+0x0a4 SegmentList : _LIST_ENTRY [ 0x710010 - 0x5760010 ]
+0x0ac AllocatorBackTraceIndex : 0
+0x0b0 NonDedicatedListLength : 0
+0x0b4 BlocksIndex : 0x00710260 Void
+0x0b8 UCRIndex : (null)
+0x0bc PseudoTagEntries : (null)
+0x0c0 FreeLists : _LIST_ENTRY [ 0x3c2b140 - 0x59ed748 ]
+0x0c8 LockVariable : 0x00710248 _HEAP_LOCK
+0x0cc CommitRoutine : 0x1bb2ba92 long +1bb2ba92
+0x0d0 StackTraceInitVar : _RTL_RUN_ONCE
+0x0d4 FrontEndHeap : 0x00150000 Void
+0x0d8 FrontHeapLockCount : 0
+0x0da FrontEndHeapType : 0x2 ‘’
+0x0db RequestedFrontEndHeapType : 0x2 ‘’
+0x0dc FrontEndHeapUsageData : 0x007161b0 -> 0
+0x0e0 FrontEndHeapMaximumIndex : 0x802
+0x0e2 FrontEndHeapStatusBitmap : [257] “???”
+0x1e4 Counters : _HEAP_COUNTERS
+0x240 TuningParameters : _HEAP_TUNING_PARAMETERS
+0x040 Flags 堆的flag由heapcreate时的flag参数控制,其中HEAP_GROWABLE(0x2)属性是默认的。且私有堆的flag要 or 0x1000.




创建时参数为4,flag为 2or 4 or 0x1000=0x1006
+0x050为之后解密HEAP_ENTRY头部的密钥

+0x060 Signature : 0xeeffeeff heap结构头签名
+0x078 MaximumAllocationSize 允许分配的最大空间,由于heapcreate时参数选择了0,这里允许分配整个地址空间大小。
+0x07c heap在peb堆数组中的索引
+0x074 TotalFreeSize : 0x3c440 全部free堆块的总大小。
+0x07e HeaderValidateLength heap结构头的大小
+0x0a4 SegmentList 堆段的链表,前向指针指向0号堆段,后向指针指向最后一个堆段。
+0x0c0 FreeLists 保存整个堆的空闲堆块,所有堆块的空闲堆块的集合

总结

堆的分配和释放还没有源码只能根据之后的实验进行尝试,菜鸟文章,大牛勿喷。写的有问题还需要改进。