【问题标题】:How can I safely unhook a Win32 API that blocks?如何安全地解开阻止的 Win32 API?
【发布时间】:2016-09-28 23:05:18
【问题描述】:

我有一个application,它执行一堆 API 挂钩到一个 Win32 目标应用程序(它使用 ASIO)来观察目标处理的命名管道流量。它使用 ReadFile 或 WriteFile 调用设置事务的开始,如果调用是同步进行的,则获取该调用的结果。如果调用是异步的,它还会通过到 GetQueuedCompletionStatus 的挂钩来捕获该调用的结束。检索到的数据被发送到我自己的 ASIO 线程池,该线程池为与 Wireshark 的命名管道连接提供服务。其实挺甜的。

一切都很好,除非我必须让程序恢复到原来的状态。取消挂钩功能可以正常工作,除非我强制目标应用程序通过命名管道处理大量请求,否则可以在 GetQueuedCompletionStatus 中无限期阻止现有调用。如果我不等待这些线程解除阻塞,当 GetQueuedCompletionStatus 最终解除阻塞并返回到不再存在的代码洞穴时,我将导致 AV。

我尝试的另一件事是跟踪对 GetQueuedCompletionStatus 的每次调用,并让挂钩函数在 GetQueuedCompletionStatus 的 LpOverlapped 参数与对 PostQueuedCompletionStatus 的相应调用匹配时通知信号。当然,这会解除对所有内容的阻塞,但会严重破坏调用 GetQueuedCompletionStatus 的代码,从而导致 AV。

有谁知道处理这个问题的好方法吗?如果我可以执行以下操作之一,这将起作用:

  • 使用 ASIO 将忽略的 PostQueuedCompletionStatus 进行调用
  • 捕获 PostQueuedCompletionStatus 进行的虚拟调用并覆盖堆栈中的返回值
  • 创建一个半永久性代码洞穴,其中包含一个用于挂钩阻塞调用的蹦床,它将函数指针访问器传递给挂钩调用。 trampoline 函数将看到,如果该值不为 null,它将改为调用该函数指针,而不是将执行返回给调用者。在解除阻塞调度后,挂钩代码可以将该函数指针设置为 GetQueuedCompletionStatus 的地址,然后我们就能够将正确的调用结果返回给调用者。

第一个选项很简单,但如果能够将此应用程序概括为与其他目标一起使用会很好。第二个很容易,除了我想尽可能安全地编写代码。最后一个可能是可行的,我只是不想用代码洞穴污染外部内存空间。

【问题讨论】:

  • 钩子函数可以自己解钩吗?如果是这样,它可以跟踪它被输入了多少次,并响应一个全局的“unhook”标志或信号,当入口计数下降到 0 时将它自己解开。很难让它完全线程安全不过。
  • OP:首先,您的标题和第二段错误且具有误导性。脱钩没有问题。您的问题是“我如何确定何时可以安全地卸载函数用作挂钩的 DLL”或类似的问题。请编辑它。要取消挂钩当前被阻止的功能,您只需取消挂钩即可。对于这个问题,最简单、最安全的解决方案可能就是保持 DLL 处于加载状态。
  • @conio:这很公平。

标签: c++ winapi boost-asio api-hook


【解决方案1】:

我怀疑是否存在通用且安全的解决方案——除了简单地保持 DLL 加载之外。或者首先避免所有这些钩子......(跳到答案的末尾以获取主要破坏者!)


该论点与我在对问题的评论中所写的内容一致。

要释放 DLL,您必须确保当前没有人正在执行 DLL 中的代码,并且没有人会执行 DLL 中的代码(除了执行我们正在讨论的拆卸线程)。

代码可能会因为两个原因在 DLL 中执行,要么我们将要调用它,要么我们要返回它(或者返回要返回 DLL 的代码,等等。 )。假设我们通过脱钩解决了第一个问题。我们还剩下第二个原因。也许我们已经陷入困境。

