[翻译]木马病毒和EDR的一些Bypass思路
vghost 发表于 北京 二进制安全 1626浏览 · 2024-06-17 15:42

原文:https://www.vaadata.com/blog/antivirus-and-edr-bypass-techniques/

EDR的简介:

在我们的审计过程中,特别是基础设施和网络渗透测试期间,我们的渗透人员可能会需要在配备了防病毒软件的机器上运行工具。此类工具在实际网络攻击中也会使用,因此会被现有的安全解决方案逻辑地阻止。例如,用于评估 Active Directory 安全性的 Rubeus 和 SharpHound 等工具就是这种情况。

防病毒/EDR 如何工作?

在开始创建加载器之前,重要的是要了解为什么计算机程序被安全解决方案视为恶意或非恶意。
防病毒/EDR 软件有许多用于对计算机程序进行分类的工具:
签名数据库
任何计算机程序都可以通过哈希函数(例如 SHA-256)来生成唯一的签名。
基于签名的分析涉及存储已知属于恶意程序的所有签名的列表,然后将每个新签名与该列表进行比较。
虽然这种方法很有趣,但仅靠它不足以保证防病毒软件的有效性。事实上,它相对容易被绕过。程序中的任何细微变化,无论多么微小,都会完全改变其签名。
此外,这种安全方法无法防御尚未列出的签名的新威胁。
例如,Github 上 Mimikatz 2.2.0 版本对应的 SHA256 哈希为:

如果将具有相似哈希值的程序下载到机器上,大多数防病毒软件会立即检测到它。

静态分析

静态分析包括在程序中搜索已知属于恶意程序的一个或多个字符串。
例如, Matt Hand 的DefenderCheck工具可用于确定 Windows Defender 检测到给定可执行文件中的哪些字符串。
下面是使用 Mimikatz 程序的示例:

使用 Windows Defender 静态分析检测 Mimikatz

我们可以看到,Defender 根据包含“mimikatz”一词的字符串将该程序认定为恶意软件。
我们也可以使用 YARA 规则来观察这种现象。例如,我们使用 YARA 规则来检测“Portable Executable”可执行文件中是否存在以下字符串:

## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )

我们可以看到,当我们对“mimikatz.exe”使用 YARA 规则时会触发“Windows_Hacktool_Mimikatz_1388212a”警报,而“notepad.exe”不会触发警报(因为后者不包含有问题的字符串):

Yara 规则检测 Mimikatz

如果我们运行“mimikatz.exe”程序,我们会看到该字符串确实存在:

Yara 规则已正确检测到字符串
因此很明显,可以通过程序的静态分析来将其表征为威胁。

启发式和行为检测

启发式检测的目的是了解程序的工作原理并确定它将在系统上执行什么操作。
这可以通过使用沙盒来实现:一个隔离的虚拟机,潜在危险的程序可以在其中运行。然后,防病毒软件可以检查程序在执行过程中采取的操作并寻找恶意操作的迹象。
类似地,行为检测包括观察程序在执行期间执行的操作,以发现任何可疑活动。
例如,按特定顺序对 Windows API 进行的某些调用被认为是典型的恶意软件模式。

导入地址表 (IAT) 检查

在编写计算机程序时,开发人员可以调用库。
在 Windows 系统上,这是通过 DLL(动态链接库)实现的。DLL 是一个导出现成函数的程序。然后,开发人员可以选择将某些 DLL 加载到他们的程序中,以利用它们导出的函数。一定数量的 DLL(例如“user32.dll”或“kernel32.dll”)是所有 Windows 系统的标准配置,它们导出对开发人员有用的函数。
这些函数由 Microsoft 记录并构成通常所熟知的 Windows API。
例如,开发人员可以使用“user32.dll”DLL导出的“MessageBoxA”函数来显示对话框:

使用 MessageBoxA 打开对话框
导入地址表是与每个可移植可执行文件(包含在 PE 的可选头的导入目录中)相关的表,其中包含程序使用的已加载 DLL 及其导出函数的列表。
下面的示例显示了“notepad.exe”程序的导入地址表的启动:

