文章目录
Windows堆学习(一)
0x0 堆的数据结构
Windows中分堆块和堆表,其中我们通过动态分配内存函数( m a l l o c \textcolor{cornflowerblue}{malloc} malloc、 r e l o o c \textcolor{cornflowerblue}{relooc} relooc、 H e a p A l l o c \textcolor{cornflowerblue}{HeapAlloc} HeapAlloc等)分配到的内存就是堆块中的内存,而堆表是用来维护这些堆块的。
1.堆块
处于性能的考虑,堆区的内存按不同大小组织成堆块。堆块由堆首和块身构成,详细结构如下:
字段名 | 含义 | 占用大小 |
---|---|---|
Chunk_size | 本身块大小(包含块头),实际上存的是堆块在堆表中的偏移。实际大小=Chunk_size*8 | 2字节 |
Prev_size | 以本身相邻的前一个块大小(包括它的块头),实际上存的是前一个堆块在堆表中的偏移。实际大小=Prev_size*8 | 2字节 |
Segment_index | 不明确 | 1字节 |
Flag | 标志:0x01 堆块正在被程序或者堆管理器使用 0x04 堆块使用了填充模式(File Pattern) 0x08 堆块是直接从虚拟内存管理器中分配而来的 0x10 堆块是未提交范围之前的最后一个堆块 | 1字节 |
Unused | 用于标志该堆块处于使用(0x0)还是空闲(0x1) | 1字节 |
Tag_index | 不明确。在Debug状态下有用 | 1字节 |
Flink | 前向指针,指向前一个堆块 | 4字节 |
Blink | 后向指针,指向后一个堆块 | 4字节 |
这 里 是 为 了 和 l i n u x 下 的 堆 结 构 有 个 对 比 , 所 以 相 同 的 地 方 用 相 同 的 字 段 名 标 注 。 \textcolor{green}{这里是为了和linux下的堆结构有个对比,所以相同的地方用相同的字段名标注。} 这里是为了和linux下的堆结构有个对比,所以相同的地方用相同的字段名标注。
- 在Data部分中的情况和linux下类似,当chunk被释放进入了堆表后,堆管理器会根据此时堆表的情况往释放的chunk的Data域中写入Flink和Blink。堆管理器返回给用户使用的内存其实隐去了块首部分,因此我们感觉不到。这个可以在我们连续分配多块内存的时候,通过观察相邻地址之间的偏移大小发现。
2.堆表
在Windows中,堆表一般位于堆区起始处,用于索引空闲堆块。有比较重要的两种堆表:空闲双向链表(FreeList空表)和快速单向链表(Lookaside快表)。空表和块表均有128个条目,但是快表一个条目最多只有4个节点。
FreeList:
- f r e e l i s t [ 0 ] \textcolor{orange}{freelist[0]} freelist[0]索引所有大于1024字节的堆块,升序排列。 f r e e l i s t [ 1 ] \textcolor{orange}{freelist[1]} freelist[1]索引大小为8的堆块, f r e e l i s t [ 2 ] \textcolor{orange}{freelist[2]} freelist[2]索引16字节,依次递增,直到 f r e e l i s t [ 127 ] \textcolor{orange}{freelist[127]} freelist[127]索引大小为1016字节的堆块.
Lookaside:
- 快表索引的堆块均有使用标志,因此不会发生合并,遵守先进先出原则。
0x1 堆块的操作
1.分配
根据堆表的结构,堆块的分配分为3类:
- 从快表中分配
- 从0号空表中分配
- 从普通空表中分配
假设执行以下代码:
HLOCAL h=NULL;
HANDLE hp=HeapCreate(0, 0x1000, 0x10000);
h = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
虽然用户请求的是8个字节,但正如前面所说每个堆块还有8字节大小的头部,因此系统实际会申请16个字节的堆块,此时会计算空表中的索引 i d = 16 / 8 = 2 \textcolor{orange}{id=16/8=2} id=16/8=2。
-
如果是从快表中分配就是寻找大小匹配的chunk,并将其空闲状态修改为使用状态,然后把它从堆表中“卸载”下来,最后返回一个指向堆身的指针给用户使用。
-
如果是从0号空表中分配就是先从 f r e e l i s t [ 0 ] \textcolor{orange}{freelist[0]} freelist[0]反向匹配大小,如果大于要求的size再从正向匹配,采用这种二分查找的思想直到找到符合size的chunk。理想情况下是正好找到一个大小相等的chunk,而常常可能是得到略大于要求size的chunk。
-
如果是普通空表分配就是现寻找最优的空闲分区分配,若失败再找次优的空闲分区分配。
-
如果只能找到次优的分配方案时,即找到的chunk略大于要求的size,假设为0x24。则堆管理器会从0x24中割出0x16给程序使用,并将剩下的0x8的chunk重新定位堆表,并链入。
注 : 快 表 是 精 确 分 配 的 , 并 不 会 发 生 合 并 ! \textcolor{green}{注:快表是精确分配的,并不会发生合并!} 注:快表是精确分配的,并不会发生合并!
2.释放
将释放的chunk的状态改为空闲,链入相应的堆表。所有释放的chunk均从末端链入,分配时也是先从末端拿取。
3.合并
经过多次的分配和释放,堆区必然会“千疮百孔”,产生很多内存碎片。为了高效利用内存,堆管理器便会对相邻的空闲堆块进行合并操作。这个过程为:
将这两块空闲chunk从原来的空表中“卸下”,合并这两个chunk得到一个大的chunk,调整大chunk的块首,最后将答chunk链入相应的空表。
Windows堆管理器合并chunk具体分为三种情况:
- 小块: s i z e < 1 K B \textcolor{orange}{size < 1KB} size<1KB
- 大块: 1 K B < = s i z e < 512 K B \textcolor{orange}{1KB <= size < 512KB} 1KB<=size<512KB
- 巨快: s i z e > = 512 K B \textcolor{orange}{size >= 512KB} size>=512KB
巨块的合并过程如图所示:
总的算法描述如下表所示:
分配 | 释放 | |
---|---|---|
小块 | 首先进行快表分配 若块表分配失败,则进行普通空表分配 若普通空表分配失败,则进行堆缓存(heap cache)分配 若堆缓存分配失败,则进行0号空表分配 若0号空表分配失败,则进行内存紧缩后再分配 以上步骤都失败就返回NULL | 优先链入快表,满了再放入对应的空表 |
大块 | 首先使用堆缓存进行分配 若堆缓存失败,则使用0号空表分配 | 优先放入堆缓存,满了再放入0号空表 |
巨块 | 很少使用得到,使用到虚分配的方法,并不从堆表中分配 | 直接释放,无堆表操作 |
0x2 探索堆管理器
1.堆管理器的架构
堆分配体系架构框架图:
2.堆分配函数的依赖关系
- 应用程序调用的任何堆分配函数最终都会调用位于 K e r n e l 32. d l l \textcolor{orange}{Kernel32.dll} Kernel32.dll中导出的函数 R t l A l l o c a t e H e a p ( ) \textcolor{cornflowerblue}{RtlAllocateHeap()} RtlAllocateHeap(),而 K e r n e l 32. d l l \textcolor{orange}{Kernel32.dll} Kernel32.dll中的导出函数 R t l A l l o c a t e H e a p ( ) \textcolor{cornflowerblue}{RtlAllocateHeap()} RtlAllocateHeap()会进入内核模式下进行堆分配的操作,所以研究 R t l A l l o c a t e H e a p ( ) \textcolor{cornflowerblue}{RtlAllocateHeap()} RtlAllocateHeap()对攻击堆的意义非常大!
0x3 堆的调试方法
想写出漂亮的堆溢出exploit,仅仅知道堆分配的策略是远远不够的,我们需要对堆中的重要数据结构掌握到字节级别。
1.调试前准备
Windows下面调试会存在两种状态的堆,这也是与Linux不同的一点。如果编译的程序是Debug版本或者直接用调试器加载程序,看到的堆都是调试堆,其堆的数据结构也会和发行版(Release)的不一样,而我们期望看到的应是发行版的,所以这里需要一点技巧。方法就是在堆分配之前写个 i n t 3 \textcolor{orange}{int\ 3} int 3断点,将调试器设置为实时调试器。这里提一点,想要更好的调试堆选择一款好的调试器特别重要,我建议使用windbg或者immunity debuger。我用的是immunity debuger,设置过程如下:
- o p t i o n − > j u s t − i n − t i m e d e b u g g i n g \textcolor{orange}{option->just-in-time\ debugging} option−>just−in−time debugging,在弹出的窗口中:
- 依次点击圈中的按钮。
编译为Release版并运行程序,示例代码:
#include<stdio.h>
#include<windows.h>
int main(){
HLOCAL h1,h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0, 0x1000, 0x10000);
__asm{int 3}
h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
h3=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h4=HeapAlloc(hp,HEAP_ZERO_MEMORY,12);
h5=HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
h6=HeapAlloc(hp,HEAP_ZERO_MEMORY,32);
HeapFree(hp,HEAP_ZERO_MEMORY,h1);
HeapFree(hp,HEAP_ZERO_MEMORY,h3);
HeapFree(hp,HEAP_ZERO_MEMORY,h5);
return 0;
}
调试器就会自动附加进程,此时会断在 i n t 3 \textcolor{orange}{int\ 3} int 3处
将 i n t 3 \textcolor{orange}{int\ 3} int 3代码 n o p \textcolor{orange}{nop} nop掉就可以愉快的单步调试了。
Tips:亲历了两个坑,这里记录一下。
不能设置试试调试,即just-in-time按钮是灰的。此时应该检查以下选项是否打开
设置实时调试成功后,运行程序调试器没有自动启动并附加目标进程。此时应该修改注册表位置:
H K E Y _ L O C A L _ M A C H I N E / S O F T W A R E / M i c r o s o f t / W i n d o w s N T / C u r r e n t V e r s i o n / A e D e b u g \textcolor{orange}{HKEY\_LOCAL\_MACHINE/SOFTWARE/Microsoft/Windows\ NT/CurrentVersion/AeDebug} HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Windows NT/CurrentVersion/AeDebug
修改Debuger的值为调试器绝对路径 − A E D E B U G % l d % l d \textcolor{orange}{-AEDEBUG\ \%ld\ \%ld} −AEDEBUG %ld %ld
调试堆与正常堆不同的地方有:
- 调试堆不使用快表,只用空表分配
- 所有堆块都会追加16个字节用来防止溢出(防止程序溢出,而不是堆溢出攻击),包括8个字节的 0 x A B \textcolor{orange}{0xAB} 0xAB和8个字节的 0 x 00 \textcolor{orange}{0x00} 0x00。
- 堆首的标志位不同
2.调试过程部分细节
创建完堆区,还没有执行任何堆块的分配操作时,堆区信息如下
-
只有一个空闲的大块称为尾块(个人认为这里可以理解为Linux下的Top chunk),如图中的0x3a680。
-
0x3a0000是堆首,大小为0x640,由 H e a p C r e a t e ( 0 , 0 x 1000 , 0 x 10000 ) \textcolor{orange}{HeapCreate(0, 0x1000, 0x10000)} HeapCreate(0,0x1000,0x10000)创建的。
-
偏移为0x640处(启动快表之后这个位置将是快表头部),这里算上堆基址=0x3a0640。
-
0x3a0178为 f r e e L i s t [ 0 ] \textcolor{orange}{freeList[0]} freeList[0],指向尾块。
Tips:可以通过 H e a p C r e a t e ( 0 , 0 , 0 ) \textcolor{orange}{HeapCreate(0,0,0)} HeapCreate(0,0,0)创建一个可扩展的堆,这样系统就会启用快表。
6次堆块分配操作过后,堆区信息如下:
用下面一张表来表示用户分配的这6块堆和系统实际分配的结果
堆句柄 | 用户请求的大小(字节) | 系统实际分配大小(字节) | 系统得到的堆指针 | 用户得到的堆指针 |
---|---|---|---|---|
h1 | 3 | 16 | 0x3a0680 | 0x3a0688 |
h2 | 6 | 16 | 0x3a0690 | 0x3a0698 |
h3 | 8 | 16 | 0x3a06a0 | 0x3a06a8 |
h4 | 12 | 32 | 0x3a06b0 | 0x3a06b8 |
h5 | 24 | 48 | 0x3a06c8 | 0x3a06d0 |
h6 | 32 | 64 | 0x3a06e8 | 0x3a06f0 |
可见操作系统分配堆的大小是以8字节对齐的,例如h1、h2、h4,用户请求的大小均不是8的倍数,系统分配的时候就会向上取整并加上头部8字节的大小作为实际分配的大小,然后系统将分配得到的内存地址偏移8个字节取得堆的数据域返回给用户使用。
同时,还观察到尾块的大小正好减小了 0 x 90 = 0 x 980 − 0 x 8 f 0 = 0 x 10 ∗ 3 + 0 x 18 + 0 x 20 + 0 x 28 \textcolor{orange}{0x90=0x980-0x8f0=0x10*3+0x18+0x20+0x28} 0x90=0x980−0x8f0=0x10∗3+0x18+0x20+0x28。
再经过3次释放操作,此时堆区的信息如下图:
空表中的情况:
用户释放的顺序是h1、h3、h5,而h1和h3是同属一个空表的,他俩链入空表的顺序是最后释放的放在最前面。并且 h 1 − > F l i n k = h 3 \textcolor{orange}{h1->Flink=h3} h1−>Flink=h3, h 1 − > B l i n k = F r e e L i s t [ 2 ] \textcolor{orange}{h1->Blink=FreeList[2]} h1−>Blink=FreeList[2], h 3 − > F l i n k = F r e e L i s t [ 2 ] \textcolor{orange}{h3->Flink=FreeList[2]} h3−>Flink=FreeList[2], h 3 − > B l i n k = h 1 \textcolor{orange}{h3->Blink=h1} h3−>Blink=h1,完全符合堆块在堆表中的组织结构。
0x4 总结
本章主要介绍了堆的数据结构,堆的操作过程、原理和堆的调试方法以及细节,为后续攻击堆打下了基础。