Windows堆学习

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*82字节
Prev_size以本身相邻的前一个块大小(包括它的块头),实际上存的是前一个堆块在堆表中的偏移。实际大小=Prev_size*82字节
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被释放进入了堆表后,堆管理器会根据此时堆表的情况往释放的chunkData域中写入FlinkBlink。堆管理器返回给用户使用的内存其实隐去了块首部分,因此我们感觉不到。这个可以在我们连续分配多块内存的时候,通过观察相邻地址之间的偏移大小发现。

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类:

  1. 从快表中分配
  2. 从0号空表中分配
  3. 从普通空表中分配

假设执行以下代码:

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

  1. 如果是从快表中分配就是寻找大小匹配的chunk,并将其空闲状态修改为使用状态,然后把它从堆表中“卸载”下来,最后返回一个指向堆身的指针给用户使用。

  2. 如果是从0号空表中分配就是先从 f r e e l i s t [ 0 ] \textcolor{orange}{freelist[0]} freelist[0]反向匹配大小,如果大于要求的size再从正向匹配,采用这种二分查找的思想直到找到符合sizechunk。理想情况下是正好找到一个大小相等的chunk,而常常可能是得到略大于要求sizechunk

  3. 如果是普通空表分配就是现寻找最优的空闲分区分配,若失败再找次优的空闲分区分配。

  4. 如果只能找到次优的分配方案时,即找到的chunk略大于要求的size,假设为0x24。则堆管理器会从0x24中割出0x16给程序使用,并将剩下的0x8chunk重新定位堆表,并链入。

注 : 快 表 是 精 确 分 配 的 , 并 不 会 发 生 合 并 ! \textcolor{green}{注:快表是精确分配的,并不会发生合并!}

2.释放

将释放的chunk的状态改为空闲,链入相应的堆表。所有释放的chunk均从末端链入,分配时也是先从末端拿取。

3.合并

经过多次的分配和释放,堆区必然会“千疮百孔”,产生很多内存碎片。为了高效利用内存,堆管理器便会对相邻的空闲堆块进行合并操作。这个过程为:

将这两块空闲chunk从原来的空表中“卸下”,合并这两个chunk得到一个大的chunk,调整大chunk的块首,最后将答chunk链入相应的空表。

Windows堆管理器合并chunk具体分为三种情况:

  1. 小块: s i z e < 1 K B \textcolor{orange}{size < 1KB} size<1KB
  2. 大块: 1 K B < = s i z e < 512 K B \textcolor{orange}{1KB <= size < 512KB} 1KB<=size<512KB
  3. 巨快: 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>justintime 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:亲历了两个坑,这里记录一下。

  1. 不能设置试试调试,即just-in-time按钮是灰的。此时应该检查以下选项是否打开

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

  2. 设置实时调试成功后,运行程序调试器没有自动启动并附加目标进程。此时应该修改注册表位置:

    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

调试堆与正常堆不同的地方有:

  1. 调试堆不使用快表,只用空表分配
  2. 所有堆块都会追加16个字节用来防止溢出(防止程序溢出,而不是堆溢出攻击),包括8个字节的 0 x A B \textcolor{orange}{0xAB} 0xAB8个字节的 0 x 00 \textcolor{orange}{0x00} 0x00
  3. 堆首的标志位不同

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块堆和系统实际分配的结果

堆句柄用户请求的大小(字节)系统实际分配大小(字节)系统得到的堆指针用户得到的堆指针
h13160x3a06800x3a0688
h26160x3a06900x3a0698
h38160x3a06a00x3a06a8
h412320x3a06b00x3a06b8
h524480x3a06c80x3a06d0
h632640x3a06e80x3a06f0

可见操作系统分配堆的大小是以8字节对齐的,例如h1h2h4,用户请求的大小均不是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=0x9800x8f0=0x103+0x18+0x20+0x28

再经过3次释放操作,此时堆区的信息如下图:

在这里插入图片描述

空表中的情况:

在这里插入图片描述

用户释放的顺序是h1h3h5,而h1h3是同属一个空表的,他俩链入空表的顺序是最后释放的放在最前面。并且 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 总结

本章主要介绍了堆的数据结构,堆的操作过程、原理和堆的调试方法以及细节,为后续攻击堆打下了基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值