在侵入性防病毒软件和EDR引入热补丁以增强其监控能力的时代,现代对手必须拥有强大的工具来绕过这些看守。这里提出的动态导入解析器实现,能够即时解除所用函数的挂钩,是加强对手韧性努力的又一步。
我在这里提出的解决方案是,从使用链接器解析的WinAPI导入(在编译后可执行文件的PE头中可见,特别是导入地址表)转向完全动态的方法,仅以动态方式解析导入。这种动态解析器可以配备在后台运行的解除挂钩逻辑,无需操作员任何指导。
以下是如何确保以解除挂钩、不受监控的方式调用MessageBoxW:
RESOLVE(user32, MessageBoxW);
_MessageBoxW(0, L"看!我已经解除挂钩了!", L"第三个 - 已解除挂钩", 0);
所有魔法都发生在RESOLVE宏定义中,它构造了一个名为_MessageBoxW的ImportResolver<T>对象。

以下是UnhookMe示例的工作方式:
MessageBoxWMessageBoxW的前导码,使其始终返回0而不显示消息UnhookingImportResolver解析器动态解析MessageBoxW,该解析器会检测应用的前导码补丁,并恢复原始字节,从而有效解除MessageBoxW功能的挂钩。在弹出消息框的同时,控制台标准输出会打印以下日志行:
[~] 已解析符号 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解析器头文件您的程序只需包含两个头文件:
#include "usings.h"
#include "resolver.h"
有一些可以更改的全局选项,它们会影响解析器的工作方式或输出。这些选项定义在**resolver.cpp**文件的开头:
解析器全局选项:
globalQuietOption - 设置为true以关闭所有输出globalVerboseOption - 设置为true以显示详细的详细输出globalAntiSplicingOption - 如果函数被挂钩,则解除其挂钩。globalLogFilePath - 输出日志的目标路径。如果为空,则使用标准输出。bool globalQuietOption = false;
bool globalVerboseOption = true;
bool globalAntiSplicingOption = true;
wchar_t globalLogFilePath[MAX_PATH] = L"";
为了使用解析器,必须首先使用严格形式的using语句声明函数指针类型:
using fn_FunctionName = ReturnType WINAPI (
ParamType1 paramName1,
...,
ParamTypeN paramNameN,
);
此仓库附带**usings.h**头文件,其中包含数十个流行Windows API的预定义using类型。
FunctionName 将对应于我们想要让ImportResolver解析的WinAPI,该函数指针必须标记为具有WINAPI调用约定(x86上为__stdcall,x64上为__fastcall)。ReturnType 必须位于WINAPI类型修饰符之前。
在定义了如上所述的函数指针类型后,我们可以按以下方式使用它:
RESOLVE(libraryName, FunctionName);
ReturnType output = _FunctionName(param1, ..., paramN);
宏RESOLVE负责实例化ImportResolver模板对象,并调整指定库的名称。
解析器引入了多个宏定义,以提供在各种情况下轻松调用的构造函数调用:
#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)
解析器的构造函数:
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头的完整性以及所引用函数存根字节的完整性。
思路如下:
LoadLibrary来加载用户引用的库(由RESOLVE宏的第一个参数指定),如果无法通过GetModuleHandle访问它。std::map)中返回它们。这种动态解除挂钩解析器面临的问题之一包括处理转发API(DLL可能包含导出占位符,表明此函数未在此模块中实现,而是在另一个模块中)的问题——尽管此实现支持,但偶尔会破坏其遍历逻辑。
这个项目和其他项目都是不眠之夜和大量辛勤工作的成果。如果你喜欢我所做的事情,并感激我始终回馈社区, 请考虑请我喝杯咖啡 (或者更好,一杯啤酒)只为了说声谢谢!💪
Mariusz Banach / mgeeky, 21
<mb [at] binary-offensive.com>
(https://github.com/mgeeky)