Notepad.exe IAT 内容的开始
大多数防病毒软件都会观察该表,某些功能的存在会触发警报。
例如,如果“OpenProcess”、“VirtualAllocEx”、“WriteProcessMemory”和“CreateRemoteThreadEx”(均由“kernel32.dll”导出)函数同时存在于同一个程序中,则会引起高度怀疑。这 4 个函数一起使用可启用恶意程序经常使用的进程注入技术。

反恶意软件扫描接口 (AMSI)

AMSI 是 Windows 操作系统提供的一个接口,任何开发人员都可以使用它将防病毒保护集成到他们的程序中。
更具体地说,开发人员可以选择将“AMSI.dll”DLL 加载到他们的程序中并使用此 DLL 导出的函数。例如,“AmsiScanString”函数以一串字符作为输入,如果未检测到威胁,则返回“AMSI_RESULT_CLEAN”,否则返回“AMSI_RESULT_DETECTED”。
因此,AMSI 充当了给定程序和防病毒软件之间的桥梁(“amsi.dll”默认与 Windows Defender 配合使用,但防病毒软件供应商可以创建自己的“amsi.dll”以与其产品配合使用)。
并非所有程序都需要使用 AMSI。例如,在 Windows 上的记事本中写入字符串不会带来特别的风险。因此,“notepad.exe” 不会加载“amsi.dll”:

Notepad.exe 未加载 AMSI.dll
另一方面,某些 PowerShell 命令显然会损害系统的完整性。因此,微软开发人员选择将 AMSI 集成到 powershell.exe 中是合乎逻辑的:

AMSI.dll 由 Powershell.exe 加载

Windows 事件跟踪 (ETW)

ETW 是一种跟踪和记录由应用程序和驱动程序触发的大量事件的机制。
从历史上看,ETW 主要用于调试目的。随着时间的推移,该系统报告的大量数据引起了保护解决方案供应商的兴趣,他们看到了通过分析 ETW 报告的流量来检测恶意活动的机会。
ETW 由三个不同的组件组成:
提供者
Windows 操作系统中的各种系统组件或第三方应用程序都可以利用其代码将事件发送给提供程序。例如,Windows 事件跟踪 - 威胁情报提供程序。
在与关键功能相关的 Windows 代码中的几个地方,可以观察到与 Windows 事件跟踪 - 威胁情报提供程序相关的函数调用。例如,“MiReadWriteVirtualMemory”函数调用了“EtwTiLogReadWriteVm”。
我们可以使用 IDA 在“ntoskrnl.exe”(Windows 内核组件)上观察到这一点:

EtwTiLogReadWriteVm 使用 EtwProviderEnabled
如果我们观察“EtwTiLogReadWriteVm”函数的工作原理,我们会发现它使用“EtwProviderEnabled”函数来检查特定提供程序是否已启用来记录事件:

EtwProviderEnabled 用于检查给定的提供程序是否已正确激活
如果我们查阅该函数的 Microsoft 文档,我们可以看到第一个参数对应于指向提供程序句柄的指针,我们想要检查该提供程序的日志记录是否正确激活:

如果我们特别关注这个参数,我们就会明白它可以为我们提供所使用的提供商的指示:

这里,有问题的参数是“EtwThreatIntProvRegHandle”:

EtwThreatIntProvRegHandle 作为参数传递给 EtwProviderEnabled
因此,我们可以得出结论,每次使用“NtReadVirtualMemory”时,事件都会发送到“Windows 事件跟踪 - 威胁情报”提供程序。

消费者
消费者是使用供应商提供的日志并采取相应行动的各种程序。
我们以“Windows 事件跟踪 - 威胁情报”提供商为例。防病毒程序很可能会使用它提供的事件日志。
事实上,正如我们所见,该提供商收集了大量与关键功能使用相关的信息。因此,防病毒软件可以访问表明存在入侵行为的某些信息并采取相应措施。
控制器
在 ETW 中,控制器是负责管理事件跟踪过程的软件组件。其主要作用是启动、监视和控制跟踪会话。
因此需要注意的是,对于我们在 Windows 系统上执行的大多数操作,事件日志都会发送回防病毒软件/EDR,这增加了另一种检测可疑操作的方法。

API 挂钩

