PowerShell 技能连载 - 日志分析与取证

适用于 PowerShell 5.1 及以上版本

日志是系统运维和安全取证的基石。Windows 事件日志、IIS 日志、应用程序日志中隐藏着故障根因和安全威胁的关键线索。当系统出现异常行为或发生安全事件时,快速定位和分析日志数据是响应的第一步。

传统的日志分析往往依赖图形界面的事件查看器或第三方 SIEM 工具,但它们在面对大规模日志数据或需要自定义分析逻辑时显得力不从心。PowerShell 提供了 Get-WinEventGet-EventLog(旧版)以及强大的对象管道,让我们能够以脚本化的方式高效收集、过滤、关联和可视化日志数据。

本文将从三个层面展开:首先是 Windows 安全事件日志的审计分析,其次是跨日志源的关联与异常检测,最后是自动化取证报告的生成。掌握这些技能后,你可以在安全事件响应、合规审计和故障排查中大幅提升效率。

Windows 事件日志审计

Windows 安全日志是取证分析的首要数据源。以下脚本演示了如何批量提取登录失败事件、追踪特权使用情况,并按频率统计可疑账户。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# 定义分析时间范围
$StartTime = (Get-Date).AddDays(-7)
$EndTime = Get-Date

# 提取安全日志中的登录失败事件(Event ID 4625)
$LoginFailures = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = $StartTime
EndTime = $EndTime
} -ErrorAction SilentlyContinue | ForEach-Object {
$xml = [xml]$_.ToXml()
$targetUser = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
$sourceIP = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'
$logonType = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'LogonType' }).'#text'

[PSCustomObject]@{
Time = $_.TimeCreated
User = $targetUser
SourceIP = $sourceIP
LogonType = $logonType
}
}

# 统计失败登录次数最多的账户
$LoginFailures | Group-Object User |
Sort-Object Count -Descending |
Select-Object Count, Name -First 10 |
Format-Table -AutoSize

# 提取特权使用事件(Event ID 4672)
$PrivilegeUse = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4672
StartTime = $StartTime
EndTime = $EndTime
} -ErrorAction SilentlyContinue | ForEach-Object {
$xml = [xml]$_.ToXml()
$subjectUser = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'SubjectUserName' }).'#text'
$privileges = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'PrivilegeList' }).'#text'

[PSCustomObject]@{
Time = $_.TimeCreated
Account = $subjectUser
Privileges = $privileges
}
}

# 输出特权使用摘要
$PrivilegeUse | Group-Object Account |
Select-Object Count, Name |
Sort-Object Count -Descending |
Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Count Name
----- ----
47 Administrator
12 backupsvc
8 testuser
3 jdoe
1 LOCAL SERVICE

Count Name
----- ----
156 Administrator
23 SYSTEM
8 backupsvc
2 NETWORK SERVICE

从结果可以看到 Administrator 账户在一周内有 47 次登录失败,值得进一步调查。同时特权使用频率也显示该账户活动异常频繁。

日志关联与异常检测

单一日志源往往无法呈现完整的安全图景。以下脚本展示如何将安全日志、系统日志和 PowerShell 脚本块日志进行关联,建立时间线并检测统计异常。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 收集多日志源数据并建立统一时间线
$TimeRange = @{
StartTime = (Get-Date).AddDays(-3)
EndTime = Get-Date
}

# 安全日志:账户管理变更(Event ID 4720 创建、4726 删除、4728 加入管理员组)
$AccountChanges = Get-WinEvent -FilterHashtable @{
LogName = 'Security'; Id = 4720, 4726, 4728; @TimeRange
} -ErrorAction SilentlyContinue | ForEach-Object {
$xml = [xml]$_.ToXml()
$targetAccount = ($xml.Event.EventData.Data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
[PSCustomObject]@{
Time = $_.TimeCreated
Source = 'Security'
Event = "AccountChange($($_.Id))"
Detail = $targetAccount
}
}

# 系统日志:服务安装和驱动加载
$SystemEvents = Get-WinEvent -FilterHashtable @{
LogName = 'System'; Id = 7045, 7036; @TimeRange
} -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
Source = 'System'
Event = if ($_.Id -eq 7045) { 'NewService' } else { 'ServiceStateChange' }
Detail = $_.Message.Substring(0, [Math]::Min(80, $_.Message.Length))
}
}

# PowerShell 脚本块日志(Event ID 4104)
$PSLogs = Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-PowerShell/Operational'; Id = 4104; @TimeRange
} -ErrorAction SilentlyContinue | ForEach-Object {
[PSCustomObject]@{
Time = $_.TimeCreated
Source = 'PowerShell'
Event = 'ScriptBlock'
Detail = $_.Message.Substring(0, [Math]::Min(80, $_.Message.Length))
}
}

# 合并时间线并按时间排序
$Timeline = $AccountChanges + $SystemEvents + $PSLogs |
Sort-Object Time |
Select-Object Time, Source, Event, Detail

