☰
Windows安全加固进阶:日志审计、端口收敛与完整性巡检实战
2026/10/2 9:41:37 网站建设 项目流程

把 Windows 系统安全当成一个持续深挖的方向,第二篇就该从一个很现实的问题切入:加固之后,你知道系统里正在发生什么吗?上一篇我们聊了账户密码策略、共享权限、服务收敛这些基础工作,算是把门锁换了。但门锁换得再结实,如果人进来了你完全没察觉,等于白干。所以这一篇的主题很明确——怎么让 Windows 把它的所见所闻都留给你看,以及你拿到这些痕迹之后,如何快速判断哪些是正常噪音、哪些是真问题。

这篇内容更适合已经做过一轮基础加固、想往纵深走的朋友,也适合每天要运维一堆 Windows 机器的管理员。不绕弯子,从四个方面展开:安全日志怎么读、端口怎么收、文件注册表怎么验、补丁和巡检怎么做。全程是实际敲过的命令和踩过的坑,你可以直接照着复现。

1. 安全日志里藏着多少信息:先学会读再谈查

先说结论:Windows 的本地安全日志是排查“谁碰过这台机器”的第一现场。攻击者哪怕手脚再干净,只要在这台机器上做过程序级别的操作,就一定会留下安全日志事件。真正高明的入侵者会想办法清日志,但清日志本身就是一件事,也对应一个事件 ID。所以日志能不能用,首先取决于你敢不敢开。

1.1 事件 ID 速查:先记住这几个

安全日志里事件 ID 特别多,但日常排查高频用到的不超过 15 个。下面这几个你可以当成基础词汇表记:

事件 ID含义什么时候要看
4624登录成功非工作时间、异地 IP 登录时必须查
4625登录失败IP 尝试频率高时基本就是爆破
4672分配给特殊权限管理员权限被调用的一次记录
4688进程创建配合命令行参数可还原攻击执行链
4720创建用户账户系统莫名多了账务,第一反应查这个
4732将成员添加到安全组攻击者提权后把自己拉进管理组的痕迹
4698创建计划任务持久化常用手法,几乎跟服务安装并列
7045安装服务注意,这个属于系统日志,但必须配合看
1102安全日志已清除看到这个 ID,基本可以确认机器已经出过事

我见过太多人一上来就翻 4625,觉得暴力破解最重要。这没错,但有个顺序问题:先看有没有 1102,即日志清空。如果日志被人主动清过,那后面所有分析都要带着更高警惕去做——因为你看不到的东西可能比看到的更多。

1.2 用 Get-WinEvent 做基础日志查询

最直观的命令是 Get-WinEvent。筛选 4625 登录失败事件并提取 IP、用户名、登录类型:

Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} -MaxEvents 20 | ForEach-Object { $e = [xml]$_.ToXml() $t = $e.Event.EventData.Data [PSCustomObject]@{ Time = $_.TimeCreated User = ($t | Where-Object { $_.Name -eq 'TargetUserName' }).'#text' LogonType = ($t | Where-Object { $_.Name -eq 'LogonType' }).'#text' IpAddress = ($t | Where-Object { $_.Name -eq 'IpAddress' }).'#text' } } | Format-Table

这里有个细节值得强调:4625 里的 LogonType 必须和 IP 一起看。登录类型 3 表示网络共享访问,你只看 IP 不看类型,容易把正常网络扫描误判为爆破。类型 10 是远程交互登录,也就是 RDP,一旦 RDP 出现连续的 4625,那才是真正需要高度警惕的暴力破解信号。

如果你发现日志量巨大,查询变慢,别硬撑着翻全部。先按时间段过滤:

$start = Get-Date "2025-01-01 00:00:00" Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=$start} -MaxEvents 100

统计一小时内的失败次数:

$cutoff = (Get-Date).AddHours(-1) Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=$cutoff} | Group-Object {$_.Properties[13].Value} | Select-Object Count, Name | Sort-Object Count -Descending

这是一个非常常用的短时间段聚合思路。看到某个 IP 一小时失败几十次,基本不用犹豫,直接把对应 IP 拉黑。

1.3 日志容量与覆盖策略:别等出事才想起设置

默认情况下,Windows 安全日志容量是 20MB。对于一台正常办公机,20MB 可能够用一星期;但对于一台被重点盯防的服务器,20MB 撑不过一天,一旦满了,老日志会被覆盖。出事之后再去看,只剩最近的碎片,什么都查不出来。

