PowerShell4104攻防:双触发机制与内存级绕过

admin 2026-09-06 04:48:01 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章剖析PowerShell4104脚本块日志双触发机制:显式开启加120个内置可疑词兜底,未开审计也会留痕。红队可反射毒化进程内策略缓存或清空词表实现内存级静默且不写注册表,但绕过命令自身会先落4104;蓝队应确保采集Verbose级4104,对反射特征、断流与时间窗关联告警,并用CLM与受保护事件日志加固。 综合评分: 83 文章分类: 红队,安全运营,安全建设,实战经验,终端安全


PowerShell 4104 攻防:双触发机制与内存级绕过

原创

枪火·攻防技术 枪火·攻防技术

赛博57库

2026年9月4日 05:00 广东

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

| | | — | | POWERSHELL LOGGING · 脚本块日志攻防 · 4104 PowerShell 4104 攻防:双触发机制与内存级绕过 显式开启 + 120 词内置兜底的双触发 · 内存缓存毒化与词表清空 · 附蓝队狩猎与加固清单 |

| | | — | | ⚠️ 阅读须知 本文技术细节来自 MDSec《Exploring PowerShell AMSI and Logging Evasion》(Adam Chester, 2018-06)的源码级拆解与微软官方文档,仅供安全从业者学习研究。文中绕过手法与代码仅限授权环境测试,禁止用于任何未授权目标;较新版本 PowerShell 内部实现已有演进,请以实际环境验证。 |

蓝队把事件日志ID是 4104 内容当金矿:脚本块日志记录的是”即将执行的一段代码”,混淆在这里被还原成明文,是全链路里离攻击意图最近的常规日志;红队则把它当成必须回答的问题:怎么让 PowerShell 自己闭嘴,又不留下改注册表、改配置的痕迹?

本文沿 2018 年 MDSec 的源码级拆解(Adam Chester)把 4104 讲透:双触发机制、内置 120 词可疑词表、两条不碰注册表的内存级绕过方法,以及蓝队侧可落地的狩猎与加固思路。

本系列前篇《AMSI 攻防十年》讲了 AMSI 本身,这篇聚焦它旁边的脚本块日志。

01 · 先认识 4104:一条 Verbose 级别的”创建记录”

PowerShell 5.0(WMF 5.0)起,微软给 PowerShell 补上了三件套审计:模块日志(模块内命令调用细节)、脚本块日志(Script Block Logging,简称 SBL)与转录(记录会话完整输入输出)。其中 SBL 对应的事件就是 4104

事件本身长这样:

•  通道:Windows PowerShell 5.1(powershell.exe)落在 Microsoft-Windows-PowerShell/Operational;PowerShell 7+(pwsh)落在 PowerShellCore/Operational

•  EventId:4104(0x1008);Level = Verbose;Opcode = Create;Task = CommandStart;Keyword = Runspace

•  消息形如 “Creating Scriptblock text (n of N)”:脚本块文本过大时按块拆分,一条脚本对应多条 4104

•  关键字段:消息正文携带反混淆后的明文脚本块,另有 ScriptBlockId(GUID,可跨分片、跨调用关联)与来源 Path

开启方式(Windows PowerShell 5.1 路径):

| | | | — | — | | 手段 | 位置 | | 组策略 | 计算机配置 → 管理模板 → Windows 组件 → Windows PowerShell → “打开 PowerShell 脚本块日志记录” | | 注册表 | HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging → EnableScriptBlockLogging = 1 | | PowerShell 7 | 注册表路径换成 HKLM\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging,其余一致 |

策略窗口里还有一个可选勾选项”记录脚本块调用开始/停止事件”(对应注册表 EnableScriptBlockInvocationLogging),打开后对单条命令的调用生命周期产生额外起止记录,对追踪”哪条命令干了什么”很有用。

为什么蓝队看重它:SBL 的记录点选在脚本块创建这一时刻。攻击者的混淆最终要执行,就得在某个时点把还原后的明文交给引擎解析成新脚本块——这个新脚本块一旦创建就会被记录。所以 4104 里经常能看到 Invoke-Obfuscation、拼接、编码层层包裹之后最终要跑的那段明文,这正是狩猎想要的内容。而它的前提是——日志得开着。问题来了:很多环境”没开”,日志真的就没有吗?

02 · 双触发:你以为没开,其实在记

