防病毒、反恶意软件和EDR是预防攻击的常用工具。但它们可以被绕过,本文探讨了在加载程序中实现的各种绕过技术。加载程序是一种通过绕过保护措施执行恶意负载的程序。文章将展示这些技术如何帮助渗透测试团队,并介绍各种可能的安全措施及其绕过技术
在我们的审计过程中,特别是在进行基础设施和网络渗透测试时,我们的安全专家可能需要在安装了防病毒软件的环境中运行特定的评估工具。这些工具,如用于评估Active Directory安全性的Rubeus和SharpHound等,由于其功能与实际网络攻击中可能使用的工具相似,通常会被现有的安全解决方案逻辑性地阻止运行。然而,为了确保能够准确地进行入侵测试并发现潜在的安全漏洞,这些工具的使用是不可或缺的。因此,找到一种方法来绕过现有的保护措施,以便在这些受保护的环境中运行这些关键工具,就显得尤为重要。此时,加载程序(Loader)的应用就显得尤为关键。加载程序能够获取到防病毒软件所检测到的计算机程序,并通过某种方式将其转换为未被防病毒软件检测到的形式。这样,我们就可以在不触发安全警报的情况下,顺利地在这些环境中运行所需的评估工具,从而确保我们的渗透测试工作能够顺利进行。
1防病毒/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”不会触发警报(因为后者不包含有问题的字符串):
如果我们运行“mimikatz.exe”程序,我们会看到该字符串确实存在:
Yara 规则已正确检测到字符串。因此很明显,可以通过对程序的静态分析来将其描述为威胁。
启发式和行为检测
启发式检测的目的是了解程序的工作原理并确定它将在系统上执行什么操作。这可以通过使用沙盒来实现:一个隔离的虚拟机,潜在危险的程序可以在其中运行。然后,防病毒软件可以检查程序在执行过程中采取的操作并寻找恶意操作的迹象。类似地,行为检测包括观察程序在执行期间执行的操作,以发现任何可疑活动。例如,按特定顺序对 Windows API 进行的某些调用被认为是典型的恶意软件模式。
导入地址表 (IAT) 检查
在编写计算机程序时,开发人员可以调用库。在 Windows 系统上,这是通过 DLL(动态链接库)实现的。DLL 是一个导出现成函数的程序。然后,开发人员可以选择将某些 DLL 加载到他们的程序中,以利用它们导出的函数。一定数量的 DLL(例如“user32.dll”或“kernel32.dll”)是所有 Windows 系统的标准配置,它们导出对开发人员有用的函数。这些函数由 Microsoft 记录并构成通常所熟知的 Windows API。例如,开发人员可以使用“user32.dll”DLL导出的“MessageBoxA”函数来显示对话框:
导入地址表是与每个可移植可执行文件(包含在 PE 的可选头的导入目录中)相关的表,其中包含程序使用的已加载 DLL 及其导出函数的列表。下面的示例显示了“notepad.exe”程序的导入地址表的启动:
Notepad.exe IAT 内容的开始。大多数防病毒软件都会观察该表,某些功能的存在会触发警报。例如,如果“OpenProcess”、“VirtualAllocEx”、“WriteProcessMemory”和“CreateRemoteThreadEx”(均由“kernel32.dll”导出)函数同时存在于同一个程序中,则会引起高度怀疑。这 4 个函数一起使用可启用恶意程序经常使用的进程注入技术。
反恶意软件扫描接口 (AMSI)
AMSI(Microsoft Antimalware Scan Interface)是 Windows 操作系统提供的一个接口,它允许开发人员将防病毒保护集成到他们的程序中。具体来说,开发人员可以选择将“AMSI.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 报告的流量来检测恶意活动的机会。
2.ETW 由三个不同的组件组成
提供者
在Windows操作系统中,无论是系统组件还是第三方应用程序,都可以通过其代码将事件发送给提供程序。以Windows事件跟踪-威胁情报提供程序为例,我们可以观察到与关键功能相关的Windows代码中的多个地方都有与之相关的函数调用。例如,“MiReadWriteVirtualMemory”函数就调用了“EtwTiLogReadWriteVm”。通过使用IDA(一种反汇编工具),我们可以在Windows内核组件“ntoskrnl.exe”上观察到这一点。这为我们提供了深入了解Windows事件跟踪-威胁情报提供程序如何与Windows代码交互的机会。
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’中观察到这种现象:
现在让我们看看“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(Endpoint Detection and Response)可以自由执行其认为必要的安全测试,并能够监视对 Windows API 函数的任何调用。回顾本文前面讨论过的地址导入表 (IAT),即使攻击者没有让可疑函数出现在 IAT 中,API 挂钩技术仍然会检测到它!因此,这种方法对付恶意程序非常有效。网络检测是另一种安全解决方案,它可以监控机器的连接并根据某些指标阻止威胁。例如,如果某个程序发起与已知与恶意服务器关联的 IP 地址的连接,则可能会决定进行阻止。这通过阻止恶意软件与危险服务器通信来增强安全性。有许多不同的防病毒和 EDR 绕过技术。在第二部分中,我们将探讨绕过这些保护的不同方法。基于签名的检测是一种常见的技术,它基于给定程序上的哈希函数生成的数字签名。如果我们更改程序中的哪怕一位数据,其签名也会完全不同。因此,绕过这种保护措施相当容易。例如,我们可以改变程序中某个变量的名称,这将导致不同的签名,从而避免基于该签名的检测。逃避恶意软件的静态检测在技术上并不一定复杂,但可能很耗时。目的是修改某些可检测到的元素,例如函数名称。
以下为 Go 程序示例:
我们可以使用以下命令来编译程序:$ go build helloWorld.go
。完成编译后,我们可以通过在 Linux 上运行 strings
命令并结合 grep
来查找特定的函数名称:
1 bash 2 $ strings helloWorld | grep myHello main.myHelloWorldFunc main.myHelloWorldFunc 3
从输出中我们可以看到,“myHelloWorldFunc”函数的名称清晰可见。为了避免这种情况,一种解决方案是手动更改此函数的名称。然而,在大型程序中,重命名每个函数会花费大量时间。为了避免浪费时间,我们可以尝试自动执行此任务。使用 Go 语言,可以使用出色的“garble”库来混淆 Go 二进制文件。让我们来看一下如何使用“garble”库来编译“helloWorld”程序:
我们可以看到“myHelloWorldFunc”函数的名称不再以纯文本显示。Garble 进行了以下更改:
1. 替换了尽可能多的哈希标识符。
2. 用哈希值打包路径。
3. 带有哈希值的文件名和位置信息。
4. 删除所有构建和模块信息。
5. 通过 -ldflags=”-w -s” 选项删除调试信息和符号表。
6. 如果给出了 -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,攻击者可以通过以下步骤禁用 AMSI。首先,我们需要知道相应进程的 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 的方法类似。所涉及的步骤如下:
获取我们自己进程的句柄。确定链接到 ETW 的函数的地址(在此示例中为“EtwEventWrite”)。对于每个地址,使用“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”)开头的函数来与 Windows 内核进行交互。这些“Nt”函数可以进行“系统调用”,即允许调用其在 Windows 内核中的对应函数的系统调用。当一个新进程启动时,第一个加载的 DLL 通常是“ntdll.dll”,它导出了大多数“Nt”函数。然后,任何 EDR 都可以加载其自己的 DLL 并挂钩“ntdll.dll”导出的函数,如上所述。
实现“直接系统调用”的原理如下:我们不会先搜索“ntdll.dll”的地址(例如,使用“GetModuleHandle”),然后再搜索想要使用的“Nt”函数的地址(例如,使用“GetProcAddress”),而是直接在代码中实现与所需“Nt”函数相对应的汇编代码。这样,我们就不再需要“ntdll.dll”中的函数来执行系统调用了。然而,实现这种技术有一个困难:每个负责系统调用的函数都与一个 SSN(系统调用服务号)相关联。这个 SSN 被传输到内核,使内核能够识别它需要执行的函数。
我们以“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 是增量的。为了更清楚,我们来看下面的例子:
SSN 是递增的。
我们注意到,每个“Nt”函数(或 Zw)的 SSN 都等于前一个函数的 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
。完成这些操作后,我们得到一个动态计算的“函数名Nt->SSN”映射表。
结论
值得注意的是,逃避防病毒领域正在不断发展。面对安全解决方案的进步,当前有效的技术很快就会过时。防病毒/EDR 规避仍然很复杂,需要高度定制并结合使用多种技术。本文讨论的策略仅代表可用方法的一部分,不一定是 2024 年最有效的方法。后续文章可能会探讨更多最新概念和技术,包括内核回调如何工作、EtwTI 补丁可能需要利用易受攻击的驱动程序(BYOVD 技术 - 自带易受攻击的驱动程序)以及 Patchguard 的工作原理,以及用于动态解析 SSN 的更高级版本,例如 HalosGate(地狱之门的演变)。