如何在RtlAllocateHeap中获取或存储堆大小,以便稍后在RtlFreeHeap中使用

How to get or store heap size in RtlAllocateHeap for later use in RtlFreeHeap?

本文关键字:RtlFreeHeap 获取 RtlAllocateHeap 存储      更新时间:2023-10-16

核心问题:除非在Windows XP兼容模式下运行,否则旧的Windows XP游戏在新的Windows版本上会冻结。

即使不一定要为此提供代码修复,我仍然喜欢为这个冻结进行修复,因为没有用户经常遇到这个问题,每次都会问他们如何修复它。当然,应用程序可执行文件没有源代码,所以调试冻结不是直接的。然而,由于WindowsXP兼容模式"修复"了它,我想知道如何修复。

所以我在网上查了一下,发现有一个叫Shim Engine的东西,它通过AppHelp.dll提供了各种修复程序。我安装了Windows Compat Toolkit,并找到了一个叫FaultTolerantHeap的Shim可以删除应用程序冻结。到目前为止还不错。

关于FTH的信息不多,但这段与微软Calinoiu先生的视频充分说明了他们的所作所为以及FTH的4个核心战略:

  • 总是将内存归零
  • 为每个分配追加8个字节以帮助缓冲区溢出
  • 延迟堆释放
  • 在正常关机时禁用Free'ing

如视频中所述,这是在ntdll中的RtlAllocateHeapRtlFreeHeap等函数上完成的。所以我想尝试一下。

下一步,我将这两个功能都与Microsoft Detours挂钩,并顺利访问。我将调用从挂钩函数直接重定向到ntdll中的原始函数。比赛进行得很好,勾手很好。

然后,我尝试了清零记忆:冻结仍然会发生。向每个堆分配追加8个字节:冻结仍然会发生。禁用释放内存:冻结已消失!啊哈!

因此,微软FTH的延迟释放似乎是冻结的"解药"。因此,下一步是实现延迟免费。我们需要的是一个队列,它接受的最大字节数,然后下一次释放将清除并释放队列中的第一个条目。Microsoft为其队列使用4MB缓冲区。所以我可以做我想的同样的事情。但我有一个大问题:我需要跟踪将被释放的堆分配的大小,否则就无法判断我的队列有多大。此外,我不想对超巨大的堆进行排队,例如位图图像的缓冲区非常大。

对于微软的FTH,他指出他们在每个堆分配的最后都会附加他们的神奇字节。就像30个字节+8个字节的填充。相当多我想我可以通过添加一些4字节的值来简化它,作为一种"刚刚好"的模式,从我的RtlAllocateHeap挂钩中识别一个堆(因为我在运行时挂钩),再添加4个字节来存储每个分配的大小,这样我就可以在分配到达RtlFreeHeap时访问它。当前代码:

#define FTH_MAGIC_BYTES 0xFEDCBA98ul
#define FTH_ALLOC_PADDING 0
...
static PVOID __stdcall RtlAllocateHeap_hook(PVOID HeapHandle, ULONG Flags, SIZE_T Size)
{
if(Size==0)
{
return RtlAllocateHeap_orig(HeapHandle, Flags, Size);
}
// For our allocations we assume 32bit application:
// treat everything with size of 4 bytes
Size += 8+FTH_ALLOC_PADDING;
PVOID HeapBase = RtlAllocateHeap_orig(HeapHandle, Flags, Size);
if(HeapBase)
{
*(uint32*)((uint32)HeapBase+4) = FTH_MAGIC_BYTES;
*(uint32*)((uint32)HeapBase) = Size;
return (PVOID)((uint32)HeapBase+8);
}
return HeapBase;
};

static BOOL __stdcall RtlFreeHeap_hook(PVOID HeapHandle, ULONG Flags, PVOID HeapBase)
{
if(HeapBase==0)
{
return RtlFreeHeap_orig(HeapHandle, Flags, HeapBase);
}
if(*(uint32*)((uint32)HeapBase-4)==FTH_MAGIC_BYTES)
{
HeapBase = (PVOID)((uint32)HeapBase-8);
}
return RtlFreeHeap_orig(HeapHandle, Flags, HeapBase);
};

它在启动时使应用程序崩溃。有趣的是,当我运行Debug时,它并没有崩溃。发布时确实如此。我在Visual Studio中逐步完成了程序集,但没有发现任何问题。我还监视了新分配的字节,看看字节是否写在堆中——它们在我期望的位置。然而,它抛出了应用程序,我不明白为什么。当我在返回RtlAllocateHeap_hook时不更改HeapBase的偏移量时,代码将运行得很好。

if(HeapBase)
{
//*(uint32*)((uint32)HeapBase+4) = FTH_MAGIC_BYTES;
//*(uint32*)((uint32)HeapBase) = Size;
return HeapBase;
}

当我带着偏移量返回时,应用程序又开始崩溃了。我可以在末尾存储我的字节,然后返回HeapBase而不带偏移量,但当我们到达RtlFreeHeap_hook时,找到这种对齐方式将是疯狂的。我想知道微软是怎么做到的,除非他们知道堆的大小。我看到的唯一解决方案是正确使用上述方法,找出如何以Microsoft的方式检索堆的大小,或者尝试挂接malloc或new。

有什么想法吗?也许我错过了一些非常简单的东西:-)

谢谢。

调用堆栈样本:

ntdll.dll!76f030bd()
[下面的帧可能不正确和/或丢失,没有为ntdll.dll加载符号]msvcrt.dll!7498f480()
msvcrt.dll!7498f4e6()
msvcrt.dll!7498f52c()
comctl32.dll!73656c62()
game.dat!00413b90()
game.dat!00413fc8()
msvcrt.dll!7499edc8()
msvcrt.dll!7498a53a()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498a6a9()
msvcrt.dll!7498d255()
msvcrt.dll!7498d272()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498a53a()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498a5d6()
msvcrt.dll!7498dfd5()
game.dat!0041d4ed()
game.dat!006f0048()
game.dat!004af57d()
game.dat!00413b90()
game.dat!00413fc8()
game.dat!00413b90()
game.dat!00413fc8()
game.dat!004140d8()
game.dat!008fa9ad()
game.dat!0041f018()

EAX=0262CE78 EBX=00000000 ECX=7c7 ec8d9 EDX=00000088 ESI=00B70000 EDI=00000000 EIP=76F030BD ESP=001886F8 EBP=00188704 EFL=00210202

7C7EC8E9=????

经过更多的测试和阅读,我在MSDN中看到kernel32.dll也导出了Heap函数。Kernel32有一个函数HeapSize,它返回我们需要的内容。内核函数似乎只是通过ntdll对应的通道。

因此,我怀疑ntdll应该有一个HeapSize的对应项。我查看了ntdll的导出,找到了RtlSizeHeap。MSDN上没有此函数的文档。

在谷歌上搜索从ntiternals.net返回了此页面。因此,您可以使用内核函数HeapSize或ntdll函数RtlSizeHeap,通过获取其指向GetProcAddress的指针。