我建议直接把安全日志容量加到 1GB,并在满容量时暂停写入而非覆盖。虽然暂停写入会暂时停止记录新日志,但总好过让关键痕迹被冲掉。这个操作在事件查看器的“安全日志属性”里改就行,也可以用 wevtutil:

wevtutil set-log Security /ms:1048576

单位是字节,1048576 就是 1GB。设置完日志自动归档,旧日志不会终身堆积,Windows 会在接近满容量时生成 .evtx 归档文件,仍然可以从“应用程序和服务日志”里翻。

另一个反复有人问的问题:为什么事件查看器里 4688(进程创建)有时根本没有?因为默认情况下进程创建审计只记录部分事件,并且不带命令行参数。想要完整记录攻击链,必须把命令行参数也纳入审计。在组策略编辑器里找到“本地计算机策略 -> 计算机配置 -> 管理模板 -> 系统 -> 审核进程创建”,把“包括命令行”设为已启用。这一步强烈建议在一开始就做,因为你永远不知道哪一次进程创建会牵扯出整个事件链条。

2. 端口和服务的收敛:把不必要的门焊死

端口和服务这两个词永远一起出现。很多系统被人拿走权限,根本原因不在系统漏洞,而是某个服务在监听一个不该监听的端口,然后被扫到。

2.1 先盘点:当前机器开了哪些端口

推荐用 PowerShell 命令,一眼看全监听端口和对应进程 ID:

Get-NetTCPConnection -State Listen | Select-Object LocalAddress, LocalPort, OwningProcess | Sort-Object LocalPort

或者用大家更熟悉的老牌命令:

netstat -ano | findstr LISTENING

拿到 OwningProcess 之后,定位是哪个程序开的端口:

Get-Process -Id <PID> | Select-Object ProcessName, Path

在这个命令后面加上 Path 真的很重要。我看过太多人只查进程名,看到 svchost 就以为正常。实际上恶意代码经常把自己伪装成 svchost.exe、explorer.exe 这类常见进程。Path 字段能直接看出程序真正所在的路径。如果 svchost.exe 的路径不是 C:\Windows\System32,那基本可以断定是被替换了。

2.2 根据进程判断该不该关

对外监听端口里,常见的危险暴露区有这几个:135 是 RPC,139/445 是 SMB,3389 是远程桌面。这三组端口一旦在公网暴露,扫描工具闭着眼睛都能扫到。

很多人第一反应是“把服务停掉、把端口关掉”。这没错,但要注意顺序:先看必要,再看替代。比如 445 对应的是“Server”服务,停止它会影响共享和打印;如果这台机器确实需要共享,就不能简单粗暴地停服务,而应该用防火墙规则把访问源限制在局域网。再比如 135 RPC 端口,如果你要用 Windows 管理工具远程管理这台机器,就需要保留。先问自己一句——这个服务是不是这台机器的核心职能?不是,就停;是,就用防火墙限制来源。

停服务的标准操作,以打印后台服务 Spooler 为例:

Stop-Service Spooler -Force Set-Service Spooler -StartupType Disabled

这里有个细节:Stop-Service 只是把当前运行停掉,重启后又会起来。一定要把启动类型改成 Disabled,再配合 sc 命令确认:

sc.exe config Spooler start= disabled sc.exe query Spooler

注意 sc 语法里 start= 后面必须有一个空格,否则命令会静默失败。写脚本的时候这个空格特别容易丢,别问我怎么知道的。

2.3 用防火墙规则把端口白名单化

停服务是一刀切的方案,但生产环境里很多端口根本不能停。这时最好的做法不是堵死,而是用防火墙做成“默认拒绝、名单放行”。

比如 RDP(3389),我想限制为只允许公司办公网络的某个网段访问,就把 Windows 自带的“远程桌面”组规则先禁用,再新做两条规则:

# 禁用默认规则组 Disable-NetFirewallRule -DisplayGroup "远程桌面" # 仅允许公司办公网段访问 3389 New-NetFirewallRule -DisplayName "RDP Allow Office" ` -Direction Inbound -Protocol TCP -LocalPort 3389 ` -RemoteAddress 192.168.10.0/24 -Action Allow # 其余来源一律阻断 New-NetFirewallRule -DisplayName "RDP Block Others" ` -Direction Inbound -Protocol TCP -LocalPort 3389 ` -RemoteAddress Any -Action Block

这里有一个很常见的认知误区:默认规则“远程桌面”不只是允许 3389,它同时包含针对不同配置文件的规则,如果你只是新增一条阻止规则而不禁用默认组,可能会出现“新的阻止规则优先级不如默认的允许规则”的情况。我一直建议直接先禁用整组再建新规则,逻辑最清晰。

