与猫捉老鼠相似的研究逃逸安全防护软件的技巧

作者:Yobo体育

时间:
2022-05-22 13:55:26

  我会在本文中列出一系列可用于绕过行业领先的企业终端保护解决方案的技术。出于安全的考虑,所以我决定不公开发布源代码。

  在模拟攻击中, 初始访问 阶段的一个关键挑战是绕过企业终端上的检测和响应能力 ( EDR ) 。商业命令和控制框架向红队操作员提供不可修改的 shellcode 和二进制文件,这些文件由终端保护行业大量签名,为了执行该植入,该 shellcode 的签名(静态和行为)需要被混淆。

  我会在将介绍以下技术,其最终目的是执行恶意的 shellcode,也被称为(shellcode)加载程序:

  让我们从静态 shellcode 混淆话题开始。在我的加载程序中,我利用了 XOR 或 RC4 加密算法,因为它易于实现并且不会留下大量加载程序执行的加密活动的外部指标。用于混淆 shellcode 静态签名的 AES 加密会在二进制文件的导入地址表中留下痕迹。在此加载程序的早期版本中,我已经让 Windows Defender 专门触发了 AES 解密函数(例如 CryptDecrypt、CryptHashData、CryptDeriveKey 等)。

  许多 AV/EDR 解决方案在评估未知二进制时考虑二进制熵。由于我们正在加密 shellcode,我们的二进制文件的熵相当高,这清楚地表明二进制文件中的代码部分被混淆了。

  一个更好的解决方案是设计和实现一种算法,将 shellcode 混淆(编码 / 加密)成英文单词(低熵)。

  许多 EDR 解决方案将在本地沙箱中运行二进制文件几秒钟以检查其行为。为了避免对最终用户体验的影响,他们检查二进制文件的时间不能超过几秒钟 ( 我曾经见过 Avast 检查时间超过 30 秒,但这是一个例外 ) 。我们可以通过延迟 shellcode 的执行来滥用这个限制。简单地计算一个质数是我个人的最爱,并将该数字用作加密 shellcode 的(一部分)密钥。

  我们添加 WINAPI 调用的函数签名,在 ntdll.dll 中获取 WINAPI 的地址,然后创建指向该地址的函数指针:

  src=使用字符数组混淆字符串会将字符串分割成更小的部分,使它们更难从二进制文件中提取

  许多 EDR 解决方案广泛利用 Windows 事件跟踪 ( ETW ) ,特别是 Microsoft Defender for Endpoint(以前称为 Microsoft ATP)。 ETW 允许对进程的功能和 WINAPI 调用进行广泛的检测和跟踪。ETW 在内核中有一些组件,主要用于注册系统调用和其他内核操作的回调函数,但也包含一个用户域组件,它是 ntdll.dll ( ETW 深度攻击向量 ) 的一部分。。由于 ntdll.dll 是一个加载到二进制文件进程中的 DLL,因此我们可以完全控制该 DLL,从而控制 ETW 功能。在用户空间中,ETW 有很多不同的绕过方法,但最常见的是为 EtwEventWrite 函数打补丁,它被调用来写入 / 记录 ETW 事件。我们在 ntdll.dll 中获取它的地址,并用返回 0 ( SUCCESS ) 的指令替换它的第一个指令。

  src=我发现上述方法仍然适用于两个测试的 EDR,但这是一个嘈杂的 ETW 补丁

  大多数行为检测最终都是基于检测恶意模式。其中一种模式是在短时间内特定的 WINAPI 调用的顺序。第 4 部分中简要提到的可疑 WINAPI 调用通常用于执行 shellcode,因此受到严格监控。然而,这些调用也用于良性活动(VirtualAlloc、WriteProcess、CreateThread 模式结合内存分配和写入大约 250KB 的 shellcode),因此使用 EDR 解决方案的挑战是区分良性调用和恶意调用。 你可以参考 Filip Olszak 的一篇文章,其中提到利用延迟和较小的分配和写入内存块来融入良性 WINAPI 调用行为。简而言之,他的方法调整了典型 shellcode 加载程序的以下行为:

  6.1 与其分配一大块内存并直接将大约 250KB 的植入 shellcode 写入该内存,不如分配小的连续块,例如小于 64KB 内存并将它们标记为 NO_ACCESS。然后以类似的块大小将 shellcode 写入分配的内存页面。

  6.2 在上述每个操作之间引入延迟。这将增加执行 shellcode 所需的时间,但也会使连续执行模式变得不那么突出。

  使用这种技术的一个问题是,要确保在连续的内存页中找到一个可以容纳整个 shellcode 的内存位置。 Filip 的 DripLoader 实现了这个概念。

  我构建的加载程序不会将 shellcode 注入另一个进程,而是使用 NtCreateThread 在自己的进程空间中的线程中启动 shellcode。未知进程(我们的二进制文件实际上流行率很低)进入其他进程(通常是 Windows 原生进程)是突出的可疑活动。当我们在加载程序进程空间的线程中运行 shellcode 时,更容易混入进程中良性线程执行和内存操作的噪音。然而,不利的一面是任何崩溃的开发后模块也会导致加载程序的进程崩溃,从而导致植入程序崩溃。持久性技术以及运行稳定可靠的 BOF 可以帮助克服这一缺点。

  加载程序利用直接系统调用绕过 EDR 放入 ntdll.dll 的任何挂钩。我不想在此过多地讨论直接系统调用的话题。

  为了直接调用系统调用,我们从 ntdll.dll 中获取我们要调用的系统调用的 syscall ID,使用函数签名将函数参数的正确顺序和类型推送到堆栈,然后调用 syscall指令。在此推荐两个工具,SysWhispers2 和 SysWhisper3 就是两个很好的例子。从规避的角度来看,调用直接系统调用有两个问题:

  7.1 你的二进制文件最终具有 syscall 指令,该指令很容易静态检测。

  7.2 与通过等效的 ntdll.dll 调用的系统调用的良性使用不同,系统调用的返回地址不指向 ntdll.dll。相反,它指向我们调用系统调用的代码,它驻留在 ntdll.dll 之外的内存区域中。这是一个未通过 ntdll.dll 调用的系统调用的指标,非常可疑。

  7.3 实现一个寻蛋机制。将系统调用指令替换为 egg ( 一些随机的唯一可识别模式 ) ,在运行时,在内存中搜索这个 egg,并使用 ReadProcessMemory 和 WriteProcessMemory 的 WINAPI 调用将其替换为系统调用指令。然后,我们可以正常地使用直接系统调用。该技术已由 klezVirus 实现。

  7.4 我们不是从我们自己的代码中调用 syscall 指令,而是在 ntdll.dll 中搜索 syscall 指令,并在我们准备好调用系统调用的堆栈后跳转到该内存地址。这将导致 RIP 中的返回地址指向 ntdll.dll 内存区域。

  我建议使用与第 7 部分中描述的相同的方法来调整 RefleXXion 库。

  接下来的两部分将介绍两种技术,它们可以避免在内存中检测我们的 shellcode。由于植入程序的信标行为,大部分时间植入程序都处于休眠状态,等待其操作员的传入任务。在此期间,植入程序容易受到来自 EDR 的内存扫描技术的攻击。本文中描述的两种规避方法中的第一种就是欺骗线程调用堆栈。

  当植入程序处于休眠状态时,它的线程返回地址指向我们驻留在内存中的 shellcode。通过检查可疑进程中线程的返回地址,可以很容易地识别出我们的植入 shellcode。为了避免这种情况,想打破返回地址和 shellcode 之间的这种联系。我们可以通过挂钩 Sleep ( ) 函数来做到这一点。当该挂钩被调用(通过植入 / 信标 shellcode)时,我们用 0x0 覆盖返回地址并调用原始的 Sleep ( ) 函数。当 Sleep ( ) 返回时,我们将原始返回地址放回原处,以便线程返回到正确的地址以继续执行。 Mariusz Banach 在他的 ThreadStackSpoofer 项目中实现了这种技术。

  在下面的两个屏幕截图中,我们可以观察到欺骗线程调用堆栈的结果,其中非欺骗的调用堆栈指向非备份内存位置,而欺骗的线程调用堆栈指向挂钩的 Sleep ( MySleep ) 函数,并 切断 调用堆栈的其余部分。

  另一个规避内存检测的方法是在休眠时加密植入程序的可执行内存区域。使用与上一节中描述的相同的 sleep 挂钩,我们可以通过检查调用者地址(调用 Sleep ( ) 的信标代码以及我们的 MySleep ( ) 挂钩)来获取 shellcode 内存段。如果调用者内存区域是 MEM_PRIVATE 和 EXECUTABLE ,并且大小与我们的 shellcode 差不多,那么内存段将使用 XOR 函数加密并调用 Sleep ( ) 。然后 Sleep ( ) 返回,它解密内存段并返回给它。

  另一种技术是注册一个 vector Exception Handler ( VEH ) ,它处理 NO_ACCESS 违规异常,解密内存段并更改 RX 的权限。然后就在休眠之前,将内存段标记为 NO_ACCESS,这样当 Sleep ( ) 返回时,就会出现内存访问冲突异常。因为我们注册了一个 VEH,所以异常是在该线程上下文中处理的,并且可以在引发异常的完全相同的位置恢复。 VEH 可以简单地解密并将权限更改回 RX,并且植入程序可以继续执行。这种技术可以防止在植入程序处于睡眠状态时出现可检测的 Sleep ( ) 挂钩。

  我们在这个加载程序中执行的信标 shellcode 最终是一个需要在内存中执行的 DLL。许多 C2 框架利用 Stephen Fewer 的 ReflectiveLoader。关于反射性 DLL 加载程序的工作原理有很多书面解释,Stephen Fewer 的代码也有很好的文档记录,但简而言之,反射式加载程序执行以下操作:

  在你的 Malleable C2 配置文件中,确保配置了以下选项,这些选项限制了 RWX 标记内存的使用 ( 可疑且容易检测 ) ,并在信标启动后清理 shellcode。