Windows 10 x86/wow64用户态堆
引言
在过去几周中,我多次接到亲友的"紧急"电话,称其计算机出现异常并被升级到Windows 10。虽然可以讨论强制升级策略,但显然Windows 10正在获得市场份额,因此研究其堆管理机制变得尤为重要。
本文汇集了关于Windows 10中32位进程用户态堆管理器行为的笔记,重点记录与Windows 7的异同,并探讨如何操纵堆以提高行为可预测性。特别关注以下问题:
- 后端分配器的行为特征
- 激活LFH的条件
- Win7与Win10的LFH行为差异
- 创建特定堆布局的技术(相同/不同大小对象的相邻内存分配)
- 可靠精确堆喷洒的实现
需要说明的是,这些观察结果基于有限测试,并非通过反编译ntdll.dll获得。希望这些笔记能激励更多人进行测试和逆向工程研究。
测试环境
- Windows 10 Enterprise x64(VirtualBox虚拟机)
- Visual Studio Express 2015
- WinDBG for Windows 10
- 符号服务器配置:
srv*c:\symbols*http://msdl.microsoft.com/download/symbols
测试代码可在https://github.com/corelan/win10_heap获取。
堆结构分析
Windows 10堆位于ASLR影响的随机地址,以头部结构开始。使用dt _HEAP
命令可查看堆头字段,其中:
- 偏移0x4c(EncodeFlagMask)和0x50(Encoding)存储块头编码信息
- 偏移0x5c(VirtualMemoryThreshold)值为0xfe00(0x7F000字节)
- 偏移0xd6(FrontEndHeapType)值0x2表示使用LFH前端分配器
LFH头部结构(dt _LFH_HEAP
)包含桶、子段等管理信息。与Win7不同,Win10的LFH头部通常存储在堆首段之外。
后端分配器(BEA)行为
BEA_Alloc1测试
分配两个0x300字节块,观察分配位置和合并行为。结果显示块从段尾空闲空间分割获得,释放后会发生合并。
BEA_Alloc2测试
分配10个0x300字节块后释放最后一个,然后分配两个0x100字节块。发现精确大小的空闲块优先被重用,较大块会被分割使用。
BEA_Alloc3测试
通过精心布局实现0x58字节块的精确回收:用0x100字节块包围目标块,避免合并,然后通过相同大小分配重新占用释放的空间。
BEA_Alloc4测试
尝试用0x80字节分配覆盖0x58字节块的前4个双字。通过合并两个相邻释放块创建大空闲块,然后用大块分配覆盖目标区域。
前端分配器(LFH)行为
LFH激活条件
测试表明仍需18次连续分配才能激活LFH,且以下操作不影响触发计数:
- 中间分配不同桶大小的块
- 中间释放不同大小的块
- 释放同桶大小的块
LFH分配特性
- 块在子段内不再连续分配
- LIFO行为不再稳定,回收释放块所需分配次数变化较大(0-50次)
- 最大块大小限制仍为0x4000字节
LFH空间回收
通过释放整个子段中的所有块,可以回收内存页用于其他桶大小的分配,实现跨桶大小的空间重用。
大块分配
VirtualAllocdBlocks
分配超过VirtualMemoryThreshold(0x7F000字节)的块会触发VirtualAllocdBlocks机制。这些分配位于新页面起始处,但间隔比Win7更大,降低了堆喷洒的精确性。
精确堆喷洒
通过选择合适大小(如0x20000-8或0x40000-8字节)和数量的分配,可以在普通段中获得对齐的连续分配。关键是要避免LFH和VirtualAllocdBlocks机制干扰。
在复杂多线程环境中,可先分配大量大块(如0x1ff00字节)后保留小块防止合并,然后释放大块为应用程序提供"噪声"内存,保护喷洒区域不受干扰。
结论
Windows 10堆管理在保持基本机制的同时,在细节行为上与Windows 7存在显著差异:
- LFH激活条件不变但内部分配更随机
- BEA行为基本一致但需要更精细的控制
- 大块分配间隔增大影响喷洒精度
- 通过精心设计的分配策略仍可实现可靠堆操作
这些发现为Windows 10漏洞利用技术开发提供了重要参考。