# 显示最近 20 条时间线事件
$Timeline | Select-Object -Last 20 | Format-Table -Wrap

# 统计异常检测:每小时事件数量,标记超过 2 个标准差的时间段
$HourlyStats = $Timeline | Group-Object { $_.Time.ToString('yyyy-MM-dd HH:00') } |
ForEach-Object { [PSCustomObject]@{ Hour = $_.Name; Count = $_.Count } }

$Mean = ($HourlyStats | Measure-Object Count -Average).Average
$StdDev = [Math]::Sqrt(
($HourlyStats | ForEach-Object { [Math]::Pow($_.Count - $Mean, 2) } | Measure-Object -Average).Average
)

$HourlyStats | ForEach-Object {
$isAnomaly = $_.Count -gt ($Mean + 2 * $StdDev)
[PSCustomObject]@{
Hour = $_.Hour
Count = $_.Count
Anomaly = if ($isAnomaly) { '*** ALERT ***' } else { '' }
}
} | Format-Table -AutoSize

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Time                  Source    Event              Detail
---- ------ ----- ------
2026-01-25 14:22:01 Security AccountChange(4720) svc-backup
2026-01-25 14:22:05 System NewService A service was installed in the system....
2026-01-25 14:23:10 PowerShell ScriptBlock Invoke-WebRequest -Uri http://10.0.0.5/...
2026-01-25 15:01:33 Security AccountChange(4728) svc-backup
2026-01-25 16:45:12 System ServiceStateChange The Background Intelligent Transfer Ser...

Hour Count Anomaly
---- ----- -------
2026-01-23 08 34
2026-01-23 09 41
2026-01-23 10 28
2026-01-24 02 112 *** ALERT ***
2026-01-24 03 98 *** ALERT ***
2026-01-24 09 37
2026-01-25 14 67 *** ALERT ***

凌晨 2 点和 3 点的事件量远高于平均水平,这在正常业务场景中极其可疑,需要重点排查该时间段的所有活动。

取证报告生成

分析完成后,生成结构化的取证报告是交付成果的关键步骤。以下脚本将分析结果整理为包含时间线、IOC 提取和可视化汇总的 HTML 报告。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 定义报告参数
$ReportPath = "$env:TEMP\ForensicReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').html"
$AnalysisPeriod = "$((Get-Date).AddDays(-7).ToString('yyyy-MM-dd')) 至 $((Get-Date).ToString('yyyy-MM-dd'))"

# 收集关键 IOC(Indicators of Compromise)
$SuspiciousIPs = $LoginFailures |
Where-Object { $_.SourceIP -and $_.SourceIP -ne '-' -and $_.SourceIP -ne '::1' } |
Group-Object SourceIP |
Where-Object { $_.Count -gt 5 } |
Select-Object @{N = 'Indicator'; E = { $_.Name } }, @{N = 'Occurrences'; E = { $_.Count } }

$SuspiciousAccounts = $LoginFailures |
Group-Object User |
Where-Object { $_.Count -gt 10 } |
Select-Object @{N = 'Indicator'; E = { "Account: $($_.Name)" } }, @{N = 'Occurrences'; E = { $_.Count } }

$AllIOCs = $SuspiciousIPs + $SuspiciousAccounts

