如何在 RtlAllocateHeap 中获取或存储堆大小以供以后在 RtlFreeHeap 中使用?

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

核心问题:一个旧的 Windows XP 游戏在更新的 Windows 版本上冻结,除非它是 运行 具有 Windows XP 兼容模式。

即使没有必要为此提供代码修复,我仍然想修复此冻结,因为 noobie 用户 运行 经常遇到这个问题,每次都询问他们如何才能修理它。应用程序可执行文件当然没有源代码,因此调试冻结并不直接。但是,由于 Windows XP 兼容模式 "fixes" 它,我想知道如何。

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

关于这个 FTH 的信息不多,但是 this video 来自 Microsoft 的 Calinoiu 先生充分介绍了他们所做的事情以及 FTH 中的 4 个核心策略:

如视频中所述,这是对来自 ntdll 的 RtlAllocateHeapRtlFreeHeap 等函数完成的。所以我虽然试了一下。

下一步,我将这两个函数与 Microsoft Detours 挂钩并顺利访问。我将调用从我的钩子函数直接重定向到 ntdll.h 中的原始函数。游戏 运行 很好,钩子很好。

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

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

对于微软的 FTH,他说他们在每个堆分配的最后附加了他们的魔法字节。像 30 字节 + 8 字节填充。非常多。我想我可以通过添加一些 4 字节值作为一种 "just good enough" 模式来简化它,以从我的 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);
};

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

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

一旦我 return 有了偏移量,应用程序就会再次崩溃。我可以在末尾存储我的字节,然后 return HeapBase 没有偏移量,但是当我们点击 RtlFreeHeap_hook 时找到那个对齐将是疯狂的。我想知道微软是怎么做到的,除非他们当然知道堆的大小。我看到的唯一解决方案是正确使用上述方法,了解如何以 Microsoft 方式检索堆的大小,或者尝试挂钩 malloc 或 new。

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

谢谢。

调用堆栈示例:

ntdll.dll!76f030bd()
[Frames below may be incorrect and/or missing, no symbols loaded for 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 = 7C7EC8D9 EDX = 00000088 ESI = 00B70000 EDI = 00000000 EIP = 76F030BD ESP = 001886F8 EBP = 00188704 EFL = 00210202

7C7EC8E9 = ????

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

因此我怀疑 ntdll 应该与 HeapSize 对应。我查看了 ntdll 的导出,发现 RtlSizeHeap。此函数未在 MSDN 上记录。

对 google 的搜索返回了 this page from ntinternals.net。因此,您可以通过使用 GetProcAddress.

获取其指针来使用内核函数 HeapSize 或 ntdll 函数 RtlSizeHeap