MDSec 对 System.Management.Automation 5.x 做了源码级拆解,结论颠覆”没开 GPO 就没日志”的直觉。先看事件入口的判定链(C# 语义):

| | | — | | // LogScriptBlockStart:每个新脚本块开始时的入口 bool force = false; if (scriptBlock._scriptBlockData.HasSuspiciousContent) { force = true;                 // ← 命中"可疑内容",强制记日志 } ScriptBlock.LogScriptBlockCreation(scriptBlock, force); // LogScriptBlockCreation 内部的关键判定: // if ((force || ShouldLog("EnableScriptBlockLogging")) && !HasLogged) { //     if (ScriptBlockLoggingExplicitlyDisabled() || 产品代码) return;  // ← 唯一的关门点 //     写入 4104 // } |

拆开看,触发记录有两个完全独立的来源:

01  显式开启:组策略/注册表把 EnableScriptBlockLogging 置 1,ShouldLog 恒真——所有新建脚本块都记;

02  内置可疑词兜底:脚本块 AST 的文本被 TokenizeWordElements 切词后,与一个硬编码在程序集里的静态集合 ScriptBlock.signaturesHashSet<string>,OrdinalIgnoreCase,恰好 120 个词)做并行比对,任一命中就把 force 置真——即使 SBL 从未开启,这一条也照样落 4104

MDSec 的演示很直白:在一台完全没有配置脚本块日志的主机上执行:

| | | — | | Write-Host "I wouldn't want to call DeviceIoControl here" |

事件日志里照样出现这条 4104——因为字符串里的 DeviceIoControl 命中了内置词表。这不是文档 bug,是设计意图:微软让”看起来在干危险事”的脚本即使在没有审计配置的环境里也留一道兜底记录。

对红队,这意味着”这环境没配审计所以可以裸奔”的假设在 PowerShell 5.0+ 上不成立,至少绕道动作本身会留痕;对蓝队,这意味着未纳入集中收集的机器可能躺着你没采的兜底日志——尤其是没接 SIEM 的服务器、临时主机。

还有一个细节值得记住:判定链末端的 ScriptBlockLoggingExplicitlyDisabled() 是唯一的”关门”出口——只有当策略值被显式置为 “0” 时它才返回真。也就是说词表兜底与显式开关共享同一个总闸,而总闸的控制权在进程内存里。这就是红队绕法的靶心。

03 · 120 个可疑词逐类拆解

词表本身值得通读一遍——它暴露了微软对”可疑 PowerShell”的建模。把全部 120 个词按用途归类:

| | | | | — | — | — | | 类别 | 代表词(均确认在表内) | 为什么敏感 | | 代码生成 | Add-Type、 DllImport、 DefineDynamicAssembly、 DefinePInvokeMethod、 ILGenerator、 Emit、 UnverifiableCodeAttribute | 现场编译新代码、声明原生函数签名——加载 Win32 API 的必经之路 | | 反射元编程 | GetType/GetMethod/GetField/GetProperty/GetEvent 全家族、 GetDefaultMembers、 InvokeMember、 BindingFlags、 NonPublic | 越权访问 internal/private 成员;改日志、改 AMSI 标志全靠这一族 | | 非托管互操作 | InteropServices、 Marshal、 AllocHGlobal、 PtrToStructure、 StructureToPtr、 IntPtr、 DangerousGetHandle | 直接摆弄内存指针 | | 原生 API / 进程操作 | kernel32、 OpenProcess、 VirtualAlloc/VirtualFree、 VirtualProtect、 WriteProcessMemory、ReadProcessMemory、 CreateThread、 CreateRemoteThread、 GetProcAddress、 LoadLibrary、 memcpy | 注入、内存 patch、shellcode 装载的标准零件 | | 凭据与令牌 | MiniDumpWriteDump、 AdjustTokenPrivileges、 OpenProcessToken、 GetTokenInformation、 SetThreadToken、ImpersonateLoggedOnUser、 RevertToSelf、 CreateProcessWithToken、 DuplicateTokenEx、 OpenWindowStation、 OpenDesktop | lsass 转储、令牌窃取、会话劫持的标配 | | 流与加解密 | MemoryStream、 DeflateStream、 FromBase64String、 ToBase64String、 CryptoStream、 CreateEncryptor、 PasswordDeriveBytes、 RijndaelManaged | 载荷编解码管线(压缩+Base64+对称加密)的构件 | | 输入采集 | GetAsyncKeyState、 GetKeyboardState、 GetForegroundWindow | 键盘记录 | | 自曝词 | EncodedCommand、 Bypass、 ExpandString、 ScriptBlockLogging、LogPipelineExecutionDetails、ProtectedEventLogging | 工具名/日志开关名直接躺在表里——你以为在绕日志,动作本身先命中 |

