65.9K
CodeProject 正在变化。 阅读更多。
Home

API挂钩的实现(第一部分)

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.27/5 (10投票s)

2005 年 1 月 18 日

CPOL

5分钟阅读

viewsIcon

120540

downloadIcon

3445

本文提供了一个非常简单但高效的 API 函数钩子基础代码,可用于任何应用程序。

Sample Image - APIHookingRevisited.jpg

引言

网上有很多关于如何在远程进程中钩子 API 函数的代码。那么,为什么要再写一个呢?

本文是我正在开发的一个工具的第一部分(I)。这些工具不会太复杂,因此不需要像大多数其他代码那样庞大的类层次结构,也不需要硬编码的汇编代码。这个工具只是一个技术预览,展示了通过 快速而粗糙的 “把你的函数放在这里” 块可以实现什么。

在你阅读之前

好的,说清楚点,当你打开项目并查看代码时,你会发现它不是很完善。它编译时会有警告,注释很少(与生产代码相比)。但是,它在 WinXP 和 2003 平台上可以编译并干净地运行(应该也可以在 Win2K 下运行,但我没检查过)。

背景

我过去曾开发过一个使用 DirectShow 的软件。不幸的是,我在一个简单的同步对象上遇到了问题,导致应用程序死锁。我仍然认为开发者可能会害怕两件事。

  1. 第一件是“内存泄漏”。内存泄漏大多数时候是由于疏忽造成的。它们被很好地覆盖了,因为它们很容易(自动)纠正。
  2. 另一件事是“错误处理”。错误处理乍一看似乎很简单,但大多数时候,总会有“坏情况”导致“坏错误”。

在软件中错误处理不当会导致崩溃或逻辑混乱的状态。与物理状态相比,很容易预测并创建一个处于稳定状态的系统。然而,达到/离开这个稳定状态很难预测,因为错误/线程调度顺序/内存使用/黑暗现象可能会改变参数。从不稳定状态恢复非常困难。

我无法对此提供一个全局的解决方案(如果有人能,请给我发邮件)。所以,让我们看看通常的解决方案。

  • 错误返回:永远不要期望函数总能成功。有无数关于这些问题的文章。
  • 线程:这是整个项目的课题。
  • 内存使用:通过重写 malloc/free/new/delete 函数/运算符来监控分配的内存,这通常就足够了。
  • I/O 状态:您需要建立一个完整的状态图来处理所有情况。

线程是最“奇怪”的事情之一。虽然可以模拟几乎所有的内存条件、I/O 状态和错误返回,但无法模拟线程的执行顺序。

(呃,太长了!)

总而言之,本项目只关注一个问题:多线程环境下的数据同步。本文介绍了一种监控系统函数调用的技术。全局项目的目标是构建一个死锁检测器。当一个线程在保护一个数据块(A)的同时,试图访问另一个受保护的数据块(B)时,就会发生死锁。这个另一个块(B)由另一个试图访问第一个受保护块(A)的线程保护。

那么,为了实现这个目标,让我们把项目分成几个小步骤:

  • 第一部分。钩子系统函数。
  • 第二部分。拦截所有对同步函数的调用。
  • 第三部分。构建死锁检测逻辑。
  • 第四部分。[外观]使用映射文件+堆栈地址获取源代码[/外观]。

第一部分。钩子系统函数

我强烈建议您阅读 Anton Bassov 的文章《Process-wide API spying - an ultimate hack》和 Ivo Ivanov 的文章《API Hooking system》。

我不会深入探讨第 N 次关于如何将代码注入进程的解释。但我会解释我所做的选择,以及我如何让它为我服务。这个想法是,将一个 DLL 注入一个远程进程,以便该 DLL 执行函数劫持。在这个项目中,该 DLL 被称为 ThreadSpy.DLL。当该 DLL 被创建时,DllMain 函数会以 ProcessAttach 的原因被调用。在这种情况下,它会解析 ThreadSpy.cpp 中定义的 HookStruct 数组,并将给定的 Win32 API 函数替换为 DLL 提供的函数。

钩子函数定义在 Hooked.cpp 源文件中。然后,每个已加载模块(如 Kernel32.DLLUSER32.DLLGDI32.DLL 等)的导入表都会被更新,以用提供的函数地址替换原始函数地址。

然后,服务器可以注入这个 DLL 到一个正在运行的进程中,或者自己创建一个进程。服务器在 ThreadDLD 项目中,它只是一个 WTL 应用程序,它将接收来自被钩子进程的消息,并将 DLL 注入到被钩子进程中。我选择使用 CreateProcess 函数和 CREATE_SUSPENDED 标志来创建被监控的进程。这样,我就可以在任何其他 DLL 加载之前(实际上是在 NTDLL 和 Kernel32 等关键 DLL 加载之后)轻松地注入我的 DLL。然后服务器恢复线程,并准备从 DLL 接收消息。

为了拦截被钩子进程中任何新加载的 DLL,需要拦截 LoadLibraryGetProcAddress 函数。这就是为什么这些函数总是被钩子。

我不会在本文中讨论如何将消息发送到服务器,因此我已将演示和源代码中的相关代码删除。通过这个演示,您可以通过更改 ThreadSpy.{h,cpp}Hooked.cpp 中的函数来轻松实现自己的 API 钩子。这样,您就可以通过添加一个 MyCreateFile 函数来轻松监控谁在尝试创建文件(并在凭据不足时拒绝创建)。

在这个例子中,我选择钩子 TextOutATextOutW,这样在视觉上就很容易区分。

致谢

正如我之前所说,请阅读:

未来...

在项目的第二部分,我将尝试解释如何拦截所有必需的函数来监控线程的创建/销毁和同步对象,并将这些信息通信给服务器。本文更像是正文的序言。第二部分在这里:线程死锁检测(第二部分)

© . All rights reserved.