端口收敛这件事,做完之后要重新跑一次第一段的查看命令,确认监听端口列表和预期一致。改完不看结果,等于白改。

2.4 关闭端口后要顺手做的几件事

还有一个被忽略的点:很多服务即使你通过防火墙封了端口,进程还在持续监听。防火墙拦截的是外部流量,不解决进程本身的问题。如果攻击者已经在本机执行了代码,他完全可以走到进程内部,不必通过防火墙。所以理想情况是“没用的服务直接停,必须用的服务才用防火墙收口”。停不掉的也要盯住进程路径、版本和签名,确保它没有被替换。

3. 文件、注册表和驱动:完整性检查三板斧

如果说日志是事后追查,那完整性检查就是事中预警。攻击者落地到系统里,一定会改动某些文件;想在开机时自动运行,一定会写注册表或者创建计划任务。这些改动不是每次都能被实时拦截,但只要你留下了一份基线,就能在巡检时快速发现异常。

3.1 关键文件哈希基线怎么建

Windows 查看当前文件夹每个文件的 hash,PowerShell 可以直接批量做。先以 system32 下可能会被替换的常见程序为例:

Get-ChildItem C:\Windows\System32\cmd.exe, C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe | Get-FileHash -Algorithm SHA256

输出里每次的 Hash 值,定期重跑,然后对比。文件被替换或者被植入后门,哈希必然变化。

更实用的是建基线。第一次跑的时候把结果保存成 CSV:

Get-ChildItem "C:\Program Files\某重要应用服务目录" -Recurse -File | Get-FileHash -Algorithm SHA256 | Export-Csv "D:\baseline\app_hashes.csv" -NoTypeInformation

之后巡检的时候重新生成一份,再和基线比对:

$new = Get-ChildItem "C:\Program Files\某重要应用服务目录" -Recurse -File | Get-FileHash -Algorithm SHA256 $old = Import-Csv "D:\baseline\app_hashes.csv" Compare-Object $old $new -Property Path, Hash

这里有个经验值得分享:不要把整块 system32 全部做哈希基线,文件太多、更新频繁,会产生大量误报。实用性最强的对象是:对外提供服务的关键应用目录、启动目录下的可执行文件、以及有可能被伪装替换的系统常用小工具。对一般业务系统,挑 50 到 100 个关键文件建基线就够了。

3.2 注册表的权限与审计

攻击者要做持久化,最爱去的地方就是 Run 键和计划任务。Run 键挂的是登录时启动的程序,计划任务则更灵活,能指定时间、指定用户、指定触发条件。

先看几个关键 Run 键:

reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" reg query "HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run" reg query "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"

看到可疑路径、可疑文件名,马上用哈希验证。很多后门就是在这里写一行命令,指向某个不起眼的目录里的可执行文件。

还有一个很常见但容易被当成系统问题的场景:设备管理器或某个驱动报错“由于其配置信息(注册表中的)不完整或已损坏”。这个报错通常意味着对应设备的注册表键值残留损坏,比如 USB、网卡、显卡这类设备在异常断电后经常出现。处理思路是:先到设备管理器识别是哪类设备,然后卸载对应设备,再重新扫描硬件改动。如果仍然报错,就要手动清理设备类键值。这个操作有风险,做之前记住导出当前注册表分支做备份。清理的时候千万别删错类键,不然会波及一批设备。

注册表权限收敛也值得做一遍。重点检查这几个项的 Everyone 和 Users 权限,非必要不应该有写权限:

  • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
  • HKLM\SYSTEM\CurrentControlSet\Services
  • HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options

特别是 Image File Execution Options,这个键出镜率极高,攻击者可以在里面设置 Debugger 项,让某个程序启动时先执行他安插的进程。

3.3 驱动数字签名的验证方法

热词里有一个很典型的场景:“Windows 无法验证此设备所需的驱动程序的数字签名,某软件或硬件最近有所更改”。出现这个弹窗,常见于刚换新硬件、升级过系统、或某个驱动被篡改。

先验签名,用 PowerShell 看签名和签名状态:

Get-AuthenticodeSignature C:\Windows\System32\some.dll

状态字段如果是 NotSigned 或者 HashMismatch,那基本可以判定文件已经被改动过。对驱动文件,还可以用:

Get-AuthenticodeSignature C:\Windows\System32\drivers\某个驱动.sys

如果是公司内部自己写的驱动没签名,那就是另外一回事;但凡是正规硬件的驱动,签名状态都会是 Valid。看到 NotSigned 的第三方驱动还加载进系统,第一反应应该是去找官方签名版本,而不是绕过签名强制加载。系统越新,签名校验越严格。我见过不少人想着用开发模式把签名强制关掉,这条路不仅危险,还会让整个系统的完整性防线崩溃。