你可能会想到做这样的事情:

  1. 分配一个“代码洞穴”来做类似的事情

    PreHookWriteFile:
    LOCK INC [ref_count]
    POP R15
    CALL HookWriteFile
    PostHookWriteFile:
    LOCK DEC [ref_count]
    JMP R15
    
  2. JMP [PreHookWriteFile]挂钩WriteFile

  3. 在解除 WriteFile 挂钩并等待重新计数归零的专用线程中执行释放。然后它会释放代码洞穴。

但是这样做有两个问题。首先,我们真的没有地方存储原始退货地址。我们不能把它放到堆栈上,因为HookWriteFile 不希望在那里看到它,而且我们不能将它存储在任何非易失性寄存器中,因为我们需要在PostHookWriteFile 之前恢复它跳回来,但问题是我们没有地方存放东西。

其次,释放线程可能会在跳转发生之前注意到减少,并提前释放代码洞穴。

我认为我们尝试变得多么聪明(使用__declspec(naked) 函数或通过修改原型)让钩子函数期望堆栈上的原始返回地址并不重要。第二个问题仍然存在 - 您在返回之前减少了引用计数。在您返回之前删除代码是不安全的,但您无法通知您的返回,因为在您返回后您不再可以控制。这正是创建 FreeLibraryAndExitThread 的原因。

人们可能会想到一些疯狂的事情,比如暂停 DLL 中的所有线程并遍历它们的堆栈以确保 DLL 中没有代码在其中任何一个中,但这只会打开另一个蠕虫罐。


但也有好消息。

如果您真正想要的是监控命名管道通信并且您愿意将自己限制在 Windows 8 和更新版本中,那么有一个解决方案更健壮、记录和支持,需要的代码行数要少得多,而且不是侵入性的。 Koby Kahane 写了一个filesystem mini-filter that does exactly that

它的输出是 ETW 事件,您可以在 Microsoft Message Analyzer 或使用来自基于清单的提供程序的 ETW 事件的其他工具之一中查看。如果您真的需要在 Wireshark 中查看它,您可以编写一个小型 ETW 消费者来消费事件并通过管道将它们发送回 Wireshark。它仍然会比所有这些钩子更容易,而且肯定更安全且不那么混乱。

【讨论】:

  • 此捕获 DLL 所做的一件事是通过命名管道连接将数据作为虚拟 TCP 会话发送到 Wireshark。这是我需要的,因为我有一个 Wireshark 插件来剖析协议。如果可以将 Microsoft 消息分析器转换为原始数据,我想我可以编写一个转换为 pcap 格式的转换器。感谢您的建议。
  • @HarryJohnston:你不能直接跳转到InterlockedDecrement。 OP 正在挂钩 Win32 API 函数,这些是 stdcall。更改堆栈上的返回地址很好,但是在返回期间清理堆栈呢?例如InterlockedDecrementRETN 4 结尾,而CreateFileWRETN 1C 结尾。 (另请注意,没有 extra 调用框架。代码 替换 返回地址并跳转到钩子。但由于我不能使用 ESP 作为间接访问的一部分MOV 指令我正在执行 POPPUSHJMP,其中 PUSHJMP 浓缩为 CALL。)
  • 再想一想,虽然 可能 可以对正在退出的线程做一些事情,但我认为没有任何明智的方法来处理其中之一的可能性线程已经跟随跳转到代码洞穴,但还没有增加计数器。哦,好吧,这是一次有趣的思想实验!
  • 我不明白您在说什么以及它与您之前的评论有何关系。没有什么能阻止我做任何事。我可以为所欲为。但有什么意义呢? “标准”方法是通过跳转到钩子函数来修补钩子函数的开头,以便钩子函数具有与钩子函数完全相同的原型并直接访问其参数。你想做点别的吗?前进。但是有什么意义呢?
  • 你可以随心所欲地复杂化它,但是请 ELI5 跳转到InterlockedDecrementRETN 4 如何正确地从堆栈中清除 0x1C 字节的参数。您可以做的最接近的事情是手动(可能使用内联汇编)在跳转到InterlockedDecrement 之前重新排列堆栈,但这不是通用 解决方案。您必须分别为每个钩子函数破解它。
猜你喜欢
  • 1970-01-01
  • 2012-01-03
  • 2011-12-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-05-24
相关资源
最近更新 更多