为了完全掌握 API 挂钩的概念,我们首先需要了解系统调用。
如前所述,大多数 Windows API 函数由“kernel32.dll”导出。这些函数不直接与内核通信;要进行通信,它们必须使用系统调用。
这些系统调用充当程序与 Windows 操作系统交互的接口。它们中的大多数都导出为“ntdll.dll”,命名约定是以字母“Nt”开头(尽管并非所有 NT API 函数都是系统调用)。
例如,如果开发人员想要使用 Windows API 中的“OpenProcess”函数,该函数实际上会调用“ntdll.dll”中的“NtOpenProcess”函数。
我们可以在‘x64dbg’中观察到这种现象:

OpenProcess函数调用NtOpenProcess
现在让我们看看“NtOpenProcess”在IDA中做了什么:

NtOpenProcess 执行系统调用
如您所见,在“NtOpenProcess”内部执行的操作很少。这是因为,与大多数以 Nt 开头的函数一样,“NtOpenProcess”实际上位于内核中。这些函数的“ntdll”版本只是执行系统调用来调用其内核模式对应函数。
因此,我们已经了解了“kernel32.dll”中的函数如何与系统交互。
现在让我们尝试了解防病毒软件如何使用这一原理来检测恶意行为。
当一台机器上安装了安全解决方案(例如 EDR)时,它可能会试图执行 API 挂钩。为此,安全解决方案将监视该机器以检测新进程的创建。
当启动新进程时,EDR 会将自己的 DLL 注入其中。EDR 将查找其希望监视其函数的其他 DLL 的内存地址。例如,希望监视“ntdll.dll”的“NtProtectVirtualMemory”的 EDR 将首先在注入的进程中找到“ntdll.dll”的基址,然后找到“NtProtectVirtualMemory”函数的地址。
一旦执行了这些操作,EDR 将把目标函数(负责执行系统调用)基地址的第一个字节替换为跳转指令(jmp)对应的字节,跳转到其自身 DLL 的代码。
因此,在挂钩之前:

挂接后:

绕过

这样,EDR 可以自由执行它认为必要的安全测试,并能够监视对 Windows API 函数的任何调用。
如果我们回顾一下本文前面讨论过的地址导入表 (IAT),即使攻击者没有让可疑函数出现在 IAT 中,API 挂钩技术仍然会检测到它!因此,这种方法对付恶意程序非常有效。

网络检测

最后,一些安全解决方案可能会监控机器的连接并根据某些指标阻止威胁。
例如,如果某个程序发起与已知与恶意服务器关联的 IP 地址的连接,则可能会决定进行阻止。这通过阻止恶意软件与危险服务器通信来增强安全性。
有哪些不同的防病毒和 EDR 绕过技术?
正如我们所见,安全解决方案有许多检测恶意活动的技术。在第二部分中,我们将探讨绕过这些保护的不同方法。

绕过基于签名的检测

基于签名的检测基于给定程序上的哈希函数生成的数字签名。如果我们更改程序中的哪怕一位数据,其签名也会完全不同。因此,绕过这种保护措施相当容易。
例如,我们可以改变程序中某个变量的名称,这将导致不同的签名,从而避免基于该签名的检测。

静态检测绕过

逃避恶意软件的静态检测在技术上并不一定复杂,但可能很耗时。
目的是修改某些可以检测到的元素,例如函数名称。
以下为 Go 程序示例:

我们可以使用以下命令编译该程序:$ go build helloWorld.go。
完成后,让我们使用 Linux 上的 strings 命令:

$ strings helloWorld | grep myHello
main.myHelloWorldFunc
main.myHelloWorldFunc

我们可以看到,“myHelloWorldFunc”函数的名称清晰可见。为了避免这种情况,一种解决方案是手动更改此函数的名称。但是,在大型程序中,重命名每个函数会花费时间。为了避免浪费时间,我们可以尝试自动执行此任务。使用 Go 语言,可以使用出色的“garble”库来混淆 Go 二进制文件。
让我们来看看“helloWorld”程序,并使用“garble”来编译它:

我们可以看到“myHelloWorldFunc”函数的名称不再以纯文本显示。Garble 进行了以下更改。
它取代了:

