
1. 项目概述为什么我们需要关注AMSI与ETW绕过如果你在Windows环境下做过渗透测试或者红队评估大概率对PowerShell又爱又恨。爱的是它强大的系统管理、自动化能力和与.NET生态的无缝集成恨的是微软为了安全给它套上的层层枷锁。其中AMSI反恶意软件扫描接口和ETWWindows事件跟踪就是两把最锋利的“达摩克利斯之剑”时刻悬在攻击性PowerShell脚本的头顶。我最近在整理和实战应用一个名为“RedTeaming_CheatSheet”的开源项目时对其中的AMSI/ETW绕过方法进行了深度研究和验证。这份指南不是简单的命令罗列而是结合我多年的实战经验拆解其原理、评估其有效性、并分享在真实对抗环境下的应用技巧与注意事项。简单来说AMSI就像一个安装在PowerShell解释器门口的安检机。当你尝试执行一段脚本甚至只是一行命令时PowerShell引擎会先把脚本内容包括从网络下载的、从文件读取的、甚至是从命令行直接输入的送到这个“安检机”进行扫描。AMSI本身不判断好坏但它提供了一个标准接口让安装在系统上的杀毒软件如Windows Defender能够检查这些内容。一旦匹配到已知的恶意特征执行就会被阻断。而ETW则像一个无处不在的监控摄像头和录音笔它由Windows内核提供支持能够以极高的效率和极低的性能开销记录下系统中发生的各种事件包括每个PowerShell脚本块的解析、编译和执行。安全产品和EDR终端检测与响应解决方案可以订阅这些ETW事件从而实现对无文件攻击、内存攻击等高级威胁的深度可见性。因此一个成熟的攻击链或红队工具链如果不能有效处理AMSI和ETW就如同在布满摄像头的银行里蒙面抢劫——可能还没动手就被发现了。RedTeaming_CheatSheet中收集的方法正是为了在这种严密的监控下为红队成员和安全研究人员争取到宝贵的“操作窗口”。2. AMSI绕过原理深度解析与历史演进要有效绕过必须先理解其工作机制。AMSI的扫描发生在脚本内容被真正“执行”之前。对于PowerShell来说关键入口点是System.Management.Automation.AmsiUtils这个类。当PowerShell引擎准备执行脚本时会调用AmsiUtils.ScanContent方法。这个方法会将待执行的脚本内容、来源如脚本文件路径等信息打包通过Win32 APIAmsiScanBuffer或AmsiScanString发送给AMSI提供程序即杀软。2.1 经典的内存Patch方法及其失效早期最流行的绕过方法是直接修改amsi.dll在内存中的关键函数。其原理是在进程内存中找到amsi.dll的基址然后定位到AmsiScanBuffer或AmsiScanString函数的开头几个字节将其修改为直接返回“扫描安全”的结果通常是修改为mov eax, 0x80070057(E_INVALIDARG) 并立即返回或者直接ret。在PowerShell中这可以通过.NET的反射和Win32 API调用来实现。RedTeaming_CheatSheet中收录了一个经典的代码片段$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 $ptr [Win32]::GetProcAddress([Win32]::LoadLibrary(amsi.dll), AmsiScanBuffer) $oldProtection 0 [Win32]::VirtualProtect($ptr, [uint32]5, 0x40, [ref]$oldProtection) $patch [Byte[]] (0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3) # mov eax,0x80070057; ret [System.Runtime.InteropServices.Marshal]::Copy($patch, 0, $ptr, 6)这段代码的精妙之处在于它完全在内存中操作不触及磁盘文件。然而这种方法现在几乎完全失效。原因在于现代EDR和杀软会进行钩子检测和内存完整性校验。它们不仅在自己的进程中挂钩这些关键API还会监控这些API在内存中的代码段是否被异常修改。一旦检测到内存Patch会立即触发告警甚至终止进程。实操心得在现今2024年的环境中直接对amsi.dll进行内存Patch属于“噪音”极大的操作除非是针对特定、隔离的测试环境否则在真实的红队评估中应避免作为首选方案。它更适合作为理解AMSI机制的教学案例。2.2 基于上下文与混淆的绕过技术当“硬碰硬”的Patch失效后绕过思路转向了“欺骗”和“隐藏”。这类方法不直接攻击AMSI组件本身而是通过改变脚本的呈现形式使其逃过特征检测。字符串拆解与拼接这是最基本但曾经非常有效的方法。AMSI扫描的是提交给它的完整字符串。如果我们把敏感关键字如Invoke-Mimikatz、AmsiUtils拆分成多个部分在运行时再拼接起来就能绕过基于静态字符串匹配的扫描。# 示例拆分敏感词 $c ‘Invoke-‘ ‘Mimikatz‘ iex $c # 这行本身可能被扫描但拼接后的变量内容在传递给iex时可能绕过早期扫描不过现在的AMSI和杀软引擎已经能对简单的拼接进行动态分析和还原。强制降低脚本块日志级别与混淆PowerShell有一个叫做“脚本块日志记录”的功能它会记录执行的脚本块内容。通过PowerShell.exe -ExecutionPolicy Bypass -NoLogo -NonInteractive -NoProfile -WindowStyle Hidden -Command等方式启动时可以尝试附加一些降低日志级别的参数但更有效的是使用强大的混淆工具。RedTeaming_CheatSheet提到了使用Invoke-Obfuscation这类工具。它的原理是通过大量的编码Base64、Hex、ASCII值数组、字符串反转、插入大量无关注释和空白符、使用别名和通配符等方式将脚本变得面目全非同时保持功能不变。例如将Get-Process写成g*p利用通配符或者将命令转换为一个由字符ASCII码组成的数组然后拼接执行。注意事项过度混淆或使用公开的、特征明显的混淆模板如Invoke-Obfuscation的某些默认模板本身可能成为新的检测特征。高级的EDR会检测混淆模式而不仅仅是内容。因此定制化的、轻度的混淆往往比重度使用公开工具更有效。2.3 .NET程序集加载与反射的“降维打击”这是目前更高级、也更持久的一种绕过思路。AMSI主要扫描的是通过PowerShell引擎解释执行的脚本内容。如果我们不直接执行PowerShell脚本而是通过PowerShell加载一个.NET程序集DLL并在该程序集内部调用方法来完成恶意操作那么AMSI在PowerShell层面就“看”不到实质性的恶意内容。具体操作是先将后门或攻击工具编译为.NET DLL例如使用C#然后将这个DLL进行Base64编码或分割。在目标PowerShell会话中通过[System.Reflection.Assembly]::Load([System.Convert]::FromBase64String($base64String))来将DLL加载到当前AppDomain中最后通过反射调用其入口方法。# 假设 $EncodedCompressedAssembly 是经过压缩和Base64编码的恶意DLL $AssemblyBytes [System.Convert]::FromBase64String($EncodedCompressedAssembly) $LoadedAssembly [System.Reflection.Assembly]::Load($AssemblyBytes) $EntryPoint $LoadedAssembly.EntryPoint $EntryPoint.Invoke($null, (,[string[]] ()))这种方法之所以强大是因为AMSI扫描的是Load方法调用时的Base64字符串。只要这个字符串本身不包含明显的恶意特征可以通过编码、加密、分块传输进一步隐藏且加载的程序集在内存中解密后执行就能有效绕过。杀软虽然可以监控.NET程序集加载但判断一个内存中加载的程序集是否恶意比扫描脚本字符串要困难得多。3. ETW绕过从进程级禁用到提供程序劫持如果说AMSI是门口的安检ETW就是房间里的录音棚。ETW绕过的重要性甚至高于AMSI因为一个设计良好的EDR可以通过ETW捕获到绕过AMSI之后的行为。3.1 进程内ETW事件提供程序禁用PowerShell通过System.Management.Automation.Tracing.PSEtwLog类来提供ETW事件。一个直接的想法是阻止本PowerShell进程发出这些事件。RedTeaming_CheatSheet中提供了一种通过.NET反射来将关键ETW提供程序设置为null的方法# 尝试禁用PowerShell相关的ETW提供程序 [Reflection.Assembly]::LoadWithPartialName(System.Core) [System.Diagnostics.Eventing.EventProvider].GetField(m_enabled,‘NonPublic,Instance‘).SetValue([System.Management.Automation.Tracing.PSEtwLog]::Provider, $false)这段代码试图通过反射修改私有字段m_enabled来禁用提供程序。然而这种方法极不稳定且容易引发崩溃。因为ETW提供程序可能被多个线程使用粗暴地修改其内部状态会导致不可预知的后果。在较新版本的PowerShell和.NET中这种反射操作也可能因为JIT编译优化或字段布局改变而失败。3.2 更稳定的方法Patch ETW相关函数与AMSI类似更底层的方法是Patch内存中ETW事件报告的函数。在.NET中事件最终通过ntdll!EtwEventWrite或advapi32!EventWrite等Win32 API发出。理论上可以找到这些函数在内存中的地址并进行Patch使其什么都不做就返回成功。但是这面临着比AMSI Patch更严峻的挑战系统范围影响Patch系统DLL中的函数可能会影响同一进程内其他合法组件对ETW的使用导致程序行为异常增加被发现的概率。内核回调与受保护进程现代Windows安全机制如受保护的进程、内核回调会严密监控这些关键系统函数的完整性。用户态的Patch很容易被内核驱动检测到。多提供程序PowerShell和.NET运行时可能注册了多个ETW提供程序Patch一个可能不够。因此在真实对抗中单纯的ETW函数Patch风险极高通常只作为最后手段或在非常短期的交互式会话中使用。3.3 推荐方案内存操作与直接系统调用对于高对抗环境更可行的ETW绕过思路是“不触发”而非“禁用”。这意味着我们的恶意操作尽可能不调用会触发敏感ETW事件的公开API。直接系统调用不使用CreateRemoteThread等被所有EDR重点监控的Win32 API来注入线程而是通过汇编指令直接发起syscall调用底层的NtCreateThreadEx等函数。这可以绕过用户态的API监控钩子自然也绕过了基于这些API调用的ETW事件。RedTeaming_CheatSheet可能引用了一些通过C#和D/Invoke实现直接系统调用的项目。父进程欺骗与进程镂空通过创建处于挂起状态的合法进程如svchost.exe,notepad.exe然后将其内存替换为恶意载荷再恢复执行。这样产生的ETW事件会显示为合法进程的活动混淆了攻击行为的来源。使用非托管代码完全脱离PowerShell和.NET环境使用纯C/C编译的、经过混淆和反调试处理的二进制文件。这样的程序产生的ETW事件模式与正常软件无异除非其行为异常否则很难从海量事件中被筛选出来。核心技巧ETW绕过的黄金法则是“最小化异常事件产生”。与其费尽心思去关闭录音棚不如让自己的行动听起来像一段正常的背景噪音。这意味着要深入研究目标系统上正常软件产生的ETW事件模式并尽量模仿。4. 实战整合构建一个绕过AMSI和ETW的PowerShell Runner理解了原理我们来看如何将这些技术整合到一个可用的、针对现代环境的“Runner”中。这个Runner的目标是在尽可能不触发告警的情况下将一段加密的PowerShell脚本或.NET程序集加载到内存并执行。4.1 阶段一初始侦察与环境准备首先我们不能盲目执行。一个稳健的Runner应该先检查环境。# 检查AMSI初始化状态和当前运行策略 $AmsiInitialized [Ref].Assembly.GetType(‘System.Management.Automation.AmsiUtils‘).GetField(‘amsiInitFailed‘,‘NonPublic,Static‘).GetValue($null) $ExecutionPolicy Get-ExecutionPolicy Write-Host [*] AMSI Init Failed: $AmsiInitialized Write-Host [*] Current Execution Policy: $ExecutionPolicy # 检查常见的EDR/AV进程示例实际列表需定制 $SuspiciousProcesses (MsMpEng, CSFalconService, CylanceSvc, SentinelAgent) Get-Process | Where-Object {$SuspiciousProcesses -contains $_.ProcessName} | Select-Object Name, Id这个步骤帮助我们评估监控的严格程度。如果AMSI初始化失败可能意味着已经有其他攻击者或工具禁用了它。了解执行策略有助于决定是否需要使用-ExecutionPolicy Bypass。4.2 阶段二选择并应用AMSI绕过基于环境选择一个合适的AMSI绕过方法。对于中等安全环境一个基于混淆和.NET反射的混合方法可能更稳妥。方案A轻量级字符串混淆与反射加载将核心Payload例如一个PowerShell脚本进行Base64编码。对编码后的字符串进行简单的变换如每两个字符交换位置、或与一个固定密钥进行XOR。在Runner中包含一个反向变换的函数将字符串还原。使用[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($restoredString))得到原始脚本。关键一步不要使用IEX或Invoke-Expression。而是将脚本内容转换为一个ScriptBlock然后通过.GetNewClosure()和.Invoke()来执行。在某些情况下这比直接IEX触发的检测要少。$encodedPayload 你的混淆后Base64 $decodedBytes [System.Convert]::FromBase64String(($encodedPayload[-1..-$encodedPayload.Length] -join ‘‘)) # 简单反转示例 $decodedScript [System.Text.Encoding]::UTF8.GetString($decodedBytes) $scriptBlock [ScriptBlock]::Create($decodedScript) $scriptBlock.GetNewClosure().Invoke()方案B.NET程序集加载推荐用于复杂操作将你的后门功能用C#编写编译为DLL。使用工具如ConfuserEx等对DLL进行混淆和保护对抗内存扫描。将混淆后的DLL进行压缩如GZip和Base64编码。在Runner中解码、解压然后使用[System.Reflection.Assembly]::Load加载。通过反射调用其方法。调用时避免使用容易监控的方法名可以通过方法签名参数类型来查找。4.3 阶段三实施ETW缓和措施在加载和执行Payload的同时我们需要实施ETW缓和措施。如前所述完全禁用风险高我们采用“缓和”策略。清除PowerShell历史执行Clear-History并手动删除$env:APPDATA\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt文件。这减少在日志中留下命令行记录。脚本块日志绕过尝试在启动PowerShell时使用-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -Command参数组合。虽然不能完全禁用脚本块日志但可以减少一些默认上下文信息。内存操作确保Payload本身在内存中解密和执行避免在磁盘上留下明文的阶段文件。所有组件都通过网络下载或嵌入在Runner中。避免敏感API在Payload的C#代码中使用P/Invoke和直接系统调用如果涉及进程注入等避免使用Process.Start、WebClient.DownloadString等高频监控的API。4.4 阶段四清理与退出执行完毕后Runner应尝试清理痕迹。# 尝试清除刚才创建的变量和函数 Remove-Variable -Name encodedPayload, decodedBytes, decodedScript, scriptBlock -Force -ErrorAction SilentlyContinue [GC]::Collect() # 建议垃圾回收但不保证立即清除内存 # 注意无法彻底清除已加载的.NET程序集除非进程结束。重要的是要明白在内存中完全抹除痕迹非常困难。因此红队操作的核心原则是“快进快出”在达到目标后尽快退出或转移到更持久的、低监控的立足点。5. 对抗升级现代EDR的检测与应对策略安全是动态对抗。随着AMSI/ETW绕过技术的普及EDR也在进化。我们必须了解当前的检测维度才能思考下一步。行为检测EDR不再只依赖静态特征。它们会分析PowerShell进程的行为序列。例如一个进程在短时间内修改了amsi.dll的内存属性VirtualProtect、进行了内存Patch、然后立即加载一段Base64字符串并执行——这个行为链本身就极具恶意特征。内存扫描EDR可以定时或触发式地对进程内存进行扫描寻找已知的Shellcode模式、Mimikatz等工具的二进制特征甚至是某些绕过工具如某些公开的AMSI Patch代码的字节模式。父子进程关系与上下文异常一个来自winword.exe或outlook.exe的子进程powershell.exe执行了下载、解码、反射加载等一系列操作这比从cmd.exe或正常管理会话中启动的PowerShell更可疑。ETW深度分析高级EDR会分析ETW事件之间的关联和时序。即使你Patch了部分事件事件流的缺失或不连贯本身就可能成为告警点。应对策略武器化定制绝对不要使用从互联网直接下载的、未经修改的公开攻击工具或脚本。对其进行源码级的修改、混淆和重构。合法上下文模拟让你的攻击进程诞生于合法的上下文。例如通过COM对象劫持、计划任务伪装、服务DLL劫持等方式让恶意代码由svchost.exe、explorer.exe等系统核心进程加载这比一个孤立的powershell.exe要隐蔽得多。时序化与延迟将攻击步骤拆解并在步骤之间加入符合正常用户操作的、随机的延迟。避免在几秒内完成从投递到执行的全过程。使用白名单信任研究并利用应用程序控制策略的弱点或者将你的工具签名伪装成受信任的发布者这需要更高的权限和准备。关注无文件、无PowerShell的技术终极的绕过可能是不使用PowerShell。考虑使用VBA、JScript、Windows原生工具如bitsadmin,certutil、WMI、甚至.NET的csc.exe编译器动态编译执行代码。RedTeaming_CheatSheet中也应包含这些替代方案。6. 防御视角如何检测和防范此类绕过作为蓝队成员或安全运维了解攻击技术是为了更好地防御。针对上述绕过方法可以部署以下检测策略启用并集中分析PowerShell脚本块日志确保组策略中启用了PowerShell脚本块日志记录包括启用模块日志记录并将日志集中收集到SIEM如Splunk, Elastic Stack中。编写检测规则寻找典型的绕过特征命令中包含大量无关字符、编码字符串长串Base64。使用了[System.Reflection.Assembly]::Load。命令中出现了AmsiUtils、GetProcAddress、VirtualProtect等关键词。脚本块长度异常或结构混乱高熵值。部署具备行为分析能力的EDR选择能够监控进程行为序列、内存操作如对amsi.dll的PAGE_EXECUTE_READWRITE权限修改、以及异常的直接系统调用的EDR产品。实施应用程序控制使用Windows Defender应用程序控制或类似的白名单策略禁止未经签名的PowerShell脚本和.NET程序集执行。这能从根本上阻断大部分无文件攻击。限制PowerShell使用对于非管理员用户和前端工作站通过组策略限制PowerShell 2.0的使用因其安全性较差并限制PowerShell的脚本执行能力。只允许在必要的管理跳板机上使用完整的PowerShell功能。监控.NET程序集加载通过ETW或EDR工具监控非标准路径或从内存中加载的.NET程序集。特别关注由powershell.exe、cscript.exe等脚本宿主进程加载的程序集。网络层检测许多攻击Payload需要从网络下载。监控出站连接特别是到可疑域名或IP的、由脚本宿主进程发起的HTTPS/HTTP连接并使用SSL解密技术在合规前提下检查内容。防御是一个体系化工程没有银弹。通过结合日志分析、终端行为监控、网络流量检测和应用控制可以构建起多层防御显著提高攻击者的成本和被发现的风险。在我个人的红队演练经验中成功与否往往不取决于拥有最尖端的0day绕过技术而在于对基础技术的深刻理解、对目标环境的细致侦察以及将多种简单技术组合运用的创造力。RedTeaming_CheatSheet是一个优秀的起点但它提供的代码片段更像是“食材”。真正的“烹饪”艺术在于你如何根据目标的“口味”安全配置将这些食材以正确的顺序和火候进行处理最终做出一道不被“食物检测仪”安全产品发现的“佳肴”。时刻记住隐蔽性和可靠性往往比技术的复杂性更重要。在每次操作前问自己一个问题如果我是防守方我会如何发现这个行为