三个诚实的边界,写检测规则时务必记住:

•  词表不含下载执行类词:IEXInvoke-ExpressionDownloadStringWebClient 都不在表内。命中可疑词既不是恶意充分条件,也不是必要条件;

•  词表匹配的是静态文本切出的词元。运行时拼接出来的名字('DeviceIo'+'Control')在创建时刻的 AST 文本里不存在,因此绕词表不需要任何高级技巧——这也是为什么实战混淆普遍爱拼字符串;

•  反过来,正常运维脚本偶尔也会踩词(比如合法的 Add-Type 编译辅助代码),命中词表只能作为”值得看”的信号,不能直接当结论。

04 · 红队:两条内存级绕法

场景设定:SBL 开着(或者你担心兜底词表),想让它闭嘴。约束条件:不写注册表(改策略键是明显留痕且未必有权限)、不重启进程(重启后新进程恢复原状)。靶子就是 02 节那个唯一的关门出口——ScriptBlockLoggingExplicitlyDisabled()

它的实现是每次调用 Utils.GetGroupPolicySetting("ScriptBlockLogging", …) 去读注册表策略,而 GetGroupPolicySetting 读过的值会缓存在一个进程内静态字典 cachedGroupPolicySettings 里。读缓存,就意味着改内存缓存 = 改判定结果

绕法 A:毒化内存缓存(MDSec 原版三行):

| | | — | | $Settings = [Ref].Assembly.GetType('System.Management.Automation.Utils').GetField('cachedGroupPolicySettings','NonPublic,Static').GetValue($null) $Settings['HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'] = @{} $Settings['HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging'].Add('EnableScriptBlockLogging','0') |

机制:反射拿到静态字段 cachedGroupPolicySettings,把”脚本块日志”策略的缓存值覆盖成 EnableScriptBlockLogging = "0"。此后 ScriptBlockLoggingExplicitlyDisabled() 恒返回真,LogScriptBlockCreation 在唯一的关门出口直接 return——显式 SBL 与可疑词强制一并失效,MDSec 原文称之为”把现有绕过复用来确保可疑字符串也不被记录”。

绕法 B:清空词表(针对只有隐式兜底日志的环境):

| | | — | | [Ref].Assembly.GetType('System.Management.Automation.ScriptBlock').GetField('signatures','NonPublic,Static').SetValue($null,(New-Object 'System.Collections.Generic.HashSet[string]')) |

机制:force 的唯一来源是词表比对,把静态集合 signatures 整体换成一个空 HashSet,比对永不命中,可疑词强制从此失灵。MDSec 明确说明它适用于”只有隐式日志”的环境——如果显式 SBL 开着,ShouldLog 仍然为真,光清空词表没用,需要与 A 组合。

两条绕法放到一起看:

| | | | | — | — | — | | 维度 | 绕法 A:毒化缓存 | 绕法 B:清空词表 | | 靶点 | cachedGroupPolicySettings (进程内策略缓存) | ScriptBlock.signatures (静态词表) | | 针对 | 显式 SBL + 词表兜底,双杀 | 仅词表兜底(隐式日志) | | 写注册表? | 否(纯内存,进程重启即恢复) | 否(纯内存,进程重启即恢复) | | 自留痕 | 命令本身含 GetField/NonPublic 等词表词,执行瞬间 SBL 还开着 → 必先落一条 4104 | 同左 |

MDSec 特别提醒过那条自留痕:绕法执行的那一刻,日志还处于开启状态,而这行反射代码自己就命中词表——这条 4104 已经进了日志。实战里要么接受这条”明牌”,要么在已静默的进程里执行、或与其它手段组合,想做到完全无痕需要另想办法。这套手法随后被 @cobbr_io 以 PR #603 合入 Empire,成了框架内置能力。

另需说明:以上机制对应 PowerShell 5.x 时代的 System.Management.Automation 实现(2018 年拆解)。较新版本内部实现已有演进,字段名与判定结构可能不同——思路(毒化内存缓存、置空静态集合)比具体代码长寿。文中代码仅用于理解原理与构建检测特征,只允许在授权环境中验证

05 · 蓝队反制:把”绕道动作”变成最高优先级告警

绕法发生在进程内存里,进程外看不到;但绕法本身必须用 PowerShell 语法写出来,而语法是 4104 会记录的东西。所以蓝队的核心思路:别只搜”已知恶意命令”,把绕道动作的特征当成最高优先级信号。

