1

简而言之,问题是这样的。我正在编写内核模式 Windows 驱动程序,当加载内核模式 DLL(或其他可执行模块)时会收到通知。在某些情况下,我必须拦截 DLL 入口点例程。也就是说,覆盖它以便首先调用我的例程,然后我可以将控制权传递给原始入口点。

在 32 位(确切地说是 x86)上,这样做没有问题。我得到了模块基映射地址,它实际上以标准 PE 头(由 Windows 可执行文件使用)开头。DLL 入口点有一个 RVA(相对于映像库的地址)。我只是用我的例程地址减去模块基地址来覆盖它。瞧!

现在,64 位的情况更加复杂。问题是 RVA 仍然是 32 位整数。这样的 RVA 覆盖了从映像基地址开始到 4GB 偏移量结束的地址范围。引用同一个可执行模块中的任何符号都没有问题(假设它不超过 4GB 大小),但这会给跨模块拦截带来问题。自然地,我的可执行模块和我试图挂钩的模块不必落入相同的 4GB 范围内,因此存在问题。

暂时我通过无条件地覆盖原始例程序言代码jmp到我的代码中来解决这个问题。这在 64 位平台上需要 12 个字节。然后,为了从我的例程中调用原始代码,我恢复了被覆盖的 12 个字节(意味着 - 我在覆盖之前保存它们)。

到目前为止 - 没有问题。但是现在情况正在发生变化,我将不得不支持对入口点例程的多线程访问(请不要问为什么,它与加载到所谓的“用户空间”中的多会话DLL有关,单独对于每个终端会话)。

解决方案之一是使用全局锁,但我想避免这种情况。

我知道所谓的“蹦床功能”,但我也想避免这种情况。这样做需要对函数序言代码进行运行时解码,以正确识别指令边界和可能的分支。

最近我想到了另一个想法。如果我能找到原始 DLL 的一些“不需要的”部分,它的长度至少为 12 个字节(大小为mov RAX addr+ jmp RAX)怎么办。然后这部分可能会被jmp我的手中覆盖。然后可以将入口点 RVA 设置为此部分!

要使其工作,只需要可以覆盖的适当部分。我想有这种可能性,因为 PE 标头包含许多几十年来不再使用的历史字段。

这个想法值得尝试,还是这是一种众所周知的技术?安迪还有其他建议吗?

提前致谢。

4

1 回答 1

2

你有几个选择。不幸的是,您只能从这 3 个中选择 2 个: 100% 固体;易于实施;便宜的。

在 .TEXT 部分的末尾,您很有可能会发现未使用的空间。这是因为 Windows 以 4k 的块将图像部分映射到内存中,并且通常 .text 部分不是精确的乘法。

另一个易于实现的是使用 PE 标头。一个非常安全的覆盖区域是 DOS 存根。问题在于不能保证 PE 标头与入口例程位于同一部分(Microsoft 链接器将其放在同一部分,但不了解 GNU 或其他)。

另一个简单但仅适用于系统 DLL 的方法是执行“热补丁”正在执行的操作,并在每个函数前面重用设置为“nop”的 15 个字节和“mov edi,edi”指令。这是所有随 Windows 一起发布的 DLL 的情况,以支持热补丁。

可靠但困难的选择是按照@David Heffeman 的建议去做。这种技术称为“登陆函数”,您将前 12 个字节复制到登陆函数中,然后该函数将跳转到原始函数。

简单可靠的选择是使用 MS Detour。Microsoft Detour 是 Microsoft Research 的一款产品,它完全可以做到这一点,并且效果很好,并且受到支持,它可以处理可能出现的一堆极端情况和竞争条件(以及其他东西),它的 x86 版本是开放的资源。缺点是商业用途非常昂贵 - 我上次检查它是 10k。

于 2012-11-02T09:34:31.000 回答