【翻译】Bypass AMSI in 2025 - r-tec Cyber Security
引言
自从我发表第一篇关于绕过 Antimalware Scan Interface (AMSI) 的博客文章以来,已经过去了四年多。这些文章分别是通过手动修改绕过和 Powershell 与.NET 特定绕过的区别:
-
https://s3cur3th1ssh1t.github.io/Bypass_AMSI_by_manual_modification/ -
https://s3cur3th1ssh1t.github.io/Powershell-and-the-.NET-AMSI-Interface/
自 2020 年以来,许多新的绕过方法被发布,可以作为之前方法的替代选择。这篇博文将简要介绍 AMSI 的工作原理(虽然简单但希望易于理解),以及如何在四年多后仍然有效地绕过它。有什么变化吗?剧透:只是部分变化:-)
1. 什么时候需要绕过 AMSI?
尽管 AMSI 已经在许多论文和工具中被详细分析和描述,但我仍然对社区中存在如此多的困惑和误解感到惊讶。例如,GitHub 上发布了许多 shellcode 加载器,它们只是执行 shellcode。但 README 中也声称它包含 AMSI 绕过,这就是为什么它永远不会被检测到。因此,至少在 GitHub 或社交网络上存在大量误解,这可能会导致越来越多的人产生困惑。
我们什么时候真正需要使用 AMSI 绕过?至少对于 shellcode 执行来说 - 我们不需要。正如我四年多前在博客中写的那样,AMSI 主要用于在运行时分析脚本语言和.NET 托管代码,例如:
-
Powershell -
VBS -
Javascript -
VBA macros -
C# assemblies
因此,如果你使用 Command & Control Framework 的 payload,并且主要从那里运行 BOF 或 COFF,你永远不需要绕过 AMSI。如果你在加载器中实现绕过,你只会增加 IoC 和被绕过尝试检测到的可能性。除非真的需要,否则最好不要使用绕过!
另一方面,如果你想运行已知恶意和未混淆的公开工具,例如来自 GitHub 的上述任何语言的工具,或在你自己的工具中重用它们的代码,你将需要绕过 AMSI 才能运行这些工具。你要通过 Powershell 中的 Invoke expression
执行 GitHub 脚本吗?你要通过 assembly::load()
加载.NET 程序集吗?创建恶意 office 宏?通过 mshta.exe
、cscript.exe
或 wscript.exe
将脚本加载到内存中?你很可能需要 AMSI 绕过。
2. AMSI 如何工作 - 以及如何绕过它
AMSI 主要基于签名检测。与"经典"的基于签名检测方法的主要区别在于,AMSI 的签名检测是在运行时进行的,会在每次有潜在恶意内容从内存中加载时触发。根据其架构,AMSI 在某些情况下当内容从磁盘加载到内存中时不会触发扫描,正如 IBM X-Force Red 最近指出的那样。
AMSI 签名可能是什么样的?它们可以是简单的字符串,如 Invoke mimic set
,也可以是字节数组,如用于经典 AmsiScanBuffer
补丁的字节:
[byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3)
对于C#程序集来说,这可以是特定的十六进制字节序列,但根据我的经验,AMSI 也使用类似 Yara 规则的模式匹配方式,或至少是正则表达式。但如果 AMSI 是基于签名的,这也意味着我们总是可以通过修改代码来绕过它。如果你改变代码使签名不再匹配,你就可以绕过 AMSI。就是这么"简单",尽管有时候找出签名很困难。
绕过 AMSI 检测的另一种方法是以某种方式破坏 amsi.dll
或其他参与扫描过程的库的功能,或完全阻止 DLL 加载。这就是几乎所有公开记录的绕过方法的原理。它们主要在用于破坏功能的技术上有所不同,比如:
-
修补内存区域(从我的角度来看也包括钩子) -
使用向量化异常处理程序和硬件断点等来操纵工作流程 -
生成新进程并以各种方式阻止相关 DLL 的加载 -
在 CLR 启动和/或 AMSI 初始化之前阻止相关 DLL 的加载
这些操作通常在运行时执行。IoC 以及这些绕过的检测依赖于:
-
绕过代码的签名,或者 -
运行时检测,如用户层钩子、ETWti 或内存扫描。
3. 使用公开混淆器的缺点
当我写第一篇关于 AMSI 绕过的博客时,使用公开的混淆器来规避检测仍然是可能的。现在情况是否依然如此?让我们做一个简单的实验。为此,我让 ChatGPT 生成了一个随机的 Powershell 脚本,由于显而易见的原因 - 它完全不是恶意的,在 Virustotal 上获得了 0 个检测。
在使用 Invoke-Obfuscation
混淆后再次上传到 VirusTotal,它获得了 1 个检测,而不是之前的 0 个检测。乍看之下,似乎只有一个供应商有某种通用规则来检测 Invoke-Obfuscation
的混淆。然而,由于匹配到了使用 Invoke-Obfuscation
的 TOKEN OBFUSCATION
,触发了一个 Sigma 规则:
图 1:SIGMA 规则触发
混淆对已知恶意的 GitHub 工具效果如何?让我们看两个例子:
原始 GitHub 版本
-
来自 PowersharpPack 的 Invoke-Rubeus - 撰写本文时有 29 个检测。 -
WinPwn - 撰写本文时有 19 个检测
Invoke-Obfuscation 混淆版本
-
来自 PowersharpPack 的 Invoke-Rubeus - 10 个检测 -
WinPwn - 首次上传时有 2 个检测
正如我们所见,近五年后仍然可以使用相同的公开混淆器来规避基于签名的检测。哇,说实话我没想到会这样,我以为现在会有更多针对 Invoke-Obfuscation
的特定检测。注意: 在完成这篇博文后,我尝试了一些 EDR 和上述脚本 - 结果发现,有些 EDR 确实有针对 Invoke-Obfuscation 的专门 AMSI 签名!即使是第一个非恶意脚本也被标记为恶意。这意味着,VirusTotal 不会显示基于 AMSI 的检测,而只会显示基于文件本身签名的检测。这一事实使得 Invoke-Obfuscation
对这些供应商来说完全无用。
WinPwn
在多个地方出现故障,因此内置的 AMSI 绕过等功能不再工作:
图 2:部分脚本执行故障
但菜单仍然显示,原生 Powershell 功能可以正常使用。
混淆后的 Invoke-Rubeus
版本首先在这里出现故障,因为混淆导致类型 [dreIKOpFhund.pROGRam]
被放在函数之前作为变量,而这些命名空间和类名在此时无法正确解析。为了修复这个问题,我手动将这部分放在函数本身内部,位于混淆的 [assembly::load]
行之后:
图 3:Invoke-Rubeus 的混淆修复
这显示了第一个缺点,即混淆器可能会破坏我们的代码,需要手动调整才能正常工作。我们是否成功绕过了 Defender 的 AMSI?
图 4:脚本加载/执行
乍看之下似乎我们成功了,但这只是针对 Powershell 脚本本身,而不是运行时调用的 Rubeus 程序集。为什么会这样?这在我的第二篇私人博客中已经描述过。所以在这种情况下,我们必须先混淆程序集,嵌入它,然后再混淆 Powershell 脚本。而且我们需要对每个程序集/脚本都这样做。使用现有的 AMSI 绕过是否更容易?
4. 使用哪种绕过方法?
在我第一篇博客文章发布时,我的 Amsi Bypass Powershell 仓库包含了 15 种不同的绕过方法。四年多后,它现在包含 23 种不同的代码片段,增加了 8 种。而这些仅仅是已经公开发布的技术,包括 Powershell 代码。我有意没有添加其他已发布的绕过方法,例如在 CLR 完全启动之前使用的原生语言方法,比如我自己的 Ruy-Lopez。
因此,技术数量已经大幅增加。但在 2025 年,哪些方法真正有效,哪些无效?我们如何评估这一点?一般来说,所有公开的绕过方法本身都已被签名并被 AMSI 标记,至少在使用 Powershell 等上述脚本语言实现时是这样。因此,对于所有这些方法,有必要手动修改或混淆代码,使绕过本身不再被标记。这是一项艰苦的工作,需要不断尝试和犯错。但这种方法的问题在于,不同供应商有不同的签名,即使你修改后的绕过方法对一个供应商有效,对下一个可能就失效了。我自己做了几年这样的工作,但最终意识到这太费力了。
首选语言
另一方面,坚持使用原生语言的优势在于你的代码不会被 AMSI 本身扫描。相反,你将不得不处理针对磁盘上二进制文件/DLL 的"传统"基于签名的检测。你需要使用字符串混淆/加密、反模拟、反沙箱技术以及类似于脚本语言的用户层钩子绕过和所选的绕过方法。此外,CLR 默认不会加载到原生进程中,AMSI 也不会被初始化,这通常提供了更多可选的绕过选项。使用原生编程语言已经成为我现在绕过 AMSI 的首选方式,但这需要更多关于需要注意什么的背景知识以及 Windows API 编程的一般知识。
什么仍然有效
我们如何"评价"有效性?如前所述,所有公开的绕过方法都可以被修改以绕过磁盘上基于签名的检测以及 AMSI 本身。但对于某些技术,来自各种供应商的运行时检测变得越来越重要。这些检测不能像基于签名的检测那样容易被绕过。
补丁修改
如果你选择使用补丁,你将面临用户层钩子,它们会阻止你修改 amsi.dll
的内存权限或向其内存写入数据。你需要通过使用解钩、间接系统调用或类似方法来绕过这些限制。
即使在完成这些操作后,你可能仍然面临针对补丁的 ETWti/内存扫描检测。一个很好的例子是最近 Microsoft Defender 对经典 AmsiScanBuffer 补丁 的检测。每当 AmsiScanBuffer
函数(或其他几个函数)被修改为仅 return
时,就会触发警报并终止你的进程。AV/EDR 可以通过 ETWti 事件简单地看到,例如 amsi.dll
中的 AmsiScanBuffer
的保护被修改,并且数据被写入到这个位置。你无法从用户层绕过这些事件,因为它们是在内核层发出的。之后,AV/EDR 可以扫描函数位置以实际验证是否执行了恶意操作(就绕过而言)。最终,这意味着你不应该坚持使用这种特定的补丁,因为无论使用什么用户层规避技术,你都可能被检测到。
注意:这种针对入口点补丁的检测已经被其他几个 EDR 供应商使用了几年,但当 Defender 引入它时因其广泛使用而受到更多关注。
使用硬件断点
由于上述补丁部分的发现,社区中的人们提出了使用硬件断点的想法。它们有很大的优势,不需要绕过用户层钩子,目标 DLL 的完整性保持有效,内存扫描器无法检测到操作。
根据我的经验,很少有供应商能在运行时检测到使用硬件断点绕过 AMSI 的行为。然而,理论上,硬件断点可以通过检查调试寄存器值轻松检测到——例如,如果其中一个设置为 AmsiScanBuffer
地址,就可能触发警报。理论与实践,我从未遇到过这样的检测,也许是因为误报率?然而,最近一些供应商通过 SetThreadContext
开发了基于 ETWti 的检测,如此处所述。
总体而言,根据我的经验,使用硬件断点对大多数 AV/EDR 供应商来说仍被认为是操作安全的,因此是推荐的方法。然而,随着新检测的出现,这种情况随时可能改变,猫鼠游戏继续:-)
阻止 DLL 加载
有一些已发布的技术可以阻止 AMSI 相关 DLL 的加载,使初始化和扫描永远不会发生。如上所述,这主要可以由原生语言或新生成的进程使用,因为在这些情况下,加载和初始化尚未完成。例如:
-
使用 DEBUG_PROCESS
标志创建新进程,并在LOAD_DLL_DEBUG_EVENT
上使用 SharpBlock 修补入口点 -
在 DLL 加载过程中钩住函数以返回失败,以 NtCreateSection 为例 -
在 DLL 加载过程中钩住函数,使新生成的进程返回失败,使用 Ruy-Lopez
虽然钩子通常很容易被 EDR 发现,但我不知道有任何供应商会标记新设置的钩子,可能也是由于误报率的原因。即使他们确实对新钩子发出警报,也可以使用硬件断点来达到相同的效果。因此,据我所知,这些技术没有任何基于运行时的检测,至今仍然有效。
针对特定目标的替代方案
根据 AMSI 绕过目标(例如 Powershell 或 C# 程序集),可以使用几种其他替代方案。在许多情况下,仍然是打补丁——但在不同的偏移量/位置。关于 Powershell,以下图表反映了我在撰写本博客时的个人经验——不要让我负责:
第一个绕过现在是一个特殊情况。当它发布时,它对 Powershell 脚本和加载的 .NET
程序集都有效。但在发布后,Microsoft 从 Powershell 内部进行了调整,使其不再影响脚本,而仅影响 .NET
程序集。因此,这可以与橙色标记的绕过方法之一结合使用,或者如果你的脚本没有被标记并且它加载程序集——那也没问题。
对于所有绿色标记的方法,你只需要混淆/修改源代码以规避签名,就可以了。红色标记的方法由于基于补丁的检测,现在更容易被标记。橙色标记的方法只对原生 Powershell 脚本有帮助,但一旦调用 assembly::load
,AMSI 就完全没有被绕过。在某些情况下,你可能还需要移除 Add-Type
并坚持使用原生 Powershell 替代方案。极少数供应商还使用 clr.dll
钩子,在这些情况下,你可能也会因为基于行为的检测而失败,需要解钩 clr.dll
。
Provider Patch 在我的仓库中有两个代码片段,使用 Add-Type
的只对 Powershell 脚本有效,使用反射的对脚本和 .NET
程序集都有效。
如你所见,绿色/橙色标记的方法仍然包含一些基于补丁的绕过。但这些不太为人所知/使用,因此根据我的经验,不会被内存扫描检查/发现。
在少数情况下,EDR 供应商甚至不再依赖 amsi.dll
进行扫描。任何针对这个特定 DLL 的绕过都不会产生任何绕过效果。在这些情况下,你需要通过注册表或内存遍历来枚举他们的 AMSI 提供程序 DLL,并修补它或者修补自定义 AMSI DLL。更多信息可以在 2022 年的这个黑帽演讲中找到。
上面链接的 IBM 的 C# 程序集特定 AMSI 绕过现在应该也不会在运行时被标记。这种"欺骗" CLR 从磁盘加载程序集的整个概念并不新鲜,已经在 2021 年与另一个 .NET
特定绕过一起发布。自那次发布以来,Windows Defender 的行为没有改变,通过 PoC SharpTransactedLoad 加载程序集仍然有效。然而,一些 EDR 供应商的行为不同,也对从磁盘加载的程序集应用 AMSI 扫描,所以至少 2021 年的 PoC 不再完全操作安全。
5. AmsiScanbuffer 补丁真的失效了吗?
在阅读了一篇尝试绕过最近 Defender 补丁检测的博客文章后,我很好奇自己深入研究这些检测。还有什么可能导致这种检测?最著名的公开补丁在 AmsiScanBuffer
入口点的某处使用 0xC3
(RET)来退出函数并返回INVALID ARG
,使调用者认为没有发现恶意内容。让我们在这里玩一玩,仅供娱乐。
只是入口点吗?
如我之前所说,我相信 Defender 在采取行动前使用内存扫描来检查恶意行为。那么内存扫描是否只检查函数的开头?入口点是什么样子的?
图 6:AmsiScanBuffer 入口点
首先将输入参数推入栈中。如果我们在 push r15
之后打补丁,并在返回函数前将已推入的寄存器从栈中弹出,同时返回 INVALID ARG
,会怎样?如下所示:
图 7:偏移量 0x14 处的替代补丁字节
代码如下所示:
$Win32 = @" using System; using System.Runtime.InteropServices; public class Win32 { [DllImport("kernel32")] public static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport("kernel32")] public static extern IntPtr LoadLibrary(string name); [DllImport("kernel32")] public static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); } "@ Add-Type $Win32 $LoadLibrary = [Win32]::LoadLibrary("am" + "si.dll") $Address = [Win32]::GetProcAddress($LoadLibrary, "Amsi" + "Scan" + "Buffer") $p = 0 [Win32]::VirtualProtect($Address, [uint32]5, 0x40, [ref]$p) # POP R14 # POP R15 # POP RDI $Patch = [Byte[]] (0x41, 0x5F, 0x41, 0x5E, 0x5F, 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3) $Address = [Int64]$Address + 0x14 $new = [System.Runtime.InteropServices.Marshal] $new::Copy($Patch, 0, $Address, 11)
结果是 -> 我们不再被标记,成功绕过了 AMSI:
这验证了这种检测与入口点有关,并且只在那里进行某种验证。那么,如果提前返回被标记的情况下,我们是否有替代 0xC3
的方案?让我们再次检查输入参数:
第三个输入参数是要扫描的缓冲区长度。如果我们将其设置为 0 会怎样?这应该会导致扫描 0 字节的大小,对吧?这样我们的脚本或程序集就完全不会被检测到。AmsiScanBuffer 函数将输入参数从 r8
寄存器移动并放入 edi
寄存器,如下所示:
我们可以用 sub edi edi
替换 mov edi, r8d
来清除其值,如下所示:
再次,结果是一个有效的绕过,而且我们的进程没有被终止:
图 12:无内存扫描触发的工作绕过演示
有趣的是:你可能还记得 Defender 过去会将字符串 amsiscanbuffer
、amsi.dll
和补丁字节标记为恶意,对吧?现在不再是这样了,因为这个新引入的检测现在是查找和防止被修补的 AmsiScanBuffer
函数的主要方式。所以这些"旧"签名现在已经被内存扫描所取代。
这两种展示的绕过方法 - 以及其补丁的内存签名 - 理论上在本博客发布后很容易添加,所以不要期望它们能维持太久。但好消息是还有几十种其他的补丁替代方案。你只需要在调整补丁偏移和字节时发挥创意。
6. 结论
几年前用于绕过 AMSI 的许多方法在几年后的今天仍然适用。这主要还是关于如何通过手动修改或混淆来绕过签名检测。多年前的混淆工具由于某些原因仍然没有被通用签名覆盖。但是一些 EDR 供应商确实针对这些混淆工具构建了基于 AMSI 的签名,这实际上使得未经修改的混淆工具变得无效。然而,一般的修改或混淆仍然足以完全规避 AMSI 检测,但由于有许多不同的供应商,因此有不同的签名数据库,很难确保它们都被绕过。
另外,在运行时操作参与 AMSI 进程的 DLL 会导致通用绕过,这样已知的恶意脚本或程序集就可以被加载。已发布的绕过主要使用内存补丁或带有硬件断点的向量化异常处理程序等来在运行时操作扫描或初始化过程。其他一些则依赖于操作 DLL 加载过程 - 要么是在 AMSI 尚未初始化时,要么是针对新生成的进程。
2025 年什么是有效的?从我的角度来看,有效性可以从基于行为的检测方面来衡量,因为所有绕过都可以很容易地修改以避免基于签名的检测。根据我的经验,在 amsi.dll
函数的入口点使用补丁不再被认为是安全的,因为几年来已经有几个供应商通过由内核事件触发的内存扫描来检测这些补丁。在撰写本文时,使用硬件断点可以被认为是更加 OpSec 安全的,但供应商也开始对此使用基于行为的检测,猫鼠游戏仍在继续。在加载之前操作 DLL 加载过程或 AMSI 初始化尚未被行为检测,但只能在_初始化之前_或对_新生成的进程_使用。
虽然由于内存扫描检测的存在,在入口点打补丁不再被认为是安全的做法,但在自定义偏移处对 amsi.dll
打补丁仍然是一种有效的方法。修补 clr.dll
或其他参与 AMSI 进程的 DLL 的替代方案通常也不会触发基于内存扫描的检测。那么补丁是否已死?我认为远未死亡。
原文始发于微信公众号(securitainment):绕过 AMSI - 2025 版
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论