
DllShimmer 会解析原始 DLL 并提取导出函数的信息(名称、序号和转发器信息)。基于这些信息,DllShimmer 会生成一个 C++ 样板文件(.cpp)。生成的文件允许你向原始 DLL 导出的每个函数添加自己的代码,而不会干扰程序的正常运行。 无需逆向工程或插桩,因为 DllShimmer 不依赖函数签名(详见“局限性”)。
生成的第二个文件是 .def 文件,它确保代理 DLL 编译后导出的所有 DLL 与原始 DLL 具有相同的名称和序号。
编译后,代理 DLL 中的 EAT 与原始 DLL 中的 EAT 完全相同。所有导出函数的名称和序号均匹配,转发函数也会照常转发。 DllShimmer 不会像大多数工具那样显式转发所有函数,从而形成一种全新的、可疑的 EAT 结构。
编译 Go 源代码,或下载已编译的二进制文件。
依赖:
x86_64-w64-mingw32-g++x86_64-w64-mingw32-dlltool示例:
# Backdoor version.dll (proxy to absolute path)
./DllShimmer -i version.dll -o project/ -x "C:/Windows/System32/version.dll" -m
# Backdoor random chat.dll (proxy to relative path)
./DllShimmer -i chat.dll -o project/ -x "lib/chat2.dll" -m
# Backdoor random app.dll (static linking to the original DLL)
./DllShimmer -i app.dll -o project/ -x "app2.dll" -m --static
参数:
-i / --input <path> [必需]
你想要植入后门的原始 DLL。
-o / --output <path> [必需]
DllShimmer 保存所有生成文件的目录路径。
-x / --original <path> [必需]
对于动态链接(默认),请提供代理 DLL 在目标系统上查找原始 DLL 的路径。
对于静态链接(--static),只需指定原始 DLL 的名称。系统将按照 Windows 上的默认加载顺序进行搜索。
-m / --mutex [可选]
启用此选项会向源文件添加互斥体,防止你的后门在单次程序运行期间被多次执行。所有原始函数仍将正常工作。
--static [可选]
启用代理 DLL(IAT)与原始 DLL(EAT)之间的静态链接。这会在输出目录中生成一个额外的 .lib 文件,作为静态编译时的原始 DLL。
与动态链接相比,该技术存在一些严重的局限性:
然而,在某些场景下,静态链接可能更隐蔽、更自然。
默认情况下:DllShimmer 始终使用 LoadLibraryA() 和 GetProcAddress() 函数进行动态链接。
--debug-file <path> [可选]
将调试日志保存到文件。程序运行期间,日志会持续写入文件。如果选择此选项,日志将不会打印到 STDOUT。
默认情况下:DllShimmer 始终将调试日志写入 STDOUT。
调试输出示例:

开始故障排查之前:
--static)。使用动态链接(默认)更容易调试。--debug-file)。.cpp 文件中,我看不到原始 DLL 的所有导出函数。原始 DLL 中被定义为“转发”的函数不会包含在 .cpp 文件中。但是,它们会出现在 .def 文件中。编译后它们也会被导出,与原始 DLL 完全一致。
有时,即使你在 -x 参数中理论上指定了正确的相对路径,代理 DLL 在加载原始 DLL 时仍会显示错误,错误代码为 126。为什么不生效?!?
系统会在 当前目录 中搜索 DLL。在 98% 的情况下,该目录就是主 EXE 文件所在的位置,但有些程序(大多是旧版遗留程序)会使用例如 SetCurrentDirectoryW() 随意更改 当前目录。主程序知道这一更改,因此它能正确加载你的代理 DLL,而你却对此毫不知情,并尝试相对加载原始 DLL,而程序会在更改后的 当前目录 中搜索它。
此规则同时适用于原始 DLL 的静态和动态加载。遗憾的是,使用静态链接时,此问题更难发现,因为我们没有调试信息。系统加载器只会失败,然后就此结束。这就是为什么我始终建议优先使用默认的动态链接。
在动态链接的情况下,我们有两种选择:
当前目录 情况调整 -x 参数中的路径。当前目录,以便在我们想要的位置搜索 DLL。在静态链接的情况下,我们实际上只有一种选择:
当前目录。