frkngksl/ExportHider

GitHub: frkngksl/ExportHider

ExportHider 在运行时动态重建导出表以隐藏 DLL 导出函数,仅对合法调用保持透明。

Stars: 19 | Forks: 2

# ExportHider ExportHider 生成一个 C++ DLL 模板,其中包含一个代码存根,允许你在文件系统的 DLL 导出目录中隐藏导出函数。放置函数定义并编译文件后,像 CFF Explorer 这样的 PE 文件查看器将无法看到隐藏的导出函数。然而,由于模板中的代码存根会在运行时重建导出目录,合法的 GetProcAddress 调用仍能成功执行。此方法仅适用于动态 DLL 加载或自定义 DLL 加载器的情况。 # 它是如何工作的? 通常,当你想在 DLL 文件(用 C 或 C++)中定义一个导出函数时,只需要在函数名之前添加 **`__declspec(dllexport)`** 关键字,或者创建一个 .def 文件。编译后,编译器会创建一个称为“导出目录”的特定表,用于存储与导出函数相关的信息。导出目录的结构如下所示:

当某个进程想要使用 DLL 文件中的函数时,Windows 加载器会简单地解析此结构,并使用 `AddressOfFunctions`、`AddressOfNames`、`AddressOfNameOrdinals` 数组导入请求的函数。 导入函数的特定流程在 [ferreirasc 的博客文章](https://ferreirasc.github.io/PE-Export-Address-Table/) 中有详细说明,但简而言之,对于按名称导入的函数,加载器会遍历 `AddressOfNames` 数组(该数组的值只是 RVA),并搜索给定的名称。一旦加载器在“i”位置找到匹配项,它会引用 `AddressOfNameOrdinals` 数组的第 i 个索引,以获取与该函数关联的序号。拥有序号后,加载器会引用 `AddressOfFunctions` 中序号位置的值,最终获取与导入函数关联的 RVA。

这里的关键点是,当调用 LoadLibrary 时,Windows 加载器执行的所有这些搜索和访问操作都是在 DLL 被映射到进程地址空间之后进行的。在 DLL 映射期间,整个 DLL 文件(包括其 PE 头)会被写入内存,加载器解析内存中的头以到达导出目录。这意味着如果 DLL 自身能够在附加到进程后覆盖其内存中的 PE 头,从而修改导出目录地址(简单来说,就是 `DataDirectory` 的第 0 个索引),那么加载器就会在一个任意的导出目录中查找要导入的函数,而不是编译器添加的那个。

# 命令行参数 ``` __ _ _ _ /__\_ ___ __ ___ _ __| |_ /\ /(_) __| | ___ _ __ /_\ \ \/ / '_ \ / _ \| '__| __|/ /_/ / |/ _` |/ _ \ '__| //__ > <| |_) | (_) | | | |_/ __ /| | (_| | __/ | \__/ /_/\_\ .__/ \___/|_| \__\/ /_/ |_|\__,_|\___|_| |_| by @R0h1rr1m Usage of C:\Users\Public\DLLDemo\ExportHider.exe: -h | --help Show the help message. -i | --input Input path for the list of function names to be hidden. (Mandatory) -o | --output Output path for the DLL template. (Mandatory) -n | --name Name of the DLL for the Export Directory. (Mandatory) -c | --count Number of other exported functions that won't be hidden. ``` 关于 `-i | --input ` 参数,你需要为存储要逐行隐藏函数名称的输入文件指定一个路径。示例输入文件的内容如下: ``` TestFunction1 TestFunction2 TestFunction3 ``` 关于 `-c | --count` 参数,如果你不想隐藏所有导出的函数(即有些函数使用 `__declspec(dllexport)` 或 .def 文件导出,并通过 PE 文件查看器显示在 DLL 文件的导出目录中),请使用此参数指定数量,因为该工具在进行内存计算时需要这个信息。 # 快速演示视频 [](https://www.youtube.com/watch?v=ylYd89nvLEk "ExportHider Quick Demo") # 变通方法 如果你想尝试这项技术,在项目开发过程中我遇到了两个有趣的问题。在修改项目之前,你可能需要了解它们: - Windows 加载器在使用名称调用 `GetProcAddress` 函数时,会采用类似二分查找的算法来查找导出函数。因此,所有导出函数的名称(包括隐藏的)都需要在 `AddressOfNames` 数组中排序。否则,`GetProcAddress` 函数将返回 NULL。因此,我使用了冒泡排序算法来对该数组的成员进行排序。 - 正如上面所说,`AddressOfNames` 数组、`AddressOfFunctions` 数组、`DataDirectory` 数组以及其他一些字段需要相对虚拟地址(RVA)值。此外,它们将这些 RVA 值保存在 DWORD 大小的字段中。当你使用 `VirtualAlloc` 或 `HeapAlloc` 等动态内存分配函数为新的任意导出目录分配内存区域时,给定地址会远离 DLL 映射区域,RVA 值无法适应 DWORD 大小的字段,从而导致整数溢出。这就是为什么我在 DLL 模板中使用全局变量和字节数组类型来满足内存需求。 # 静态导入 DLL(又称 DLL 侧加载)情况 我创建此项目的最初目标是生成一个没有导出(或完全缺少导出表)但仍能被新创建进程成功加载的 DLL。我原本认为这种行为会为 DLL 侧加载有效载荷带来新的实验环境。然而,我无法找到任何函数或方法,使导出目录修复存根在 Windows 加载器检查导出 DLL 函数之前运行。 dll_timing_problem (1) 更技术地说,我注意到以下流程和函数调用适用于代码段中与 Windows 加载器相关的每个 DLL(在 NTDLL 中收集;可能有一些错误,因为我并非逆向工程专家): ``` 1. LdrpMapDll - This is where the DLL is mapped to the Process Address Space. All DLLs that satisfy the DLL name condition are directly put into the memory, no precheck control for the filesystem version. 2. LdrpSnapModule - This is where the Windows Loader starts to resolve imports. For each import descriptor, it parses the PE structure, checks the export table, binary searching for the imported function, calculating RVA of that, and writes its address to the corresponding caller's process' Import Address Table entry during this function. 3. LdrpDoPostSnapWork - If step 2 succeeds for each imported function, memory protections, TLS initialization, CFG enablement are done in this function. 4. LdrpInitializeNode - If step 3 succeeds, there are module linking functions in this step. 5. LdrpCallTlsInitializers - This is where the TLS callbacks are called before the DllMain function. 6. LdrpCallInitRoutine - This is where the DLLMain itself is called for the first time for the imported DLL. In the original solution, this function is too late to fix the export table. ``` 当你运行一个导入 DLL 的可执行文件时,如果 Windows 加载器在 DLL 的导出表中找不到所需的函数名,它会停止执行,并且不会执行 ```LdrpSnapModule``` 之后调用的函数。 对于 DLL 侧加载的情况,我们无法修改调用者进程;因此,唯一的机会是在 ```LdrpMapDll``` 和 ```LdrpSnapModule``` 函数之间动态修复导出表,因为加载器会在 ```LdrpSnapModule``` 函数检查期间立即停止。我尝试了 TLS 回调、第二次 DLL 加载、转发导出以及其他一些变通方法,但都没有帮助我找到这样一个位置,因此不幸的是,此方法无法直接用于 DLL 侧加载或静态导入的 DLL。如果发现解决方案或可行的变通方法,我非常乐意进一步探索——无论是讨论想法、共同思考方法,还是实现它。任何在这方面的贡献都将非常感激。 一种可能的变通方法是,将整个工作拆分为两个 DLL,即“代理 DLL”和“有效载荷 DLL”。代理 DLL 使用 EXE 期望的名称,并具有满足加载器静态导入检查的可见导出;而有效载荷 DLL 包含真实隐藏的功能,没有导出,并在 DllMain 中重建其导出表。我认为这不是解决此问题的良好变通方法,因此我没有实现它。 # 参考资料 - https://rioasmara.com/2021/10/10/analyze-dll-export-with-pe-bear/ - https://ferreirasc.github.io/PE-Export-Address-Table/ # 免责声明 仅限授权的安全测试使用。未经明确许可,滥用本工具对抗系统属于非法行为。
标签:AddressOfFunctions, AddressOfNameOrdinals, AddressOfNames, C++ DLL, DLL 注入, DOM解析, Export Directory, GetProcAddress, LoadLibrary, PE 文件结构, Windows 加载器, XML 请求, 云资产清单, 代码混淆, 函数隐藏, 动态加载, 动态链接库, 反调试, 安全开发, 导出表隐藏, 端点可见性, 自定义加载器, 软件保护, 运行时导出, 逆向工程, 防护绕过