● 尽可能多的使用哈希的标识符。
● 用哈希值打包路径。
● 带有哈希值的文件名和位置信息。
和:
● 删除所有构建和模块信息。
● 通过 -ldflags=”-w -s” 选项删除调试信息和符号表。
● 如果给出了 -literals 标志,则对文字进行混淆。

此外,在设计用于加载 shellcode(表示可执行二进制代码的字符串,在我们的例子中,我们希望使该工具先前以 shellcode 形式放入且无法检测到)的加载器中,至关重要的是,该 shellcode 在我们的可移植可执行文件 (PE) 中不清晰可见。
因此我们可以加密我们的 shellcode(例如,使用 AES)。
这只会在我们的程序运行时解密。这样,对我们的程序进行简单的静态分析将不会发现明文形式的 shellcode 并触发警报。

绕过启发式和行为检测

有多种技术可以绕过启发式和行为检测。一种有效的方法可能是避免在沙盒环境中进行加载程序分析。
为此,我们可以寻找程序在沙盒环境中运行的迹象。
例如,一个指标可能是运行我们程序的机器是否在 Active Directory 域中。
为此,我们可以使用以下代码:

在此代码中,我们首先使用“NetGetJoinInformation”函数,该函数除其他内容外还返回“status”参数。
其结构如下:

“NetSetupDomainName”的值可以告诉我们我们所在的机器是否属于域。
我们还可以进行其他测试,例如检查 RAM 内存量、CPU 核心数、可用磁盘空间或虚拟化驱动程序的存在,例如“C:\Windows\System32\drivers\VBoxGuest.sys”或“C:\Windows\System32\drivers\VBoxMouse.sys”:

有了此代码,我们的加载程序将在检测到机器上存在“C:\Windows\System32\drivers\VBoxMouse.sys”时停止正常执行。除了反沙盒测试之外,我们还可以向代码中添加良性功能。这将使我们程序的真实目的更加令人困惑,并降低其与其他恶意软件的相似性。
最后,重要的是要记住反沙盒测试是一把双刃剑:如果我们确实隐藏了程序的真实性质,运行这种测试已经可以表明其恶意性质。

混淆导入地址表

由于我们的加载器在 Go 中,因此我们按如下方式加载 DLL 和函数:

此方法涉及在执行期间检索 DLL 上的句柄。这意味着所使用的函数和 DLL 未列在 IAT 中:

我们的功能不包含在 IAT 中
我们可以看到,尽管在代码中使用了“VirtualAllocEx”函数,但它并没有出现在 IAT 中。另一种方法是重新编码“GetProcAddress”函数的实现。
然而,需要注意的是,对于 Go 程序来说,GetProcAddress 函数无论如何都会出现在 IAT 中。因此,在我们的例子中,重新实现这个函数不一定有用。

AMSI 绕过:修补技术

我们已经看到“amsi.dll”DLL 被某些程序加载,并且其函数(例如“AmsiScanString”或“AmsiScanBuffer”)可用于检查某些条目是否可疑。
对于使用 AMSI 的 PowerShell 来说尤其如此。在以下场景中,攻击者打开了 PowerShell 控制台并知道相应进程的 PID(例如“4552”)。让我们探索如何使用 Go 代码禁用 AMSI。
我们首先为 PID“4552”定义一个变量,然后寻找“AmsiScanString”函数的地址:

我们还用字节 C3 创建了“patch”变量。在汇编语言中,C3 对应于“ret”指令,表示退出当前过程。
接下来,我们通过 PID 检索 PowerShell 进程的句柄:

然后我们使用“sys/windows”库中的“WriteProcessMemory”函数:

这样,在PID 4552进程的“AmsiScanBuffer”函数地址处,我们写入一个“ret”指令。
这样,每次调用此函数时,都会立即执行程序退出,从而有效地禁用此 AMSI 功能。
经过一些修改以修补其他 AMSI 函数后,我们可以测试我们的程序。在运行它之前,我们注意到 AMSI 阻止了包含字符串“amsiscanstring”的命令:

我们已被 AMSI 屏蔽


运行我们的程序后,AMSI 已被修补,不再阻止包含潜在恶意字符串的命令:

AMSI 已修复

ETW 绕过:修补技术

修补 ETW 的方法与 AMSI 的方法类似。所涉及的步骤如下:

  1. 获取我们自己进程的句柄。
  2. 确定链接到 ETW 的函数的地址(在此示例中为“EtwEventWrite”)。
  3. 对于每个地址,使用“WriteProcessMemory”函数添加“ret”汇编指令。
    每次调用修补的函数时,此技术都会触发过程返回,从而阻止日志数据被发送回 ETW。
    在此示例中,我们获取了要为 ETW 修补的进程(此处的 PID 为 9368)的句柄:

我们检索“EtwEventWrite”函数的地址,并使用字节 C3 创建“patch”变量,对应于“ret”指令:

最后,我们使用“WriteProcessMemory”函数来修补“EtwEventWrite”:

在应用补丁之前,我们可以看到“EtwEventWrite”函数运行正常:

修补前的 EtwEventWrite 函数
一旦补丁被应用,剩下的就只是“ret”指令,导致程序退出:

因此,我们设法正确修补了“EtwEventWrite”函数。但是,当我们检查“ntdll.dll”时,我们发现“EtwEventWrite”可能调用“EtwEventWriteFull”,而后者又可以调用“NtTraceEvent”并执行系统调用:

因此,修补 ETW 的更有效方法是将“ret”指令直接放置在“NtTraceEvent”级别。
还应该注意的是,该技术对于 ETWTI(ETW 威胁情报提供商)的特定情况不起作用,这可能是未来文章的主题。

绕过 API 挂钩

在第一部分中,我们探讨了某些安全解决方案如何挂接到某些 Windows API 函数,以便更深入地分析程序行为。
当我们开发程序时,我们希望尽可能避免这种分析。为此,我们尝试绕过此函数挂钩,因为我们希望在不经过这种分析的情况下使用程序所需的函数。
有多种方法可以实现这一点,这里我们介绍“间接系统调用”技术。但必须注意的是,这项技术只是绕过 API 挂钩的众多技术之一。
在解释什么是“间接系统调用”之前,了解“直接系统调用”背后的原理非常重要。
如上所述,Windows API 函数最终会调用以“Nt”(或“Zw”——为简单起见,在本文中我们将“Nt”和“Zw”视为类似)开头的函数来与 Windows 内核进行交互。
这些“Nt”函数可以进行“系统调用”,即允许调用其在 Windows 内核中的对应函数的系统调用。当一个新进程启动时,第一个加载的 DLL 通常是“ntdll.dll”,它导出了大部分“Nt”函数。
然后,任何 EDR 都可以加载其自己的 DLL 并挂钩“ntdll.dll”导出的函数,如上所述。
“直接系统调用”背后的原理如下:我们不会先搜索“ntdll.dll”的地址(例如,使用“GetModuleHandle”),然后再搜索想要使用的“Nt”函数的地址(例如,使用“GetProcAddress”),而是直接在代码中实现与所需“Nt”函数相对应的汇编代码。
这样,我们就不再需要“ntdll.dll”中的函数来执行系统调用了。
然而,实现这种技术有一个困难:每个负责系统调用的函数都与一个 SSN(系统调用服务号)相关联。这个 SSN 被传输到内核,使内核能够识别它需要执行的函数。
汇编程序中的“Nt”函数通常采用以下形式:

我们以“NtOpenProcess”为例:

来自 NtOpenProcess 的 SSN
在这里,我们看到 NtOpenProcess 的 SSN 是“26”(值“0x26”通过“mov eax, 26”指令放置在“eax”寄存器中,作为参数传递给内核)。
为“Nt”函数实现我们自己的汇编代码的主要困难在于,不同 Windows 系统的各种函数的 SSN 可能会有所不同。因此,在我们的代码中静态写入不同的 SSN 是一种冒险的选择,因为我们可能会遇到对应问题。一个更有趣的解决方案是动态计算不同系统调用的 SSN。
直接系统调用的危险
使用直接系统调用的一个问题是,一些系统调用指令存在于“ntdll.dll”之外,这是不寻常的行为。
安全解决方案可以查找“ntdll.dll”之外的系统调用,并在发生时触发警报。一种不太可能被检测到的实现是使用间接系统调用。
间接系统调用的工作方式相同,但有一个区别:我们不是直接从我们正在实现的汇编代码执行系统调用,而是执行跳转指令(jmp)到“ntdll.dll”中我们感兴趣的系统调用的内存地址。
而不是:

