Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
UnhookMe — 动态Windows API解析器和反钩子工具,可检测并恢复被钩挂的函数(IAT、EAT、内联补丁),以便从红队恶意软件中调用未被监控的系统调用。 | Kitploit
工具/GitHubGitHub/mgeeky/unhookme
防御工具红队Payload 开发
GitHubmgeeky/unhookme

UnhookMe

动态Windows API解析器和反钩子工具,可检测并恢复被钩挂的函数(IAT、EAT、内联补丁),以便从红队恶意软件中调用未被监控的系统调用。

查看仓库
348484年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

UnhookMe - 动态解除挂钩的导入解析器

在侵入性防病毒软件和EDR引入热补丁以增强其监控能力的时代,现代对手必须拥有强大的工具来绕过这些看守。这里提出的动态导入解析器实现,能够即时解除所用函数的挂钩,是加强对手韧性努力的又一步。

我在这里提出的解决方案是,从使用链接器解析的WinAPI导入(在编译后可执行文件的PE头中可见,特别是导入地址表)转向完全动态的方法,仅以动态方式解析导入。这种动态解析器可以配备在后台运行的解除挂钩逻辑,无需操作员任何指导。


最简单的使用示例

以下是如何确保以解除挂钩、不受监控的方式调用MessageBoxW:

root@kitploit:~
    RESOLVE(user32, MessageBoxW);
    _MessageBoxW(0, L"看!我已经解除挂钩了!", L"第三个 - 已解除挂钩", 0);

所有魔法都发生在RESOLVE宏定义中,它构造了一个名为_MessageBoxW的ImportResolver<T>对象。


展示

Unhookme showcase animation

以下是UnhookMe示例的工作方式:

  1. 它首先显示一个未受挂钩影响的MessageBoxW
  2. 然后我们自己挂钩MessageBoxW的前导码,使其始终返回0而不显示消息
  3. 最后,我们使用UnhookingImportResolver解析器动态解析MessageBoxW,该解析器会检测应用的前导码补丁,并恢复原始字节,从而有效解除MessageBoxW功能的挂钩。

在弹出消息框的同时,控制台标准输出会打印以下日志行:

