这篇文章重在分享bybass的方法,循序渐进分为12种方案。
并给出了详细的参考项目和一些案例,这些都是经过实战对抗检测的。
目录
1.shellcode加密
2.降低程序的熵值
逃离本地沙箱
导入表混淆
禁用 ETW
避免“恶意API调用模式”
直接调用系统函数,规避“系统调用hook”
删除或覆盖ntdll里面的hook
欺骗线程调用堆栈(hook sleep)
beacon/shellcode 内存加密(读写权限)
反射加载
不使用RWX内存
更多内容可以关注公众号:TIPFactory情报工厂 获取更多其他对抗技术
从一个基本但重要的方法开始,静态 shellcode 混淆。在编写loader的时候一般利用XOR 或 RC4 加密算法,因为易于实现并且不会留下大量加载程序执行的加密IOC。比如用于混淆 shellcode 静态签名的 AES 加密会在二进制文件的导入地址表中留下线索, AES 解密函数(例如 CryptDecrypt、CryptHashData、CryptDeriveKey 等)以及被各家C端产品标记了。
任何 AV/EDR检测未知二进制时都会考虑二进制熵。加密 shellcode文件的熵相当高,这就表明二进制文件中的代码部分被混淆或者加密处理了。有几种方法可以减少二进制的熵:
很多 EDR会在本地沙箱中运行二进制文件以检查其行为,但是都是几秒因为考虑用户体验。我们可以通过延迟执行 shellcode 来逃避这个限制。你可以随机计算一个大素数延迟执行或者其他操作。
EDR都会有自己的一套API灰名单。比如 VirtualAlloc 、 VirtualProtect 、 WriteProcessMemory 、 CreateRemoteThread 、 SetThreadContext 等,可以从https://github.com/Mr-Un1k0d3r/EDRs/blob/main/EDRs.m获取一些参考。在大多数情况下,我们使用直接系统调用来绕过“可疑 WINAPI 调用”的两个 EDR hook(参考第 7 节),但对于不太可疑的 API 调用,我们添加 WINAPI 调用的函数签名,在 ntdll.dll 中获取 WINAPI 的地址,然后创建指向该地址的函数指针(隐式调用):
typedef BOOL (WINAPI * pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD |
这其实并没有什么大用,单纯抹掉IAT调用,对于一些win api hook无能为力,但是也可以增加静态提取关键字符检测的成本。
现代 EDR 广泛利用 Windows 事件跟踪 (ETW),特别是 Microsoft Defender for Endpoint(以前称为 Microsoft ATP)。 ETW 允许对进程功能, WINAPI 调用等行为进行广泛的检测和跟踪。 ETW 大量组件在内核中,主要用于为系统调用和其他内核操作注册回调,但也包含一个用户态组件,参考之前对ProcessMon的攻击文章。由于 ntdll.dll 是加载到我们的二进制进程中的 DLL,我们完全可以控制这个 DLL,从而控制 ETW 功能。用户空间中有很多不同的 ETW 绕过方法,最常见的一种是修补函数 EtwEventWrite,该函数被调用以写入/记录 ETW 事件。我们在 ntdll.dll 中获取它的地址,并将它的第一条指令替换为返回 0 (SUCCESS) 的指令,核心代码如下:
void disableETW(void) { |
大多数行为检测最终都是基于检测恶意模式。其中一种模式是特定 WINAPI 在短时间内的调用顺序。第 4 节中简要提到的可疑 WINAPI 调用通常用于执行 shellcode,因此受到严格监控。然而,这些调用也用于一般正常的活动(VirtualAlloc、WriteProcess、CreateThread 模式可以看成恶性:内存分配和写入 shellcode),因此 EDR挑战是区分良性和恶意调用。 可以参考此前Bypassing EDR Real-Time Injection Detection Logic - RedBluePurple的博客。分为两步:
该技术难点在找到连续内存页面中容纳整个 shellcode 的内存位置。可以参考GitHub - xuanxuan0/DripLoader: Evasive shellcode loader for bypassing event-based injection detection (PoC) 项目。
直接系统调用可以参考Red Team Tactics: Combining Direct System Calls and sRDI to bypass AV/EDR | Outflank这边文章,写的很详细。
简而言之,直接系统调用是直接对内核系统调用等效的 WINAPI 调用。我们不调用 ntdll.dll VirtualAlloc,而是调用它在 Windows 内核中定义的内核等效 NtAlocateVirtualMemory。这就避免了EDR在用户层对ntdll的检测。
为了能直接调用系统函数,我们从 ntdll.dll 中获取我们要调用的系统函数 syscall ID,使用函数签名将函数参数的正确顺序和类型推送到堆栈,然后调用 syscall < id>指令。有几个工具可以为参考,SysWhispers2 和 SysWhisper3 。当然这样会存在两个问题:
另一个在 ntdll.dll 中逃避 EDR hook的好方法是用来自 ntdll.dll 的新副本覆盖默认加载(并由 EDR hook)的 ntdll.dll。 ntdll.dll 是任何 Windows 进程加载的第一个 DLL。如果我们的代码运行之后在内存中加载一个新的 ntdll.dll 副本,那些EDR hook就会被覆盖。 RefleXXion 是一个 C++ 库
(https://github.com/hlldz/RefleXXion,https://www.mdsec.co.uk/2022/01/edr-parallel-asis-through-analysis/ ),使用直接系统调用 NtOpenSection 和 NtMapViewOfSection 来获取 \KnownDlls\ntdll.dll(预加载的 DLL 的注册表路径)中干净 ntdll.dll 的句柄。然后它会覆盖已经加载 ntdll.dll 的 .TEXT 部分,从而清除 EDR hook。
后面介绍的两种bypass是针对内存中的shellcode,大部分内存中shellcode处在等到CC指令的状态,这很容易被EDR内存扫描到。当注入程序处于休眠状态时,它的线程返回地址指向我们驻留在内存中的 shellcode。通过检查可疑进程中线程的返回地址,可以很容易地识别出我们的植入的 shellcode。我们可以通过hook sleep的方法来实现返回地址和shellcode的关联。
https://twitter.com/mariuszbit
https://github.com/mgeeky/ThreadStackSpoofer
上面两个参考程序对beacon的这种hook方式很有用,也被大量引用在动态bypass杀软上。
可以用一个例子看一下,上面是默认线程调用堆栈图示,下面是返回sleep上的调用堆栈:
我们可以注册一个自己的VEH,并写一个hook sleep函数,然后在调用mysleep的时候更改shllcode内存区域权限。但需要shellcode执行的时候抛出一个异常,由veh在线程上下文恢复,将shellcode权限改回 RX,参考:
https://github.com/mgeeky/ShellcodeFluctuation
反射加载的方式已经有点普遍,cs自带了反射dll加载方式,推荐另一个项目:
https://github.com/boku7/BokuLoader,它有以下特点:
相比较之前cs和msf使用的GitHub - stephenfewer/ReflectiveDLLInjection: Reflective DLL injection is a library injection technique in which the concept of reflective programming is employed to perform the loading of a library from memory into a host process.这个更加的“红队”。
在cs配置文件中不适用RWX权限内存空间: