一台只能 SSH 登录的 Windows 机器,装了一堆"系统清理、大文件清理、DLL 修复、壁纸"类的免费工具。用户的原话是:"卸载了,重启又回来了,删了文件夹也自己长出来。"
这类软件不是普通的"卸载不干净",它是一套互相保护的常驻组件:一个伪装成系统进程的守护、一个同名 Windows 服务、一个登录时重建的计划任务,外加内核驱动。你用控制面板卸载,它只删掉外壳;你用资源管理器删目录,它下次登录重建。下面是我们在一台无显示器节点上把这类软件清干净的全过程与判据,命令可以直接复用。
第一个要建立的概念:Windows 真正的系统宿主进程叫 svchost.exe(c 在前),而这类软件投放的守护进程叫 srvhost.exe。两个名字只差两个字母的顺序,任务管理器里一眼扫过去几乎一样,这是刻意的。
所以侦察第一步就是把它的真身捞出来,而不是靠肉眼看进程名:
wmic process where "name='srvhost.exe'" get ProcessId,ExecutablePath,CommandLine
输出里的 ExecutablePath 会直接落到 C:\Program Files (x86)\ 下某个厂商目录。拿到这个路径,整条链就有锚点了。
我们遇到的这批家族有共同特征,看到这些目录名基本可以直接判定:
| 目录 / 进程名 | 伪装身份 | 实际行为 |
|---|---|---|
DupsClean / LargeFileClean | 重复文件、大文件清理 | 常驻扫描磁盘,弹窗诱导付费 |
DllFix / DllFixTray | DLL 修复工具 | 托盘常驻,劫持 shell 扩展 |
WinClean / WinOptimize / WinEase / EasyClean | 系统优化 | 自启 + 计划任务重建 |
WhaleMemory | 内存优化 | 反复释放内存,实际拖慢系统 |
HaloWallPaper | 动态壁纸 | 注入 explorer 渲染 |
GamingBlast / LdsSurfAssistant / TinyDL / qbblink | 游戏加速、上网助手、下载器 | 互装互保,装一个带一串 |
它们共享同一套骨架:每个产品一个 srvhost.exe 守护 + 一个同名服务 + 一个登录触发的计划任务,删掉其中一个,另外两个会把它拉起来。所以清除必须按"进程 → 服务 → 注册表 → 目录 → 计划任务 → 重启验证"的顺序整体做,中途停手等于没做。
目标机没有图形界面,卸载程序弹窗会卡死 SSH 通道,所以只能命令行硬删。用 paramiko 跑命令时,第一件要定的事是解码方式:
stdout.read().decode('gbk', 'ignore')
中文版 Windows 的控制台输出是 GBK 代码页,用 UTF-8 解出来是乱码,而乱码会让你误判"命令没输出"——我们第一轮就是这么把一条成功的 sc delete 当成失败的。
还有两个同源的坑:
"C:\\Program Files (x86)\\Xxx",单个 \U、\P 会触发 unicodeescape 报错;.bat 文件不能有中文注释。cmd 按 GBK 解析,UTF-8 的中文注释会把首行吃成乱码,整段脚本一行都不执行,而你还以为它跑过了。批处理一律纯 ASCII。这是最容易翻车的一步。正确写法是:
sc delete dllfixSvc
错误写法是:
sc delete "HKLM\SYSTEM\CurrentControlSet\Services\dllfixSvc"
后者会返回 1060(指定的服务不存在) 或 123,看起来像"服务本来就没有",实际上是参数形态错了。sc 只认 ServiceName,也就是注册表 Services 下面那一层的键名。
批量捞服务名的办法是按 ImagePath 反查:
reg query "HKLM\System\CurrentControlSet\Services" /s /v ImagePath | findstr /i "DupsClean LargeFileClean DllFix WinClean WhaleMemory HaloWallPaper WinOptimize WinEase EasyClean GamingBlast LdsSurfAssistant TinyDL qbblink 360"
内核驱动型的(TYPE 1 KERNEL_DRIVER,如 wpsedrv、winoptdrv)同样用 sc delete,但要注意下一节说的文件释放问题。
sc delete 只是把服务注册项标了删除,没重启之前文件是被锁的,你删目录会得到"拒绝访问"。这不是权限问题,是加载问题——先重启,再删文件,顺序反了会白折腾半小时。很多教程教你删 C:\Windows\System32\Tasks\ 下的任务文件,删完 schtasks /query 里确实看不到了。但重启之后它又出现——因为任务调度器真正的索引在注册表里:
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache
正确顺序是先尝试正规删除,失败再动注册表:
schtasks /delete /tn "SoftLandingXXX" /f
如果报 access denied(这类任务常以 SYSTEM 身份注册),就直接三件套一起下:
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks\{guid}" /f
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tree\SoftLandingXXX" /f
rmdir /s /q "C:\Windows\System32\Tasks\SoftLandingXXX"
找 {guid} 的办法是先按 URI 反查:
reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Schedule\TaskCache\Tasks" /s /v URI | findstr /i "softland"
枚举所有第三方任务的办法是抓 schtasks /query /fo LIST 里的"任务名:",把含 \Microsoft\ 的行全过滤掉——剩下的就是第三方。这一步能立刻看出"谁在登录时把东西拉起来"。
同一句 rmdir,在三种情况下会有三种完全不同的失败姿态,对症才对:
| 症状 | 根因 | 解法 |
|---|---|---|
| "拒绝访问" | 进程占用,或 DLL 仍被加载 | 先 taskkill,服务删后先重启再删 |
| "找不到路径"(但目录明明在) | 路径超过 260 字符(Electron 类应用 node_modules 深层嵌套) | robocopy 空目录 目标 /mir /r:0 /w:0 清空后再 rmdir |
| takeown + icacls 之后仍删不掉 | 软件写了 DENY ACL | PowerShell Remove-Item -LiteralPath ... -Recurse -Force |
超长路径那条尤其反直觉:rmdir 报"系统找不到指定的路径",你会以为路径拼错了,其实是 API 长度上限。用 robocopy 做镜像清空是绕过它最稳的一招:
robocopy "C:\temp\empty" "C:\Program Files (x86)\SomeVendor\deep\path" /mir /r:0 /w:0
还有一种情况是 explorer 的 shell 扩展(常见 context.dll)锁着目录,这时可以 taskkill /f /im explorer.exe 后立刻删——explorer 会自动重启,删除窗口期足够完成。
最彻底的清法是"在流氓进程启动之前把目录删掉"。做法是上传一个纯 ASCII 的清理批处理,注册成 SYSTEM 权限的开机任务,然后重启:
schtasks /create /tn cleanupX /ru SYSTEM /rl highest /sc onstart /tr "C:\temp\cleanupX.bat" /f
shutdown /r /t 5 /f
/sc onstart 的任务在用户登录之前执行,此时没有任何用户级自启项在跑,目录无锁,删得干干净净。批处理末尾可以顺手删掉自己和这个任务。
远程连接会因为重启中断,重连要写成循环:间隔 8~10 秒重试,最多二十几次。别用一次 connect 就下结论说"机器挂了"。
清理脚本最容易造成的二次伤害,是把系统组件当流氓删了。我们实际踩到三个:
dups 会命中 ReFsDedupSvc这是 Windows ReFS 的去重服务,系统自带。删了它,ReFS 卷的相关功能会异常。恢复命令:
sc create ReFsDedupSvc binPath= "%SystemRoot%\System32\ReFsDedupSvc.exe" type= own start= demand obj= LocalSystem
教训是:批量删除前,命中列表必须逐条人工确认,不能"关键词命中即删除"。
SoftLanding 类的任务删了还会回来,而且无害如果某任务的动作是"COM 处理程序",且它的 CLSID 在 HKCR 下没有注册,同时每次登录都会被系统重建——这通常是 Windows 自带的消费者功能任务,不是流氓。删它只会让你在"删了又复活"的错觉里多绕一圈。
PddCoreService 不是严格意义的 PUP它是真实购物 App 的后台服务(含 PCDN 上传),不是流氓软件。想停它的带宽占用可以 sc delete,但 App 文件还在,用户重装就回来。这类要跟用户说清楚,别打包进"清除战果"里充数。
| 要验的项 | 命令 | 合格标准 |
|---|---|---|
| 守护进程 | tasklist /fi "imagename eq srvhost.exe" | 无结果 |
| 流氓服务 | sc query <名> / reg query Services | 1060 不存在 |
| 流氓目录 | if exist "C:\Program Files (x86)\厂商" echo ALIVE | 无输出 |
| 第三方向导任务 | schtasks /query /fo LIST | 只剩含 \Microsoft\ 的 |
| 机器级自启 | reg query "HKLM\...\Run" | 仅剩系统/本职项 |
| 用户级自启 | 逐个 HKU\<SID>\...\Run | 无拼音/怪名项 |
补一条经验:验证要在重启后做,不能在当天做。计划任务重建、驱动重新加载都发生在开机阶段,当天的"干净"是假象。我们的做法是清理完重启,等 10 分钟再连回去,把上面六项全跑一遍,输出留档。
把上面所有细节剥掉,剩下四条是可以直接搬到任何一台 Windows 上的方法:
if exist 为假,不构成结论。ReFsDedupSvc 这种误伤,恢复成本比清理成本高得多。如果这台机器还要长期跑业务(比如当渲染节点、客服机),清完之后顺手把 Run 键收敛到"系统必需 + 本职服务"两项,比事后反复清理省事得多。
因为卸载器只删外壳。这类软件的守护进程(srvhost.exe)、同名服务、登录触发的计划任务是三条独立的复活路径,只要有一条还在,其余会被重新拉起。必须按进程→服务→注册表→目录→计划任务的顺序整体清除,并在重启后验证。
不一定。如果你传的是完整注册表路径(HKLM\...\Services\Foo)而不是服务名 Foo,sc 就会报 1060 或 123。先确认参数形态,再判断服务是否真的不存在。
任务调度器的索引在注册表 TaskCache 下。删了 C:\Windows\System32\Tasks\ 里的 XML 之后,还要删 TaskCache\Tasks\{guid} 与 TaskCache\Tree\<任务名> 两处,否则会被重建。
两种可能:一是服务虽已删除但 DLL 仍被加载,需要重启释放;二是软件写了 DENY 类型的 ACL。前者重启即可,后者用 PowerShell 的 Remove-Item -LiteralPath ... -Recurse -Force 最稳。
路径超过 260 字符了。用 robocopy "空目录" "目标目录" /mir /r:0 /w:0 把目标清空,再用 rmdir 删空壳,可以绕开长度限制。
用 sc create ReFsDedupSvc binPath= "%SystemRoot%\System32\ReFsDedupSvc.exe" type= own start= demand obj= LocalSystem 可以恢复。根因是关键词 dups 同时命中了流氓软件名和这个系统服务,批量删除前必须逐条人工确认。