# 生成 HTML 报告
$HtmlHeader = @"
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>取证分析报告 - $AnalysisPeriod</title>
<style>
body { font-family: 'Segoe UI', sans-serif; margin: 20px; background: #f5f5f5; }
h1 { color: #d32f2f; border-bottom: 2px solid #d32f2f; padding-bottom: 8px; }
h2 { color: #1565c0; margin-top: 24px; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; background: #fff; }
th { background: #1565c0; color: #fff; padding: 8px 12px; text-align: left; }
td { padding: 8px 12px; border-bottom: 1px solid #ddd; }
tr:nth-child(even) { background: #f9f9f9; }
.alert { color: #d32f2f; font-weight: bold; }
.summary { background: #fff; padding: 16px; border-radius: 4px; margin: 12px 0; }
</style>
</head>
<body>
<h1>取证分析报告</h1>
<div class="summary">
<strong>分析周期:</strong>$AnalysisPeriod<br>
<strong>生成时间:</strong>$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')<br>
<strong>登录失败事件:</strong>$($LoginFailures.Count) 条<br>
<strong>异常时段:</strong>$($HourlyStats | Where-Object { $_.Count -gt ($Mean + 2 * $StdDev) }).Count) 个
</div>
"@

# 构建 IOC 表格
$IocTable = $AllIOCs | ForEach-Object {
"<tr><td>$($_.Indicator)</td><td>$($_.Occurrences)</td></tr>"
} | Out-String

$HtmlBody = @"
<h2>威胁指标 (IOCs)</h2>
<table><tr><th>指标</th><th>出现次数</th></tr>$IocTable</table>

<h2>时间线摘要(最近事件)</h2>
<table><tr><th>时间</th><th>来源</th><th>事件</th></tr>
$(
$Timeline | Select-Object -Last 15 | ForEach-Object {
"<tr><td>$($_.Time.ToString('yyyy-MM-dd HH:mm:ss'))</td><td>$($_.Source)</td><td>$($_.Event)</td></tr>"
} | Out-String
)
</table>

<h2>统计概览</h2>
<p>每小时平均事件数:<strong>$([Math]::Round($Mean, 1))</strong></p>
<p>标准差:<strong>$([Math]::Round($StdDev, 1))</strong></p>
<p>异常阈值(Mean + 2*Sigma):<strong>$([Math]::Round($Mean + 2 * $StdDev, 1))</strong></p>
</body></html>
"@

$HtmlHeader + $HtmlBody | Out-File -FilePath $ReportPath -Encoding UTF8
Write-Host "取证报告已生成:$ReportPath"

执行结果示例:

1
取证报告已生成:C:\Users\admin\AppData\Local\Temp\ForensicReport_20260126_103022.html

生成的 HTML 报告包含完整的威胁指标表格、时间线摘要和统计概览,可以直接在浏览器中打开查看,也可作为合规审计的附件存档。

注意事项

  1. 管理员权限要求:访问安全日志(Security Log)需要以管理员身份运行 PowerShell,普通用户只能查看应用程序和系统日志。建议使用 Start-Process powershell -Verb RunAs 提权后执行分析脚本。

  2. 日志大小与性能Get-WinEvent 在处理大量日志时可能消耗较多内存。对于跨月或跨年的分析,建议分批查询或使用 -MaxEvents 参数限制返回数量,避免内存溢出。

  3. 时间同步的重要性:跨服务器的日志关联分析要求所有机器的时钟保持同步(NTP)。如果时间偏差超过数秒,关联结果可能不准确,建议先验证各机器的时区和时间同步状态。

  4. 日志保留策略:Windows 默认的安全日志最大大小为 20MB,可能在繁忙环境中只保留数天数据。建议通过组策略将日志最大大小调整为 1GB 以上,并启用日志转发(Event Forwarding)集中存储。

  5. PowerShell 脚本块日志的隐私考量:脚本块日志(Event ID 4104)会记录执行的代码内容,可能包含密码等敏感信息。在取证环境中这是优势,但在日常运维中需注意合规要求,评估是否需要启用受保护事件日志(Protected Event Logging)。

  6. 报告存储与链式保管:取证报告应保存在不可篡改的存储介质上,记录哈希值(如 Get-FileHash 计算的 SHA256)以确保完整性。同时保留原始日志的导出副本(.evtx 文件),以满足法律证据链的要求。

PowerShell 技能连载 - 安全事件响应自动化

适用于 PowerShell 5.1 及以上版本(Windows)

安全事件响应(Incident Response)是企业安全运维中最关键的环节之一。当安全警报触发时,响应速度直接影响损失范围——根据 IBM 的《数据泄露成本报告》,平均响应时间每缩短一天,泄露成本可降低约 100 万美元。传统的手动排查方式耗时耗力,而且容易遗漏关键线索,特别是在攻击者已经横向移动的情况下。

PowerShell 作为 Windows 环境的原生脚本语言,天生具备深度系统访问能力,可以从事件日志、注册表、网络连接、进程树等多个维度快速采集取证数据。结合 2025 年企业安全场景中普遍面临的勒索软件、供应链攻击、凭证窃取等威胁,用 PowerShell 构建自动化事件响应流程不仅能提升效率,还能确保采集过程的标准化和可重复性。

今天是万圣节,让我们用 PowerShell 来对付那些比鬼魂更可怕的——系统里的安全威胁。本文将从实战角度出发,带你构建一套涵盖威胁检测、取证采集和自动处置的安全事件响应工具集。

场景一:实时威胁检测与告警

当安全事件发生时,第一步是快速确认威胁是否存在以及影响范围。下面的脚本会检查系统中常见的入侵指标(IOC),包括可疑进程、异常网络连接和近期创建的计划任务。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
function Test-SecurityThreatIndicator {
$report = [System.Text.StringBuilder]::new()
$null = $report.AppendLine("=== 安全威胁检测报告 ===")
$null = $report.AppendLine("扫描时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$null = $report.AppendLine()

# 检查可疑进程(已知攻击工具特征)
$suspiciousNames = @(
'mimikatz', 'procdump', 'lazagne', 'nc.exe', 'ncat',
'psexec', 'bloodhound', 'sharphound', 'rubeus', 'kekeo'
)

$null = $report.AppendLine("[可疑进程检查]")
$foundSuspicious = $false
$processes = Get-Process -ErrorAction SilentlyContinue
foreach ($proc in $processes) {
foreach ($name in $suspiciousNames) {
if ($proc.ProcessName -like "*$name*") {
$null = $report.AppendLine(
" [!] 发现可疑进程: $($proc.ProcessName) " +
"(PID: $($proc.Id), 路径: $($proc.Path))"
)
$foundSuspicious = $true
}
}
}
if (-not $foundSuspicious) {
$null = $report.AppendLine(" [OK] 未发现已知可疑进程")
}

# 检查异常出站网络连接
$null = $report.AppendLine()
$null = $report.AppendLine("[异常网络连接检查]")
$connections = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
Where-Object { $_.RemoteAddress -notmatch '^(127\.|0\.|::1|::$)' }

$suspiciousPorts = @(4444, 5555, 6666, 7777, 8888, 9999, 1234, 31337)
foreach ($conn in $connections) {
if ($conn.RemotePort -in $suspiciousPorts) {
$procInfo = try {
(Get-Process -Id $conn.OwningProcess -ErrorAction Stop).ProcessName
} catch {
"未知"
}
$null = $report.AppendLine(
" [!] 可疑连接: $($conn.LocalAddress):$($conn.LocalPort) -> " +
"$($conn.RemoteAddress):$($conn.RemotePort) (进程: $procInfo)"
)
}
}

# 检查近期创建的计划任务(24小时内)
$null = $report.AppendLine()
$null = $report.AppendLine("[近期计划任务检查 - 24小时内创建]")
$cutoff = (Get-Date).AddHours(-24)
$tasks = Get-ScheduledTask -ErrorAction SilentlyContinue |
Where-Object { $_.Date -and $_.Date -gt $cutoff }
foreach ($task in $tasks) {
$null = $report.AppendLine(
" [!] 新任务: $($task.TaskName) " +
"(状态: $($task.State), 创建时间: $($task.Date))"
)
}
if (-not $tasks) {
$null = $report.AppendLine(" [OK] 24小时内无新增计划任务")
}

$null = $report.AppendLine()
$null = $report.AppendLine("=== 扫描完成 ===")
return $report.ToString()
}

Test-SecurityThreatIndicator

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
=== 安全威胁检测报告 ===
扫描时间: 2025-10-31 14:23:05

[可疑进程检查]
[OK] 未发现已知可疑进程

[异常网络连接检查]
[!] 可疑连接: 192.168.1.105:49832 -> 10.0.0.88:4444 (进程: svchost)

[近期计划任务检查 - 24小时内创建]
[!] 新任务: WindowsUpdateSync (状态: Ready, 创建时间: 2025/10/31 08:15:00)
[!] 新任务: SystemHealthMonitor (状态: Ready, 创建时间: 2025/10/31 10:30:22)

=== 扫描完成 ===

在这个输出中可以看到一个连接到 4444 端口的可疑网络连接(4444 是 Metasploit 默认反向 shell 端口),以及两个近期新创建的计划任务——这些都是需要进一步调查的入侵指标。

场景二:自动取证数据采集

确认威胁后,下一步是采集取证数据用于分析。下面的脚本会自动收集 Windows 安全事件日志、用户登录记录和文件系统变更信息,并将结果打包保存。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
function Start-ForensicDataCollection {
param(
[string]$OutputPath = "$env:TEMP\ForensicCollection_$(Get-Date -Format 'yyyyMMdd_HHmmss')"
)

$null = New-Item -Path $OutputPath -ItemType Directory -Force

$collectionLog = [System.Text.StringBuilder]::new()
$null = $collectionLog.AppendLine("取证采集开始: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$null = $collectionLog.AppendLine("输出目录: $OutputPath")
$null = $collectionLog.AppendLine()

# 采集一:导出安全事件日志(最近 7 天)
$null = $collectionLog.AppendLine("[1/4] 采集安全事件日志...")
$securityLogPath = Join-Path $OutputPath "SecurityEvents.csv"
$startTime = (Get-Date).AddDays(-7)
$events = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
StartTime = $startTime
} -MaxEvents 5000 -ErrorAction SilentlyContinue

$events | Select-Object TimeCreated, Id,
@{N='Level';E={$_.LevelDisplayName}},
@{N='User';E={$_.UserId}},
Message |
Export-Csv -Path $securityLogPath -NoTypeInformation -Encoding UTF8

$null = $collectionLog.AppendLine(
" 已导出 $($events.Count) 条安全事件到 SecurityEvents.csv"
)

# 采集二:收集成功登录记录(Event ID 4624)
$null = $collectionLog.AppendLine("[2/4] 采集登录记录...")
$logonPath = Join-Path $OutputPath "LogonRecords.csv"
$logonEvents = $events |
Where-Object { $_.Id -eq 4624 }

$logonRecords = foreach ($evt in $logonEvents) {
$xml = [xml]$evt.ToXml()
$data = @{}
foreach ($item in $xml.Event.EventData.Data) {
$data[$item.Name] = $item.'#text'
}
[PSCustomObject]@{
TimeCreated = $evt.TimeCreated
TargetUser = $data['TargetUserName']
LogonType = $data['LogonType']
SourceAddress = $data['IpAddress']
SourcePort = $data['IpPort']
AuthPackage = $data['AuthenticationPackageName']
}
}
$logonRecords | Export-Csv -Path $logonPath -NoTypeInformation -Encoding UTF8
$null = $collectionLog.AppendLine(
" 已导出 $($logonRecords.Count) 条登录记录到 LogonRecords.csv"
)

# 采集三:列出近期修改的高风险文件位置
$null = $collectionLog.AppendLine("[3/4] 采集文件系统变更...")
$fileChangesPath = Join-Path $OutputPath "FileChanges.csv"
$criticalPaths = @(
"$env:WINDIR\System32\drivers\etc",
"$env:WINDIR\System32\WindowsPowerShell",
"$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup",
"$env:WINDIR\Temp",
"$env:TEMP"
)

$fileChanges = foreach ($path in $criticalPaths) {
if (Test-Path $path) {
Get-ChildItem -Path $path -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -gt $startTime } |
Select-Object FullName, Length, LastWriteTime, CreationTime
}
}
$fileChanges | Export-Csv -Path $fileChangesPath -NoTypeInformation -Encoding UTF8
$null = $collectionLog.AppendLine(
" 已导出 $($fileChanges.Count) 个近期变更文件到 FileChanges.csv"
)

# 采集四:导出当前运行服务状态
$null = $collectionLog.AppendLine("[4/4] 采集服务状态...")
$servicePath = Join-Path $OutputPath "Services.csv"
$services = Get-CimInstance -ClassName Win32_Service |
Select-Object Name, DisplayName, State, StartMode,
@{N='Path';E={$_.PathName}},
@{N='Account';E={$_.StartName}}
$services | Export-Csv -Path $servicePath -NoTypeInformation -Encoding UTF8
$null = $collectionLog.AppendLine(
" 已导出 $($services.Count) 个服务信息到 Services.csv"
)

# 生成采集报告摘要
$null = $collectionLog.AppendLine()
$null = $collectionLog.AppendLine("取证采集完成: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$summary = $collectionLog.ToString()
$summary | Out-File -FilePath (Join-Path $OutputPath "CollectionLog.txt") -Encoding UTF8

Write-Output $summary
Write-Output "`n取证数据已保存到: $OutputPath"
}

Start-ForensicDataCollection

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
取证采集开始: 2025-10-31 14:35:12
输出目录: C:\Users\admin\AppData\Local\Temp\ForensicCollection_20251031_143512

[1/4] 采集安全事件日志...
已导出 3247 条安全事件到 SecurityEvents.csv
[2/4] 采集登录记录...
已导出 186 条登录记录到 LogonRecords.csv
[3/4] 采集文件系统变更...
已导出 47 个近期变更文件到 FileChanges.csv
[4/4] 采集服务状态...
已导出 215 个服务信息到 Services.csv

取证采集完成: 2025-10-31 14:35:58

取证数据已保存到: C:\Users\admin\AppData\Local\Temp\ForensicCollection_20251031_143512

脚本会在临时目录下创建一个带时间戳的文件夹,里面包含四个 CSV 文件和一个采集日志。这些数据可以直接交给安全分析团队,也可以导入 SIEM 系统做进一步关联分析。整个采集过程在管理员权限下运行,大约 1 分钟即可完成。

场景三:自动隔离与威胁阻断

当确认系统被入侵后,最紧急的操作是阻断攻击者的通信通道并防止横向移动。下面的脚本提供了网络隔离、账户锁定和持久化后门清除三大处置能力。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
function Invoke-ThreatContainment {
param(
[switch]$BlockSuspiciousIPs,
[switch]$DisableCompromisedAccounts,
[switch]$RemovePersistence,
[string[]]$MaliciousIPs = @()
)

$actionLog = [System.Text.StringBuilder]::new()
$null = $actionLog.AppendLine("=== 威胁处置记录 ===")
$null = $actionLog.AppendLine("执行时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$null = $actionLog.AppendLine()

# 处置一:封禁可疑 IP 地址
if ($BlockSuspiciousIPs -and $MaliciousIPs.Count -gt 0) {
$null = $actionLog.AppendLine("[网络封禁] 封禁恶意 IP 地址")
foreach ($ip in $MaliciousIPs) {
$ruleName = "IR-Block-$($ip -replace '\.','_')"
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($existing) {
$null = $actionLog.AppendLine(" [SKIP] 规则已存在: $ruleName")
continue
}
New-NetFirewallRule -DisplayName $ruleName `
-Direction Outbound -Action Block `
-RemoteAddress $ip -Protocol Any `
-Description "事件响应自动封禁 - $(Get-Date -Format 'yyyy-MM-dd')" |
Out-Null
$null = $actionLog.AppendLine(" [OK] 已封禁出站连接: $ip")
}
$null = $actionLog.AppendLine()
}

# 处置二:禁用被盗用的本地账户
if ($DisableCompromisedAccounts) {
$null = $actionLog.AppendLine("[账户处置] 检查异常登录账户")
$recentLogons = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = (Get-Date).AddHours(-1)
} -ErrorAction SilentlyContinue

$failedAccounts = [System.Collections.Generic.HashSet[string]]::new()
foreach ($evt in $recentLogons) {
$xml = [xml]$evt.ToXml()
foreach ($item in $xml.Event.EventData.Data) {
if ($item.Name -eq 'TargetUserName' -and $item.'#text') {
$null = $failedAccounts.Add($item.'#text')
}
}
}

# 失败次数超过阈值的账户视为可疑
foreach ($account in $failedAccounts) {
$localUser = Get-LocalUser -Name $account -ErrorAction SilentlyContinue
if ($localUser -and $localUser.Enabled) {
Disable-LocalUser -Name $account
$null = $actionLog.AppendLine(
" [!] 已禁用可疑账户: $account (近期多次登录失败)"
)
}
}
$null = $actionLog.AppendLine()
}

# 处置三:清除持久化后门
if ($RemovePersistence) {
$null = $actionLog.AppendLine("[持久化清除] 检查常见持久化位置")

# 检查启动项注册表
$runKeys = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run',
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce',
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
)
$knownGoodEntries = @('SecurityHealth', 'SysTrayApp', 'OneDrive')
foreach ($key in $runKeys) {
if (Test-Path $key) {
$entries = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
$props = $entries.PSObject.Properties |
Where-Object {
$_.Name -notin @('PSPath','PSParentPath','PSChildName','PSDrive','PSProvider') -and
$_.Name -notin $knownGoodEntries
}
foreach ($prop in $props) {
$null = $actionLog.AppendLine(
" [!] 可疑启动项: $($prop.Name) = $($prop.Value)"
)
}
}
}
$null = $actionLog.AppendLine()
}

$null = $actionLog.AppendLine("=== 处置完成 ===")
$result = $actionLog.ToString()
Write-Output $result

# 将处置记录保存到安全日志目录
$logDir = "C:\IR_Logs"
if (-not (Test-Path $logDir)) {
$null = New-Item -Path $logDir -ItemType Directory -Force
}
$result | Out-File -FilePath "$logDir\ActionLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt" -Encoding UTF8
}

# 示例调用:封禁可疑 IP 并清除持久化后门
Invoke-ThreatContainment `
-BlockSuspiciousIPs `
-MaliciousIPs @('10.0.0.88', '192.168.100.200', '203.0.113.50') `
-RemovePersistence

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
=== 威胁处置记录 ===
执行时间: 2025-10-31 14:42:18

[网络封禁] 封禁恶意 IP 地址
[OK] 已封禁出站连接: 10.0.0.88
[OK] 已封禁出站连接: 192.168.100.200
[OK] 已封禁出站连接: 203.0.113.50

[持久化清除] 检查常见持久化位置
[!] 可疑启动项: WindowsUpdate = C:\Users\Public\update.exe
[!] 可疑启动项: SvchostHelper = C:\ProgramData\svchost.bat

=== 处置完成 ===

脚本通过 Windows 防火墙规则封禁了三个恶意 IP 的出站连接,同时在注册表启动项中发现了两个可疑条目——一个伪装成 Windows Update 的可执行文件和一个 svchost 辅助脚本。这些信息需要进一步分析确认,但第一时间阻断网络通道可以有效阻止数据外泄和远程控制。

注意事项

  1. 权限要求:安全事件日志的读取和防火墙规则操作需要管理员权限。建议以提升权限的 PowerShell 会话运行这些脚本,或者通过 WinRM 远程执行时确保会话凭据具有本地管理员权限。

  2. 事件日志大小:如果安全事件日志配置为按需覆盖或日志量极大,Get-WinEvent 可能返回大量数据。建议使用 -MaxEvents 参数限制返回数量,并在过滤条件中加入时间范围,避免内存溢出。

  3. 取证完整性:采集取证数据时务必注意操作顺序——先采集后处置。一旦封禁 IP 或禁用账户,攻击者可能触发自毁机制删除痕迹。优先采集内存镜像和日志,再执行阻断操作。

  4. 误封风险:自动封禁 IP 地址存在误伤业务系统的风险。建议将封禁逻辑与资产白名单结合,先排除内部合法服务器 IP,再执行封禁。封禁后持续监控业务连接是否受影响。

  5. 日志保护:事件响应操作本身也会产生日志记录,这是好事——确保操作可审计。但攻击者也可能尝试清除日志。建议在处置流程中增加一步:将关键日志导出到只读共享或集中式日志平台。

  6. 脚本安全存储:这些响应脚本包含敏感的安全检测逻辑和处置动作,应存储在受控的代码仓库中,避免被攻击者预先研究。在生产环境中使用时,建议通过 DSC 或 CI/CD 管道分发,而不是直接在目标系统上留存脚本文件。

万圣节的鬼怪不过是一晚的恶作剧,但安全威胁可能在你毫无察觉时潜伏数周。掌握 PowerShell 自动化事件响应,让安全团队拥有比驱魔人更可靠的武器。祝大家在这个万圣节既没有鬼敲门,也没有警报响。

PowerShell 技能连载 - 事件日志深度分析

适用于 PowerShell 5.1 及以上版本(Windows)

Windows 事件日志是系统运维和安全审计的核心数据源。无论是排查服务崩溃、追踪用户登录行为,还是进行安全取证分析,事件日志都提供了不可替代的线索。然而,面对动辄数十万条日志记录,手动翻阅事件查看器显然效率低下。

PowerShell 内置的 Get-WinEvent cmdlet 拥有强大的过滤和查询能力,配合结构化对象输出,可以大幅提升日志分析效率。与旧版的 Get-EventLog(已在 PowerShell 7 中移除)不同,Get-WinEvent 支持所有日志通道(包括 ETL 诊断日志),并能通过 XPath 和哈希表实现高效的服务端过滤。

本文将从安全审计和故障排查两个场景出发,演示如何用 PowerShell 构建一套实用的事件日志深度分析脚本。重点在于掌握过滤技巧和结果聚合方法,让海量日志真正为你所用。

按时间窗口和级别提取安全日志

在安全审计场景中,最常见的操作是从安全日志中提取特定时间段内的事件。以下脚本演示如何查询最近 24 小时内的登录成功事件(Event ID 4624),并提取关键信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$startTime = (Get-Date).AddHours(-24)
$logonEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4624
StartTime = $startTime
} -ErrorAction SilentlyContinue

$results = @()
foreach ($event in $logonEvents) {
$xml = [xml]$event.ToXml()
$data = $xml.Event.EventData.Data
$targetUserName = ($data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
$logonType = ($data | Where-Object { $_.Name -eq 'LogonType' }).'#text'
$ipAddress = ($data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'

$results += [PSCustomObject]@{
Time = $event.TimeCreated
User = $targetUserName
LogonType = $logonType
SourceIP = $ipAddress
}
}

$results | Sort-Object Time -Descending | Format-Table -AutoSize

上述代码使用 FilterHashtable 进行服务端过滤,只将符合条件的记录传输到客户端,避免大量无用数据占用内存。通过解析事件的 XML 结构,我们可以提取出事件数据中任意命名的字段。注意这里使用 foreach 循环而非管道,在处理大量记录时性能更稳定。

执行结果示例:

1
2
3
4
5
6
7
Time                  User           LogonType SourceIP
---- ---- --------- --------
2025/10/27 07:52:14 administrator 10 192.168.1.105
2025/10/27 07:48:33 jzhang 2 192.168.1.42
2025/10/27 07:30:19 SYSTEM 5 -
2025/10/27 06:15:42 lwei 10 10.0.0.88
2025/10/27 05:02:07 administrator 7 127.0.0.1

其中 LogonType 的含义尤为关键:类型 2 表示交互式登录(控制台),类型 10 表示远程桌面登录,类型 5 表示服务启动。如果发现异常的远程登录记录,应立即排查来源 IP 是否合法。

聚合高频错误事件并生成统计报告

在故障排查场景中,快速定位哪些错误出现频率最高,往往比逐条分析更有效率。以下脚本从 System 和 Application 两个日志中提取最近 7 天的 Error 和 Critical 级别事件,按事件 ID 分组统计。

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
30
31
32
33
34
35
36
37
38
39
$startTime = (Get-Date).AddDays(-7)
$logNames = @('System', 'Application')

$allErrors = @()
foreach ($logName in $logNames) {
$events = Get-WinEvent -FilterHashtable @{
LogName = $logName
Level = 1, 2
StartTime = $startTime
} -ErrorAction SilentlyContinue

foreach ($event in $events) {
$allErrors += [PSCustomObject]@{
LogName = $event.LogName
EventId = $event.Id
Level = $event.LevelDisplayName
Message = $event.Message.Substring(0, [Math]::Min(120, $event.Message.Length))
Time = $event.TimeCreated
Provider = $event.ProviderName
}
}
}

$summary = $allErrors | Group-Object LogName, EventId | Sort-Object Count -Descending

foreach ($group in $summary) {
$parts = $group.Name -split ', '
$log = $parts[0]
$eid = $parts[1]
$sampleMessage = $group.Group[0].Message

[PSCustomObject]@{
Count = $group.Count
LogName = $log
EventId = $eid
Provider = $group.Group[0].Provider
Sample = $sampleMessage
}
} | Format-Table -AutoSize -Wrap

这段脚本的核心思路是先用 Level = 1, 2(Critical 和 Error)做服务端过滤,减少数据传输量;然后在客户端用 Group-Object 按日志名和事件 ID 聚合,统计出现次数。最后输出每个高频错误的首次出现样例,方便运维人员快速判断问题根因。

执行结果示例:

1
2
3
4
5
6
7
Count LogName    EventId Provider              Sample
----- ------- ------- -------- ------
147 System 7036 Service Control Manager 服务已在后台启动或停止...
82 Application 1000 Application Error 故障模块名称: ntdll.dll...
45 System 7023 Service Control Manager 服务以特定错误退出...
31 Application 1026 .NET Runtime 进程已因未经处理的异常而终止...
12 System 41 Kernel-Power 系统未先正常关闭...

检测可疑的登录失败和账户锁定

安全取证中,登录失败事件(Event ID 4625)和账户锁定事件(Event ID 4740)是发现暴力破解攻击的关键指标。以下脚本汇总最近 7 天的登录失败记录,按目标账户和来源 IP 聚合,识别潜在的暴力破解目标。

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
30
31
32
33
34
35
36
37
$startTime = (Get-Date).AddDays(-7)

$failedLogonEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
Id = 4625
StartTime = $startTime
} -ErrorAction SilentlyContinue

$failedLogins = @()
foreach ($event in $failedLogonEvents) {
$xml = [xml]$event.ToXml()
$data = $xml.Event.EventData.Data
$targetUser = ($data | Where-Object { $_.Name -eq 'TargetUserName' }).'#text'
$sourceIP = ($data | Where-Object { $_.Name -eq 'IpAddress' }).'#text'
$failureReason = ($data | Where-Object { $_.Name -eq 'SubStatus' }).'#text'

$failedLogins += [PSCustomObject]@{
Time = $event.TimeCreated
User = $targetUser
SourceIP = $sourceIP
FailureCode = $failureReason
}
}

$suspicious = $failedLogins | Group-Object User, SourceIP | Where-Object { $_.Count -gt 5 } |
Sort-Object Count -Descending

foreach ($entry in $suspicious) {
$parts = $entry.Name -split ', '
[PSCustomObject]@{
FailedCount = $entry.Count
User = $parts[0]
SourceIP = $parts[1]
FirstSeen = $entry.Group[0].Time
LastSeen = $entry.Group[-1].Time
}
} | Format-Table -AutoSize

脚本中通过 Where-Object { $_.Count -gt 5 } 筛选出失败次数超过 5 次的组合,这些通常是暴力破解的特征。FailureCode 字段(SubStatus)可以进一步区分失败原因,例如 0xC000006A 表示密码错误,0xC0000234 表示账户已被锁定。

执行结果示例:

1
2
3
4
5
6
FailedCount User           SourceIP       FirstSeen             LastSeen
----------- ---- -------- --------- --------
89 administrator 203.0.113.42 2025/10/21 02:14:07 2025/10/27 06:58:33
34 backup_svc 198.51.100.7 2025/10/23 11:20:15 2025/10/26 23:47:01
12 testuser 192.168.1.105 2025/10/25 08:30:00 2025/10/27 07:12:44
8 sqladmin 10.0.0.99 2025/10/26 14:05:22 2025/10/27 03:40:18

发现此类模式后,建议立即检查对应账户的锁定策略和网络防火墙规则,并考虑将可疑 IP 加入黑名单。

注意事项

  1. 优先使用 FilterHashtable 而非管道过滤Get-WinEvent 支持 -FilterHashtable 参数做服务端过滤,比先获取全部事件再用 Where-Object 过滤效率高数十倍。在查询安全日志等大型日志时,差异尤为明显。

  2. 处理空结果集:当日志中没有符合条件的记录时,Get-WinEvent 会抛出异常而非返回空集合。务必使用 -ErrorAction SilentlyContinuetry/catch 块来优雅处理这种情况。

  3. Level 参数使用数值FilterHashtable 中的 Level 字段只接受数值:1 = Critical,2 = Error,3 = Warning,4 = Information,5 = Verbose。不要使用字符串,否则过滤不会生效。

  4. XPath 过滤更精细:当 FilterHashtable 无法满足复杂条件时,可以使用 -FilterXPath 参数编写 XPath 表达式。例如按消息内容中的特定字段值过滤,这是最灵活的服务端过滤方式。

  5. 安全日志需要管理员权限:查询 Security 日志需要以管理员身份运行 PowerShell(”以管理员身份运行”),否则会收到权限不足的错误。普通用户只能访问 Application 和 System 等日志。

  6. 大时间范围查询注意性能:如果查询跨度数月的安全日志,即使使用服务端过滤也可能返回海量数据。建议先按天或按小时分段查询,再汇总结果。也可以结合 -MaxEvents 参数限制返回条数做初步探查。