狩猎 1:4104 里的反射改静态特征。以下字符串出现在 ScriptBlockText 里基本等于”有人在关日志/关 AMSI”:

•  cachedGroupPolicySettingsScriptBlock.signatures'signatures','NonPublic'

•  GetField(...'NonPublic,Static'...) + SetValue 组合

•  amsiInitFailedAmsiScanBuffer + VirtualProtect 三元组、'Am'+'siUtils' 式拼接

落地样例(5.1 通道):

| | | — | | Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | Where-Object { $_.Message -match 'cachedGroupPolicySettings|ScriptBlock\.signatures|amsiInitFailed|AmsiScanBuffer|NonPublic,Static' } | Select-Object TimeCreated, Id, Message | Format-List |

狩猎 2:断流监控。4104 的 Level 是 Verbose,很多转发配置默认不采这个级别——先确认你的采集链真的收得到 4104,否则一切免谈。采集就绪后,监控”同一 PowerShell 进程/会话前半段有 4104、某时间点之后突然断流”的模式:SBL 开启状态下脚本块创建必然产生事件,断流本身就是一个高价值异常(绕法生效后该进程的日志会立刻静默)。与 AMSI 侧交叉:前篇讲过 AMSI ETW 事件 1101(每条扫描请求一条),绕过后 1101 同样断流——两个独立数据源同时断流,基本可以实锤。

狩猎 3:时间窗关联。绕法命令必然先落一条带反射特征的 4104,紧接着的”静默期”里通常跟着注入/转储动作。把 4104 特征命中、Sysmon 进程注入事件、Defender 1116/1117 放进同一时间窗(按会话或进程 ID 关联),一条链就串起来了:先看到谁动手关灯,再看到灯灭之后他干了什么

加固侧,把日志变成”关了也没用”的纵深:

•  GPO 开启 SBL + 勾选调用记录(4104 家族完整化)+ 转录兜底会话内容;

•  Protected Event Logging(受保护事件日志):向全网下发公钥,PowerShell 用公钥加密 4104 等敏感事件内容,私钥只保存在离线集中收集端——攻击者即使拿到主机,读到的也是密文日志,就地篡改失去意义。微软明确建议”只要 SBL 用途不止于诊断,就配 Protected Event Logging”;

•  限制反射的用武之地:CLM(约束语言模式,配合 AppLocker/WDAC 触发)下 NonPublic,Static 反射与 New-Object HashSet 这类操作受限,把绕法从”三行代码”变成”先要突破语言模式”;

•  EDR 进程内存层检测:patch 类操作(VirtualProtect 改内存保护、写 stub)在内存层有行为特征,不能只靠日志。

06 · 边界与优先级:别神化 4104

收尾前把 4104 的边界说清楚,避免过度依赖单一数据源:

•  4104 只覆盖”新建脚本块”:被标记为产品代码的内部路径不记;攻击者脱离 PowerShell 引擎执行(注入其它进程、换脚本宿主)时它自然失效;

•  绕法是进程内存态,重启即复原——持久化仍需要别的机制,日志断流监控正好能发现这类”临时静默”;

•  与日志开关同级权限的攻击者理论上总能找到关闭检测的办法,所以 4104 的定位是高质量信号源而非终极防线,最终还是要靠不可变日志 + 内核/EDR 行为检测 + 最小权限的组合。

按性价比排优先级的话:① 先查采集链,Verbose 级别的 4104 有没有真的进 SIEM(多数环境挂在这一步);② 建 4104 绕道特征告警(本节狩猎 1 的关键词,误报可控);③ 加断流监控(狩猎 2);④ 中长期推 CLM/WDAC 与 Protected Event Logging。

一句话总结:4104 的双触发让”没开审计”成为幻觉,两条内存级绕法让”开了审计”也不能高枕无忧——但绕法自身必须用 PowerShell 说出来,而说出来的每一个字都会被 4104 记住。攻防的胜负手,就藏在”绕道动作本身留不留痕”这一层。

来源:MDSec《Exploring PowerShell AMSI and Logging Evasion》(Adam Chester,2018-06,含机制源码级拆解与 120 词完整清单);Microsoft Learn《about_Logging_Windows》(4104 事件字段、注册表路径、Protected Event Logging);微软组策略”打开 PowerShell 脚本块日志记录”文档。本系列前篇:《AMSI 攻防十年:一次脚本扫描接口引发的猫鼠游戏》(赛博57库 2026-08-22)。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛博57库 枪火·攻防技术 枪火·攻防技术《PowerShell 4104 攻防:双触发机制与内存级绕过》

评论:0   参与:  0