我们将有:

间接系统调用的另一个优点是,由于我们的系统调用的 SSN 已通过指令“mov eax, SSN”放置在“eax”寄存器中,因此我们可以执行跳转 (jmp) 到“ntdll.dll”中任何系统调用的地址。理想情况下,我们将使用属于我们实际想要使用的函数以外的函数的系统调用的地址。
例如,如果我们想对“NtOpenProcess”函数进行间接系统调用:

NtOpenProcess 系统调用地址

我们的跳转指令可能不是指向“NtOpenProcess (0x00007FFCD824D522)”系统调用的地址,而是指向“NtAllocateVirtualMemory (0x00007FFCD824D362)”系统调用:

NtAllocateVirtualMemory 系统调用地址

使用 Hell's Gate 技术动态解决 SSN
正如我们所见,理想情况下我们希望能够动态解析 SSN。在解释“地狱之门”的工作原理之前,重要的是要指定与每个系统调用相关的 SSN 是增量的。
为了更清楚,我们来看下面的例子:

我们可以看到,每个“Nt”(或 Zw)函数的 SSN 等同于前一个 +1 函数的 SSN。
现在让我们解释一下地狱之门的工作原理。该技术的工作原理如下:

● 我们定义两个结构:
  ○ _VX_TABLE_ENTRY:包含函数地址、相应函数名称的哈希值及其 SSN。
  ○ _VX_TABLE:包含所有 _VX_TABLE_ENTRY 的列表(每个 Nt 函数一个)。
● 对于每个“Nt”函数名,我们应用一个哈希函数 (djb2)。“哈希 -> 函数名 Nt”匹配列表将保留以供将来使用。
● 我们通过 RtlGetThreadEnvironmentBlock 访问 TEB(线程环境块),其中包含 PEB(进程环境块)。
● 从PEB,我们可以访问“ntdll.dll”的EAT(导出地址表:所有导出函数的列表)。
● 对于在 ntdll.dll 的 EAT 中找到的每个函数名称,我们都应用一个哈希函数 (djb2)。
● 我们将这些哈希值与第二步中建立的匹配列表中包含的哈希值进行比较。
● 如果两个哈希值匹配,我们将初始化 _VX_TABLE_ENTRY 的地址和哈希名称元素。然后,将此 _VX_TABLE_ENTRY 添加到 _VX_TABLE。
● 对于 _VX_TABLE 中的每个 _VX_TABLE_ENTRY,我们转到函数地址。然后,我们查找字节序列:0x4c、0x8b、0xd1、0xb8,对应于指令“mov r10, rcx”和“mov eax, SSN”。如果未找到此字节序列(表示可能存在钩子),我们将转到下一个地址,直到找到正确的模式。找到后,我们将初始化当前 _VX_TABLE_ENTRY 的最后一个 SSN 元素。如果我们到达对应于“syscall”和“ret”指令的字节序列 0x4c、0x8b、0xd1、0xb8,则意味着我们已通过 SSN 而未找到它。因此,对相关 SSN 的解析失败,我们将转到下一个 _VX_TABLE_ENTRY。
对每个_VX_TABLE_ENTRY执行这些操作后,我们得到一个动态计算的“函数名Nt->SSN”映射表。

结论

总之,值得注意的是,防病毒逃避领域正在不断发展。
面对安全解决方案的进步,当前有效的技术很快就会过时。防病毒/EDR 规避仍然很复杂,需要高度定制并结合使用多种技术。这些包括:
● 内核回调如何工作。
● EtwTI 补丁可能需要利用易受攻击的驱动程序(BYOVD 技术 - 自带易受攻击的驱动程序)以及 Patchguard 的工作原理。
● 用于动态解析 SSN 的更高级版本,例如 HalosGate(地狱之门的演变)。

0 条评论
某人
表情
可输入 255