root@kitploit:~
[~] 已解析符号 kernel32.dll!CreateFileA
[~] 已解析符号 kernel32.dll!ReadProcessMemory
[~] 已解析符号 kernel32.dll!MapViewOfFile
[~] 已解析符号 kernel32.dll!VirtualProtectEx
[#] 在符号中发现蹦床挂钩: MessageBoxW 。已从文件恢复原始字节。
[~] 已解析符号 user32.dll!MessageBoxW

如何使用?

总共有5个C++源代码/头文件需要您的解决方案包含。但您的主程序文件只需包含以下两个头文件,如下所述。

  • resolver.h - 包含大部分UnhookingImportResolver实现和便捷宏定义的头文件
  • resolver.cpp - 定义了全局选项的源代码
  • usings.h - 一个庞大且麻烦的头文件,包含数十个常用WinAPI的using类型定义
  • PE.cpp - 自定义PE解析器源代码文件
  • PE.h - 自定义PE解析器头文件

必需的头文件

您的程序只需包含两个头文件:

root@kitploit:~
#include "usings.h"
#include "resolver.h"

全局选项

有一些可以更改的全局选项,它们会影响解析器的工作方式或输出。这些选项定义在**resolver.cpp**文件的开头:

解析器全局选项:

  • globalQuietOption - 设置为true以关闭所有输出
  • globalVerboseOption - 设置为true以显示详细的详细输出
  • globalAntiSplicingOption - 如果函数被挂钩,则解除其挂钩。
  • globalLogFilePath - 输出日志的目标路径。如果为空,则使用标准输出。
root@kitploit:~
bool globalQuietOption = false;
bool globalVerboseOption = true;
bool globalAntiSplicingOption = true;

wchar_t globalLogFilePath[MAX_PATH] = L"";

自定义API类型规范

为了使用解析器,必须首先使用严格形式的using语句声明函数指针类型:

root@kitploit:~
    using fn_FunctionName = ReturnType WINAPI (
        ParamType1 paramName1,
        ...,
        ParamTypeN paramNameN,
    );

此仓库附带**usings.h**头文件,其中包含数十个流行Windows API的预定义using类型。

FunctionName 将对应于我们想要让ImportResolver解析的WinAPI,该函数指针必须标记为具有WINAPI调用约定(x86上为__stdcall,x64上为__fastcall)。ReturnType 必须位于WINAPI类型修饰符之前。

函数解析与使用

在定义了如上所述的函数指针类型后,我们可以按以下方式使用它:

root@kitploit:~
    RESOLVE(libraryName, FunctionName);
    ReturnType output = _FunctionName(param1, ..., paramN);

宏RESOLVE负责实例化ImportResolver模板对象,并调整指定库的名称。

解析器引入了多个宏定义,以提供在各种情况下轻松调用的构造函数调用:

root@kitploit:~
#define RESOLVE(mod, func)                    RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, ::globalAntiSplicingOption)
#define RESOLVE_NO_UNHOOK(mod, func)          RESOLVE_PARAMETERIZED(mod, func, ::globalVerboseOption, false)

#define RESOLVE_VERBOSE_UNHOOK(mod, func)     RESOLVE_PARAMETERIZED(mod, func, true, true)
#define RESOLVE_VERBOSE_NOUNHOOK(mod, func)   RESOLVE_PARAMETERIZED(mod, func, true, false)
#define RESOLVE_NOVERBOSE_UNHOOK(mod, func)   RESOLVE_PARAMETERIZED(mod, func, false, true)
#define RESOLVE_NOVERBOSE_NOUNHOOK(mod, func) RESOLVE_PARAMETERIZED(mod, func, false, false)

解析器的构造函数:

root@kitploit:~
    template<typename Ret, typename ...Args>
    ImportResolver<Ret WINAPI(Args...)>(
            std::string dllName,
            std::string funcName,
            bool _verbose = false,
            bool _unhook = false,
            bool *_wasItHooked = nullptr
        )

它是如何工作的?

底层解析器利用自定义PE头解析器,该解析器处理每个引用的DLL模块以映射其导出,并验证模块PE头的完整性以及所引用函数存根字节的完整性。

思路如下:

  1. 首先,我们调用LoadLibrary来加载用户引用的库(由RESOLVE宏的第一个参数指定),如果无法通过GetModuleHandle访问它。
  2. 然后,我们处理已加载/引用的库的PE头,映射其导出,检索导出地址数组,并手动计算这些地址以进行交叉验证。
  3. 如果DLL导出地址表中定义的例程地址与我们预期的地址不符,则认为该导出被EAT挂钩。同样,如果可执行文件导入地址表(IAT)中该函数的条目被更改,不再指向DLL代码段中的正确位置,则认为该函数被IAT挂钩。
  4. 假设到目前为止未发现挂钩,我们获取函数前导码的前N个字节,并将其与磁盘上DLL文件中的内容进行比较。如果内存中的字节与文件中的字节不一致,则认为该函数被内联修补(热修补)。
  5. 如果函数被认为已挂钩,我们返回原始的导出地址(我们自己计算的地址)并/或解除该条目的挂钩。如果存在补丁字节,我们将恢复它们。
  6. 最后,为了优化解析器的性能影响,我们缓存所有已加载模块的映像基地址和已解析函数的地址,并在后续命中时从缓存(std::map)中返回它们。

这种动态解除挂钩解析器面临的问题之一包括处理转发API(DLL可能包含导出占位符,表明此函数未在此模块中实现,而是在另一个模块中)的问题——尽管此实现支持,但偶尔会破坏其遍历逻辑。


☕ 支持我吧 ☕

这个项目和其他项目都是不眠之夜和大量辛勤工作的成果。如果你喜欢我所做的事情,并感激我始终回馈社区, 请考虑请我喝杯咖啡 (或者更好,一杯啤酒)只为了说声谢谢!💪


作者

root@kitploit:~
   Mariusz Banach / mgeeky, 21
   <mb [at] binary-offensive.com>
   (https://github.com/mgeeky)
下载工具