双份 tasklist 快照揪出隐藏后门:一次 Backdoor/Lotok(银狐)木马的排查与清除实录
摘要:一次真实环境下的 Windows 后门木马应急排查实录。起点只有两份普通的 tasklist 快照,最终定位并清除一个采用「白利用 + BYOVD 内核对抗」的远控后门——微软命名 Backdoor/Lotok(国内安全社区通称 「银狐」家族)的变种。文中给出完整的排查思路、可复用脚本与 IOC 清单。
〇、TL;DR
一份普通的 tasklist 快照里藏着远控后门。我靠两份间隔 14 分钟的进程快照比对锁定异常进程,顺着查到 4 个持久化复活点(伪装服务 ×2、Winlogon Userinit 劫持、隐藏计划任务、内核漏洞驱动),完成证据固定 → 提权清除 → 重启复核,全程系统自带 PowerShell,零第三方工具。
事后经公开情报比对定性:样本属于 Backdoor/Lotok 家族(国内安全社区通称 「银狐」)的变种——一个以白利用 + BYOVD 内核对抗著称的远控后门木马。
沉淀出的方法论:排查五步(定异常 → 验身份 → 挖持久 → 查外联 → 核情报)+ 处置五则(断网 → 留证 → 拆除 → 复核 → 善后)。三个脚本附在文末,IOC 在第十一章。
一、背景
某天怀疑自己的 Windows 11 工作机中招(Windows PowerShell 5.1 环境)。手头能用的起点只有两份 tasklist /v /fo csv 快照,导出间隔约 14 分钟。
tasklist /v /fo csv > 快照1.csv :: 间隔 10~15 分钟后再导出一次 tasklist /v /fo csv > 快照2.csv
两次导出、Excel 或 Beyond Compare 一对比即可(也可以直接目视 CPU 时间列)。当然,能上机的话直接用后面的脚本全量扫也行,快照比对适合"先期低打扰排查"和"远程指导别人自查"的场景。
最终结果:定位并清除了一个活跃的远控后门。后经情报比对,定性为 Backdoor/Lotok(国内通称「银狐」)家族变种,采用白利用 + BYOVD 内核对抗,重启后无复活。
二、总体思路
木马排查本质上就回答五个问题:
|
步骤 |
问题 |
手段 |
|---|---|---|
|
① 定异常 |
哪个进程不该在? |
双快照比对 CPU 时间增量 |
|
② 验身份 |
它是什么文件、从哪来? |
路径 / 数字签名 / 版本信息 / 哈希 |
|
③ 挖持久 |
它靠什么复活? |
服务、Winlogon、计划任务、驱动、Run 键 |
|
④ 查外联 |
它在跟谁通信? |
TCP 连接归属到 PID |
|
⑤ 核情报 |
它是不是已知家族? |
哈希/路径组合搜威胁情报 |
确认感染后,处置按五则执行:断网 → 留证 → 拆除 → 复核 → 善后。
三、第一步:定异常(双快照比对)
核心看 CPU 时间 列——它是累计值,只有持续干活的进程才会涨。本例数据:
|
进程 |
PID |
会话 |
CPU 时间(前→后) |
备注 |
|---|---|---|---|---|
|
XUo.exe |
5148 |
Services |
0:01:05 → 0:02:55 |
14 分钟涨了 110 秒,后台一直在狂跑 |
|
aWNCH.exe |
5012 |
Services |
0:00:03 → 0:00:05 |
与 XUo 同目录同体积(110,208 字节) |
|
aWNCH.exe |
8384 |
Console(当前用户) |
0:00:06 → 0:00:07 |
内存 97MB,父进程 winlogon.exe |
判读要点:
-
Services 会话 + 随机大小写混淆命名(XUo / aWNCH)+ 状态 Unknown,这组特征本身就是强信号;
-
只看 CPU 会漏人:aWNCH 几乎不耗 CPU(纯控制型后门),是靠"内存占用 + 会话归属 + 父进程"交叉出来的;
-
父进程是
winlogon.exe是最大红旗——说明它由"登录流程"拉起,十有八九存在持久化劫持; -
后续还观察到过该进程的第三个实例(PID 22120),说明有自我拉起 / 多实例机制。
四、第二步:验身份(路径 / 签名 / 哈希)
坑位 1:普通权限读不到系统进程的路径
Win32_Process 的 ExecutablePath 对 SYSTEM 进程直接返回空。绕过方法:用服务信息反查——Win32_Service.PathName 或注册表 HKLM\SYSTEM\CurrentControlSet\Services\<服务名>\ImagePath(代码见 9.1 脚本的 A 段)。
反查结果:C:\Drivers\MdudrUu\aWNCH.exe。
注意:这是根目录下自建的"非常规目录"。正常软件不会把组件丢在 C:\Drivers 这种地方——红旗 +1。
签名 + 版本信息交叉验证
-
签名状态:
Valid,签名主体还是正规厂商(本案是某知名办公软件厂商); -
但版本信息全空——CompanyName、FileDescription 都是空白。
这是典型的签名滥用 / 白利用:借有效签名过信誉检查,但正规软件不可能连版本信息都不写。 "有有效签名" ≠ "该文件可信"。 三者交叉(签名 + 版本信息 + 哈希)才能定性。
再取 SHA256 留作情报检索:
B5AE5E8325AB0D35EFA8F0C6B9B50702EEDE7F32EDEDE7516CDFDFE3D42951C6 (aWNCH.exe)
坑位 2:文件偶尔"读不到" ≠ 文件不存在
排查中曾出现文件明明在目录里、Get-Item 却报找不到的情况(被拦截/占用/保护)。 换个时机、提权或进安全模式重试即可,不要因此下"文件不存在"的结论。
五、第三步:挖持久(四个复活点)
5.1 伪装服务 ×2
LogHelper => cmd /c cd /d "C:\Drivers\MdudrUu" && start "" "C:\Drivers\MdudrUu\aWNCH.exe" RemoteHandler => cmd /c cd /d "C:\Drivers\MdudrUu" && start "" "C:\Drivers\MdudrUu\XUo.exe"
特征:cmd /c ... && start 链式启动 + 指向非常规目录 + 自动启动 + LocalSystem 权限。
重点知识点:cmd 执行完 start 后就退出了,所以 SCM 里这两个服务显示的都是**"已停止"**,但目标进程早就被拉起来了。服务显示"Stopped"绝不代表没干活——它的"干活"就是把木马拉起来然后功成身退。
5.2 Winlogon Userinit 劫持
Userinit = C:\Windows\system32\userinit.exe,C:\Drivers\MdudrUu\aWNCH.exe.,
在原值后面追加一个启动项(还带个".,"尾巴做障眼法),每次登录它都复活。这也是 8384 号进程父进程是 winlogon.exe 的原因。
5.3 隐藏计划任务
同名计划任务 LogHelper → aWNCH.exe。
这个是清理阶段扫出来的——排查时容易只盯服务和注册表而漏掉计划任务。持久化清单里务必包含:服务、Winlogon、计划任务、Run/RunOnce、IFEO、驱动、AppInit。
5.4 内核漏洞驱动(BYOVD)
ardrv => \??\C:\Drivers\MdudrUu\ardrv.sys (KERNEL_DRIVER)
该驱动被指为 OPSWAT ARD(AppRemover Driver)的漏洞利用载体(CVE-2026-36425),是典型的 BYOVD(Bring Your Own Vulnerable Driver,自带易受攻击驱动) 手法:用带合法签名但存在漏洞的驱动进内核,然后关防护、杀安全软件进程/回调。这就是为什么普通杀软对它束手无策。
自查命令(只读):
# 找 cmd /c 链式启动的服务
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' | ForEach-Object {
$ip = (Get-ItemProperty -LiteralPath $_.PSPath).ImagePath
if ($ip -and $ip -match 'cmd /c') { $_.PSChildName + ' => ' + $ip }
}
# 看 Userinit 是否被加料(正常值以 userinit.exe, 结尾)
(Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon').Userinit
六、第四步:查外联
把嫌疑 PID 拿去做 OwningProcess 过滤,一条命令揪出 C2:
Get-NetTCPConnection | Where-Object { $_.OwningProcess -in 5012,5148,8384 } | Select-Object OwningProcess, LocalAddress, LocalPort, RemoteAddress, RemotePort, State
输出(节选):
pid=8384 192.168.x.x:598xx -> 222.167.35.123:443 Established pid=8384 192.168.x.x:598xx -> 222.167.35.123:443 TimeWait(多条)
aWNCH.exe(8384)与 222.167.35.123:443 保持长连接、且有多条 TimeWait——反复回连,实锤 C2。
提示:低 CPU 消耗的"纯控制型"木马,外联是它最藏不住的地方。
七、第五步:核情报
7.1 情报命中
拿"路径组合 + 哈希"去搜索引擎/情报平台检索,命中信息量很大:
-
有网友在同期(感染次日)发帖报告完全相同的路径组合:
C:\Drivers\MdudrUu+XUo.exe+ardrv.sys,截图中 Defender 对该文件报 Backdoor/Lotok.kw,并同样提到 ardrv.sys(CVE-2026-36425); -
说明这不是孤例,而是一波同期的投毒/挂马活动(我们的感染时间戳与网友案例只差一天)。
7.2 家族定性:Backdoor/Lotok(国内通称「银狐」)
以 Defender 检出名 Backdoor/Lotok.kw 为线索继续检索,样本的家族与类型就清晰了:
-
家族名称:Backdoor/Lotok——微软 Defender 的命名(
.kw为变种后缀,同类还有.MC、.sbc等);国内安全社区与厂商(火绒、奇安信等)通常把该家族统称为 「银狐」(Silver Fox),奇安信沙箱等平台亦报Backdoor.Lotok.xxx; -
恶意类型:远控后门木马(Backdoor / RAT)。模块化架构,可向受控端下发任意功能模块(窃密、远程控制、代理转发等);该家族源码曾在暗网外泄,导致变种极多、迭代极快;
-
典型手法(与本案观察高度吻合):
-
白利用(白加黑):正规签名程序本体 + 同目录恶意 DLL 侧加载。本案
aWNCH.exe带有效签名但版本信息全空,同目录还配有dtfbase.dll、ipc_core.dll、filter_installer_biz.dll等组件——经典白加黑结构; -
BYOVD 内核对抗:滥用带签名的漏洞驱动(本案
ardrv.sys/ CVE-2026-36425)进入内核,关闭防护、杀安全软件进程; -
加密载荷 + 多重持久化 + 多实例守护(本案的加密数据块与 4 个复活点)。
-
-
为什么杀软对它"视而不见":白利用骗过信誉检查、BYOVD 从内核层关掉防护,双重压制下许多终端 AV 全程无感——这也解释了本机 Defender 的"零检出、零排除项"。再次强调:杀软没报 ≠ 干净,签名、情报、外联要交叉验证。
参考:
CVE-2026-36425:https://www.cve.org/CVERecord?id=CVE-2026-36425
「银狐」(Lotok)家族技术报告:https://www.huorong.cn/document/tech/vir_report/1846
八、处置与清除
8.1 处置原则
断网 → 留证 → 拆除 → 复核 → 善后
-
断网(拔网线 / 关 Wi-Fi):先掐 C2,防止外传和被下发新指令;
-
留证:先全量复制副本(本案 robocopy 备份了 3 个目录共 21 个文件 / 127MB),再删除原文件;对外只提供哈希,切勿传播样本;
-
提权:清理需要管理员权限(普通账号可用
Start-Process -Verb RunAs拉起 UAC); -
拆除顺序(重点):删服务(先断复活链)→ 修注册表 → 杀进程 → 删文件。顺序反了的话,刚杀死的进程会被残留服务立刻拉活,删文件永远删不干净;
-
复核:重启后对"服务 / 注册表键 / 进程 / 目录 / 网络连接"五项全量复查,确认无复活;
-
善后:Windows Defender 脱机扫描(专治内核级残留)+ 重要账号改密(按已泄露处理)+ 上报公司 IT/安全部门 + 观察一周。
8.2 清除脚本做了什么
cleanup_malware.ps1(见 9.3 第三个脚本)严格按上述顺序执行,特点:
-
全程写日志(时间戳逐条落盘,留证据链);
-
服务删除不写死名字,而是按 IOC 全量匹配(
ardrv|MdudrUu|XUo|aWNCH)——正是这一步扫出了我们此前没发现的内核驱动服务和隐藏计划任务; -
删除前先 robocopy 留证;
-
幂等设计,可安全地在安全模式下重跑。
重启后复查:三个服务"未安装"、注册表零残留、无恶意进程、无 C2、Userinit 正常——无复活,收工。
九、脚本分享
说明:以下为当时实际使用版本整理,已做通用化/脱敏(输出目录改为脚本所在目录,人员信息已去除)。三个脚本可分别另存为 .ps1 文件直接使用。 复用到自己的案例时,把脚本里的 IOC 字符串(进程名 / 服务名 / 路径 / IP)替换成你的目标值即可。
9.1 scan_suspects.ps1 —— 只读排查(大部分检查不需要管理员)
八个检查面:A 嫌疑进程详情(含签名/哈希)、A2 CPU TOP25、B 服务匹配、B2 用户可写目录服务(高风险)、C Run 键、C2 启动文件夹、D Defender 排除项与检出历史、E 嫌疑进程外联、F 标准目录之外的进程、G 文件定位、H 计划任务。
# scan_suspects.ps1 - read-only triage. Replace $names with your own suspect list / IOC pattern.
$ErrorActionPreference = 'SilentlyContinue'
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Write-Output ('=== 0. admin=' + $isAdmin)
$names = @('XUo.exe','aWNCH.exe','shtctky.exe','aha_doctor.exe','FlutterPlugins.exe','qmbrowser.exe','eaio_service.exe','eaio_agent.exe','tposd.exe','tphkload.exe','FMService64.exe','SmartStandby.exe','LITSSvc.exe','WeMail.exe','WeMailNode_x64.exe','igfxCUIServiceN.exe','m365copilot_autostarter.exe','DynamicDependencyLifetimeManagerShadow.exe')
$procs = @(Get-CimInstance Win32_Process | Where-Object { $names -contains $_.Name })
$svcs = @(Get-CimInstance Win32_Service)
Write-Output ('=== A. suspect processes running now: ' + $procs.Count)
if ($procs.Count -eq 0) { Write-Output ' (none of them are running now)' }
foreach ($p in $procs) {
Write-Output ('---- ' + $p.Name + ' | PID=' + $p.ProcessId + ' | PPID=' + $p.ParentProcessId)
$par = Get-CimInstance Win32_Process -Filter ('ProcessId=' + $p.ParentProcessId)
if ($par) { Write-Output (' Parent: ' + $par.Name) }
Write-Output (' Path : ' + $p.ExecutablePath)
Write-Output (' Cmd : ' + $p.CommandLine)
foreach ($s in ($svcs | Where-Object { $_.ProcessId -eq $p.ProcessId })) {
Write-Output (' Service: ' + $s.Name + ' | ' + $s.PathName + ' | as ' + $s.StartName)
}
$fp = $p.ExecutablePath
if (-not $fp) {
$s0 = $svcs | Where-Object { $_.ProcessId -eq $p.ProcessId } | Select-Object -First 1
if ($s0 -and $s0.PathName) {
if ($s0.PathName -match '^\s*"([^"]+)"') { $fp = $matches[1] }
elseif ($s0.PathName -match '^\s*(\S+\.exe)') { $fp = $matches[1] }
}
}
if ($fp -and (Test-Path -LiteralPath $fp)) {
$fi = Get-Item -LiteralPath $fp
$sig = Get-AuthenticodeSignature -LiteralPath $fp
Write-Output (' Signer : ' + $sig.Status + ' / ' + $sig.SignerCertificate.Subject)
Write-Output (' Company: ' + $fi.VersionInfo.CompanyName + ' | ' + $fi.VersionInfo.FileDescription)
Write-Output (' SHA256 : ' + (Get-FileHash -LiteralPath $fp -Algorithm SHA256).Hash)
} else { Write-Output ' (path not readable / no admin)' }
}
Write-Output '=== A2. top 25 CPU now ==='
Get-Process | Sort-Object CPU -Descending | Select-Object -First 25 | ForEach-Object { Write-Output (' ' + $_.ProcessName + ' pid=' + $_.Id + ' cpu=' + [math]::Round($_.CPU,1) + 's mem=' + [math]::Round($_.WorkingSet64/1MB,0) + 'MB') }
Write-Output '=== B. services matching suspect names ==='
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' | ForEach-Object {
$ip = (Get-ItemProperty -LiteralPath $_.PSPath).ImagePath
if ($ip -and $ip -match 'XUo|aWNCH|shtctky|aha_doctor|Flutter|qmbrowser|eaio|tposd|tphkload|FMService64') { Write-Output (' ' + $_.PSChildName + ' => ' + $ip) }
}
Write-Output '=== B2. services running from user-writable dirs (high risk) ==='
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' | ForEach-Object {
$ip = (Get-ItemProperty -LiteralPath $_.PSPath).ImagePath
if ($ip -and $ip -match 'AppData|ProgramData|\\Temp\\|Users\\Public|^\\\\') { Write-Output (' ' + $_.PSChildName + ' => ' + $ip) }
}
Write-Output '=== C. Run keys ==='
foreach ($rk in @('HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run','HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run','HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run','HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce','HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce')) {
$props = Get-ItemProperty -LiteralPath $rk
if ($props) {
Write-Output ('[' + $rk + ']')
$props.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object { Write-Output (' ' + $_.Name + ' = ' + $_.Value) }
}
}
Write-Output '=== C2. Startup folders ==='
Get-ChildItem -LiteralPath @('C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup', ($env:APPDATA + '\Microsoft\Windows\Start Menu\Programs\Startup')) | ForEach-Object { Write-Output (' ' + $_.FullName) }
Write-Output '=== D. Defender exclusions / detections ==='
$mp = Get-MpPreference
Write-Output (' ExclusionPath: ' + ($mp.ExclusionPath -join ' ; '))
Write-Output (' ExclusionProcess: ' + ($mp.ExclusionProcess -join ' ; '))
$td = @(Get-MpThreatDetection)
if ($td.Count -gt 0) { $td | Sort-Object InitialDetectionTime -Descending | Select-Object -First 8 | ForEach-Object { Write-Output (' Threat: ' + $_.ThreatID + ' | ' + ($_.Resources -join ',') + ' | ' + $_.InitialDetectionTime) } } else { Write-Output ' (no detection history, or need admin)' }
Write-Output '=== E. TCP conns of suspect PIDs ==='
$spids = @($procs | ForEach-Object { $_.ProcessId })
if ($spids.Count -gt 0) {
Get-NetTCPConnection | ForEach-Object { if ($spids -contains $_.OwningProcess) { Write-Output (' pid=' + $_.OwningProcess + ' -> ' + $_.RemoteAddress + ':' + $_.RemotePort + ' ' + $_.State) } }
}
Write-Output '=== F. processes outside Windows/Program Files ==='
Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -and $_.ExecutablePath -notmatch '(?i)^C:\\(Windows|Program Files|Program Files \(x86\))\\' } | Sort-Object Name | ForEach-Object { Write-Output (' ' + $_.Name + ' | ' + $_.ExecutablePath) }
Write-Output '=== G. locate XUo/aWNCH/shtctky files ==='
foreach ($r in @('C:\Program Files','C:\Program Files (x86)','C:\ProgramData',($env:LOCALAPPDATA),($env:APPDATA),($env:USERPROFILE + '\AppData\Local\Temp'))) {
Get-ChildItem -Path $r -Recurse -Include 'XUo.exe','aWNCH.exe','shtctky.exe' -Force | ForEach-Object { Write-Output (' ' + $_.FullName + ' | ' + $_.LastWriteTime) }
}
Write-Output '=== H. scheduled tasks referencing suspects ==='
Get-ScheduledTask | ForEach-Object { $ex = (($_.Actions | ForEach-Object { $_.Execute }) -join '; '); if ($ex -match 'XUo|aWNCH|shtctky|aha_doctor|FlutterPlugins|qmbrowser|eaio|tposd|tphkload') { Write-Output (' ' + $_.TaskPath + $_.TaskName + ' => ' + $ex) } }
Write-Output '=== DONE ==='
9.2 scan2_evidence.ps1 —— 证据固定(含本案 IOC,复用请替换)
# scan2_evidence.ps1 - evidence capture (case-specific IOCs: service names / IP / paths; replace when reusing)
$ErrorActionPreference='SilentlyContinue'
Write-Output '=== 1. C:\Drivers children ==='
Get-ChildItem 'C:\Drivers' -Force | ForEach-Object { Write-Output ('DIR : ' + $_.Name + ' | ' + $_.LastWriteTime) }
Write-Output '=== 1b. C:\Drivers\MdudrUu content ==='
Get-ChildItem 'C:\Drivers\MdudrUu' -Recurse -Force | ForEach-Object { Write-Output (' ' + $_.FullName + ' | ' + $_.Length + ' B | created ' + $_.CreationTime + ' | modified ' + $_.LastWriteTime) }
Write-Output '=== 2. suspect file details ==='
foreach ($f in @('C:\Drivers\MdudrUu\aWNCH.exe','C:\Drivers\MdudrUu\XUo.exe')) {
$i = Get-Item -LiteralPath $f
if ($i) {
$sig = Get-AuthenticodeSignature -LiteralPath $f
Write-Output ($f + ' | ' + $i.Length + ' B | created ' + $i.CreationTime)
Write-Output (' sig: ' + $sig.Status + ' / ' + $sig.SignerCertificate.Subject)
Write-Output (' company: ' + $i.VersionInfo.CompanyName + ' | ' + $i.VersionInfo.FileDescription)
Write-Output (' sha256: ' + (Get-FileHash -LiteralPath $f -Algorithm SHA256).Hash)
} else { Write-Output ($f + ' NOT FOUND') }
}
Write-Output '=== 3. service metadata LogHelper / RemoteHandler ==='
Get-CimInstance Win32_Service -Filter "Name='LogHelper' or Name='RemoteHandler'" | ForEach-Object {
Write-Output (' ' + $_.Name + ' | Display=' + $_.DisplayName + ' | Desc=' + $_.Description + ' | StartMode=' + $_.StartMode + ' | State=' + $_.State + ' | StartAs=' + $_.StartName)
Write-Output (' Path=' + $_.PathName)
}
Write-Output '=== 4. all services launched via cmd /c ==='
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' | ForEach-Object {
$ip = (Get-ItemProperty -LiteralPath $_.PSPath).ImagePath
if ($ip -and $ip -match 'cmd /c') { Write-Output (' ' + $_.PSChildName + ' => ' + $ip) }
}
Write-Output '=== 5. processes 5148 / 8384 now ==='
Get-Process -Id 5148,8384 | ForEach-Object { Write-Output (' ' + $_.ProcessName + ' pid=' + $_.Id + ' cpu=' + [math]::Round($_.CPU,1) + 's started=' + $_.StartTime) }
Write-Output '=== 6. connections to 222.167.35.123 now ==='
Get-NetTCPConnection | Where-Object { $_.RemoteAddress -eq '222.167.35.123' } | ForEach-Object { Write-Output (' pid=' + $_.OwningProcess + ' ' + $_.LocalAddress + ':' + $_.LocalPort + ' -> ' + $_.RemoteAddress + ':' + $_.RemotePort + ' ' + $_.State) }
Write-Output '=== DONE ==='
9.3 cleanup_malware.ps1 —— 清除(必须管理员运行)
# cleanup_malware.ps1 - Backdoor removal (case: LogHelper/RemoteHandler + Userinit + drivers payload dir)
# Actions: delete fake services (incl. kernel driver), restore Userinit, kill processes, quarantine + remove payloads.
# MUST run as Administrator. Idempotent: safe to re-run (e.g. again in Safe Mode).
# Log + quarantine are written to the script's own directory.
$ErrorActionPreference = 'SilentlyContinue'
$root = $PSScriptRoot; if (-not $root) { $root = (Get-Location).Path }
$log = Join-Path $root 'cleanup_log.txt'
$quar = Join-Path $root 'quarantine'
function L($m) { $line = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + ' ' + $m; Add-Content -LiteralPath $log -Value $line; Write-Host $line }
L '===== cleanup start ====='
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
L ('isAdmin=' + $isAdmin)
if (-not $isAdmin) { L 'ABORT: not running elevated'; exit 1 }
# ---- 0) before snapshot
L '--- BEFORE ---'
Get-Process -Name XUo,aWNCH | ForEach-Object { L ('proc: ' + $_.ProcessName + ' pid=' + $_.Id + ' path=' + $_.Path) }
$wk = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
L ('Userinit before: ' + (Get-ItemProperty -LiteralPath $wk).Userinit)
# ---- 1) stop & delete fake services (+ any service matching IOC paths)
$svcNames = @('LogHelper','RemoteHandler')
Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' | ForEach-Object {
$ip = (Get-ItemProperty -LiteralPath $_.PSPath).ImagePath
if ($ip -match '(?i)ardrv|MdudrUu|XUo|aWNCH') { $svcNames += $_.PSChildName }
}
$svcNames | Sort-Object -Unique | ForEach-Object {
L ('svc stop/delete: ' + $_)
(sc.exe stop $_ 2>&1) | ForEach-Object { L (' ' + $_) }
(sc.exe delete $_ 2>&1) | ForEach-Object { L (' ' + $_) }
}
# ---- 2) restore Userinit hijack
Set-ItemProperty -LiteralPath $wk -Name Userinit -Value 'C:\Windows\system32\userinit.exe,' -Type String
L ('Userinit after: ' + (Get-ItemProperty -LiteralPath $wk).Userinit)
L ('Shell now: ' + (Get-ItemProperty -LiteralPath $wk).Shell)
# ---- 3) kill malicious processes (by name or by path under C:\Drivers\)
Get-Process | Where-Object { $_.ProcessName -match '(?i)^(XUo|aWNCH)$' -or $_.Path -match '(?i)^C:\\Drivers\\' } | ForEach-Object {
L ('kill: ' + $_.ProcessName + ' pid=' + $_.Id + ' path=' + $_.Path)
Stop-Process -Id $_.Id -Force
}
Start-Sleep -Seconds 2
Get-Process -Name XUo,aWNCH | ForEach-Object { L ('STILL ALIVE: ' + $_.ProcessName + ' pid=' + $_.Id) }
# ---- 4) quarantine copy, then delete payload
New-Item -ItemType Directory -Force -Path $quar | Out-Null
$dirs = @('C:\Drivers\MdudrUu','C:\Drivers\93Yb3h','C:\Drivers\oY8Uz')
foreach ($d in $dirs) {
if (Test-Path -LiteralPath $d) {
$dst = Join-Path $quar ((Split-Path $d -Leaf) + '_copy')
robocopy $d $dst /E /COPY:DAT /R:0 /W:0 /NFL /NDL /NJH /NJS | Out-Null
L ('quarantine copied: ' + $d + ' -> ' + $dst + ' (robocopy=' + $LASTEXITCODE + ')')
}
}
$targets = @($dirs)
$targets += (Get-ChildItem 'C:\Drivers' -Force | Where-Object { $_.Name -like 'drivers.dat*' } | ForEach-Object { $_.FullName })
foreach ($t in $targets) {
if (Test-Path -LiteralPath $t) {
L ('remove: ' + $t)
Get-ChildItem -LiteralPath $t -Recurse -Force | ForEach-Object { $_.Attributes = 'Normal' }
(Get-Item -LiteralPath $t -Force).Attributes = 'Normal'
Remove-Item -LiteralPath $t -Recurse -Force 2>&1 | ForEach-Object { L (' ' + $_) }
L (' still exists: ' + (Test-Path -LiteralPath $t))
}
}
# ---- 5) other persistence quick checks
L '--- Run keys check ---'
foreach ($k in 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run','HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce','HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run','HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce') {
$props = Get-ItemProperty -LiteralPath $k
if ($props) {
$props.PSObject.Properties | Where-Object { $_.Name -notmatch '^PS' -and ($_.Value -match '(?i)MdudrUu|XUo|aWNCH|ardrv') } | ForEach-Object {
L ('run entry FOUND: ' + $k + ' :: ' + $_.Name + ' = ' + $_.Value)
Remove-ItemProperty -LiteralPath $k -Name $_.Name
L ('run entry removed: ' + $_.Name)
}
}
}
L '--- scheduled tasks check ---'
Get-ScheduledTask | ForEach-Object {
$task = $_
$task.Actions | ForEach-Object {
if ($_.Execute -match '(?i)MdudrUu|XUo|aWNCH' -or $_.Arguments -match '(?i)MdudrUu|XUo|aWNCH') {
L ('task FOUND: ' + $task.TaskName + ' :: ' + $_.Execute + ' ' + $_.Arguments)
Unregister-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath -Confirm:$false
L ('task removed: ' + $task.TaskName)
}
}
}
L '--- AppInit / leftover check ---'
L ('AppInit_DLLs = ' + (Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows' -Name AppInit_DLLs).AppInit_DLLs)
Get-ChildItem 'C:\Drivers' -Force | ForEach-Object { L ('leftover in C:\Drivers: ' + $_.Name) }
# ---- 6) after verification
L '--- AFTER ---'
Get-Service LogHelper,RemoteHandler | ForEach-Object { L ('svc still: ' + $_.Name + ' ' + $_.Status) }
Get-Process -Name XUo,aWNCH | ForEach-Object { L ('proc still: ' + $_.ProcessName + ' pid=' + $_.Id) }
'C:\Drivers\MdudrUu','C:\Drivers\93Yb3h','C:\Drivers\oY8Uz' | ForEach-Object { L ('path ' + $_ + ' exists=' + (Test-Path -LiteralPath $_)) }
Get-NetTCPConnection | Where-Object { $_.RemoteAddress -eq '222.167.35.123' } | ForEach-Object { L ('C2 conn still: pid=' + $_.OwningProcess + ' ' + $_.State) }
L '===== cleanup finished ====='
十、踩坑清单(PowerShell 5.1 特供)
-
-f格式化字符串遇 null 直接抛异常,整条输出丢失。排查中间产物里如果有 null 字段(比如读不到的进程路径),'{0}' -f $null会把整行炸掉导致假数据丢失。改用字符串拼接 + null 判断; -
中文字符的 .ps1 在 GBK 控制台输出乱码(UTF-8 无 BOM 保存的脚本)。应急排查脚本建议全 ASCII 标签,中文只写在终端外的文档里;
-
sc在 PowerShell 里是Set-Content的别名!删服务必须写sc.exe,否则命令静默跑偏; -
非管理员读不到 SYSTEM 进程的路径——用服务
PathName/注册表ImagePath反查绕过; -
文件读取偶发失败 ≠ 文件不存在(被拦截/占用/保护),换时机或提权重试;
-
服务显示"已停止" ≠ 目标进程停了——
cmd /c ... start链式启动的服务拉起进程后就退出了; -
只看 CPU 会漏掉"低消耗纯控制型"进程——至少 CPU + 内存 + 会话归属 + 父进程 + 外联 多维交叉。
十一、IOC 附录
|
类型 |
内容 |
|---|---|
|
恶意目录 |
|
|
样本哈希 |
aWNCH.exe SHA256 = |
|
伪装服务 |
|
|
注册表 |
|
|
计划任务 |
|
|
C2 |
|
|
投放器 |
|
|
关联文件 |
|
|
关联漏洞 |
CVE-2026-36425(OPSWAT ARD 驱动,BYOVD) |
|
检出名 |
Backdoor/Lotok.kw(Microsoft Defender;国内通称「银狐」家族) |
十二、快速自查清单
# 1) Userinit 是否被加料(正常值:C:\Windows\system32\userinit.exe, ) (Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon').Userinit # 2) 有没有 cmd /c 链式启动的可疑服务 Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' | ForEach-Object { $ip = (Get-ItemProperty -LiteralPath $_.PSPath).ImagePath if ($ip -and $ip -match 'cmd /c') { $_.PSChildName + ' => ' + $ip } } # 3) 有没有服务从用户可写目录(AppData/ProgramData/Temp)运行 Get-ChildItem 'HKLM:\SYSTEM\CurrentControlSet\Services' | ForEach-Object { $ip = (Get-ItemProperty -LiteralPath $_.PSPath).ImagePath if ($ip -and $ip -match 'AppData|ProgramData|\\Temp\\|Users\\Public') { $_.PSChildName + ' => ' + $ip } } # 4) 有没有外联可疑 IP 的进程(把可疑 IP 换成你要查的) Get-NetTCPConnection -RemoteAddress 222.167.35.123 -ErrorAction SilentlyContinue | Select-Object OwningProcess, State # 5) 盘根目录下有没有陌生的自建文件夹(如 C:\Drivers、C:\Tools 之类非系统目录),里面有 exe 的更要警惕 Get-ChildItem 'C:\' -Directory -Force
浙公网安备 33010602011771号