驱动签名问题还经常伴随着启动问题。处理顺序建议是:先卸载异常设备,再安装官方最新签名驱动;如果驱动安装后系统依然报“配置信息不完整”,把注册表里的 InstalledServices 残项清理掉再重装。实在不行,才考虑硬件本身是否损坏。

4. 补丁与自动更新:做得到可控比追求“永远最新”更现实

补丁话题谈起来很矛盾。出漏洞的时候大家骂微软补丁不及时;补丁打了出兼容性问题,又骂补丁惹祸。但 Windows 系统安全的一大半工作,本质上是补丁管理。你前面把端口、服务、账户都管好了,一个高危漏洞没补,等于前功尽弃。

4.1 先搞清当前补丁状态

查补丁状态最常用的命令是 Get-HotFix:

Get-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 10

要检查某个特定补丁是否安装,按 HotFixID 过滤:

Get-HotFix | Where-Object HotFixID -eq "KB5044273"

如果你的机器连的都是域或者终端管理服务器,应该用统一管理平台查看;但对单机或没有域控的环境,Get-HotFix 足够用了。

很多人不知道的是,Get-HotFix 查到的只是已安装的更新,不一定能反映“最新状态”。更准确的做法是连微软更新服务对比,这需要用到 Windows Update Module,或者直接查注册表里的 UpdateHistory。我日常用的对比方式是看安装时间和系统发布时间,如果系统最近一个月都没有任何新补丁记录,就该查一查更新服务是不是已经停了。

4.2 把更新变成受控操作,而不是随缘

这里要专门回应一个网上高频问题:“windows 更新医生服务会启动自动更新吗?”——很多人会用第三方工具或命令手动禁用 Windows Update 服务,之后发现“更新医生”这类修复工具扫完一次,Windows Update 服务又自己恢复正常了,甚至开始自动下载补丁,感到莫名其妙。

原理很简单:更新医生这类工具的职责就是把 Windows 更新组件恢复到系统默认状态。我之前手动停掉、禁用的服务,属于“被破坏的状态”,修复工具把所有相关服务的启动类型重置为默认,禁用当然就被解除了。如果你真的想在某段时间内不让系统自动打补丁,正确做法不是禁用服务,而是通过设置里的“暂停更新”功能设置一个暂停窗口,或者在组策略里把自动更新行为改成“通知下载”。这样既不会硬碰硬地对抗系统机制,也不会在关键时刻被突发更新搞崩。

补丁时间选择上,我的习惯是:对于非关键业务机器,紧跟月度更新;对于核心业务服务器,出来后等 7 到 10 天看社区反馈再打。别追求刚出补丁就当第一批小白鼠。重点补丁一定要养成记录的习惯,哪台机器、哪一天、打了哪些补丁、有没有异常,写进值班记录本或者维护清单里。

# 查看最近一次成功更新时间 Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\UpdateHistory" -Name UpdateHistory | Out-Null # 通过历史记录快速筛选最近更新 Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-WindowsUpdateClient'; Id=19} -MaxEvents 5 | Select-Object TimeCreated, Message

事件 ID 19 是 Windows Update 成功安装的事件,ID 20 是安装失败,ID 25 是“正在重新启动”。这三个 ID 足够你快速判断一台机器的更新状况。

4.3 更新翻车后的回滚方案

补丁打崩了系统,第一步永远是进“设置 -> 系统 -> 恢复 -> 高级启动”,或者用“卸载更新”入口直接回滚。命令行也能看补丁卸载,但实际操作中图形界面更省事。

如果一个补丁导致系统连桌面都进不去,开机按 F8 进高级启动,选择“命令提示符”,用系统还原点回滚。这套操作重装系统的人也懂,但区别在于打完补丁之后要立刻建立一个还原点。我见过很多人从来不建还原点,然后把系统折腾到只能重装。补丁管理不只是“下载安装”,还包括准备回滚路径。

顺带说一句:别把关闭 Windows Update 服务当成稳定运行的手段。在一台长期没有补丁的机器上做安全加固,就像拿漏水的桶盛水——你花了大把时间端口收敛、账户管理、日志监控,结果一个公开已知漏洞就能让桶又漏完。补丁这关,省不了。

5. 主机信息收集与日常巡检:用脚本把安全变成习惯

日志要看、端口要查、哈希要验、补丁要盯。这些事每一项单拎出来工作量都不小,如果每次全靠手工一条命令一条命令敲,坚持不了两周就会放弃。真正可持续的做法是写一个巡检脚本,把关键信息集中收进一个报告,每天或每周跑一次,凭报告判断系统是否正常。

5.1 信息收集命令组合

我习惯把收集分为几个固定维度:账户、启动项、计划任务、开放端口、监听进程、补丁状态。分别看:

# 账户和组:重点看管理员组成员有没有生面孔 Get-LocalGroupMember -Group "Administrators" | Select-Object Name, PrincipalSource # 本地用户状态 Get-LocalUser | Select-Object Name, Enabled, PasswordLastSet, LastLogon # 计划任务 Get-ScheduledTask | Where-Object State -ne 'Disabled' | Select-Object TaskName, TaskPath # 开放监听端口 Get-NetTCPConnection -State Listen | Select-Object LocalAddress, LocalPort, OwningProcess

只看账户还不够,要留意账户名相似但权限异常的账户,比如 Admin、Administrator2 这种。攻击者创建后门账户时往往取一个跟常规账户很像的名字,不检查你根本注意不到。

5.2 输出一个准实时的巡检报告

把上面的命令组合成一个脚本,输出 CSV 文件。下面是一个极简但可用的骨架:

$date = Get-Date -Format "yyyyMMdd" $out = "D:\SecurityAudit_$date.csv" $items = @() # 管理员组成员 $items += Get-LocalGroupMember -Group "Administrators" | ForEach-Object { [PSCustomObject]@{Type='AdminGroup'; Name=$_.Name; Detail='Member'} } # 注册表 Run 键 $runKeys = @( "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run", "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run", "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" ) foreach ($k in $runKeys) { if (Test-Path $k) { $props = Get-ItemProperty $k $props.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' } | ForEach-Object { $items += [PSCustomObject]@{ Type='RunKey'; Name=$_.Name; Detail=$_.Value } } } } # 计划任务 $items += Get-ScheduledTask | Where-Object { $_.State -ne 'Disabled' } | ForEach-Object { [PSCustomObject]@{ Type='ScheduledTask'; Name=$_.TaskName; Detail=$_.TaskPath } } # 监听端口 $items += Get-NetTCPConnection -State Listen | ForEach-Object { $p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue [PSCustomObject]@{ Type='ListenPort'; Name="$($_.LocalAddress):$($_.LocalPort)"; Detail=$p.Path } } $items | Export-Csv $out -NoTypeInformation -Encoding UTF8 Write-Host "巡检报告已生成:$out"

脚本里加了几个我到后期才意识到很重要的细节。Get-Process 查 Path 必须加 -ErrorAction SilentlyContinue,否则碰到权限不足的进程会整条脚本中断。计划任务的 TaskPath 一定要记录,因为攻击者经常把任务放在 \Microsoft\Windows\ 下伪装成系统任务,单看 TaskName 很难发现。

这个脚本输出的是一张扁平的 CSV 表,看多了其实挺累。我后来改成只做“对比”:第一次巡检结果存成一个历史文件,后面每次运行都把新旧结果对比,输出差异。这样一眼就能看到这次和上次比,多了哪个管理员、多了哪个 Run 键项目、多了哪个监听端口。

5.3 巡检周期怎么设计,结果拿来怎么用

对大多数 Windows 工作机,一周一次足够;对外暴露的服务器,建议每天一次。重点不是巡检频率本身,而是每次巡检之后把结果和上一次对比。人眼扫不出变化,但脚本能。

我这边一个很深的体会是:安全巡检不是为了抓攻击者的,更重要的功能是让自己熟悉这台机器的“正常长相”。当你知道自己的系统平时长什么样——哪些端口在监听、哪些计划任务在跑、哪些补丁最近装上过——你会突然发现,发现异常的敏感度提高了。哪一天多了个进程、改了个启动项,你不需要分析太久就能判断出它该不该存在。这种熟悉感,恰恰是安全运营里最难替代的部分。

巡检脚本里我还建议加一项:检查本地用户是否有空密码或永不过期密码的账户。目标机器上新装的第三方软件一不留神就会创建本地账户,有些账户连密码都不设。这个风险比远程爆破还直接,因为攻击者一旦在内网摸到这台机器,空密码账户就是敞开的门。

到这里,Windows 系统安全第二篇的主体内容算是讲完了。最后补一句实操体会:这款系统安全强化到你不可能一劳永逸,所有机制本质上都是帮助你更快发现异常。你越熟悉自己机器的日常表现,就越容易在异常刚冒头的阶段把它摁掉。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询