mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-26 21:21:53 +08:00
feat: cleaner 支持清理所有持久化痕迹,minidump 增加杀软前置检测
cleaner 新增 Windows 持久化完整清理: - Winlogon Shell/Userinit 恢复默认值 - IFEO 映像劫持清理 - 注册表 Run/RunOnce 清理 - 计划任务、服务、启动文件夹清理 - BITS 任务、WMI 事件订阅清理 - Prefetch 记录清理 minidump 增加杀软前置检测,发现 Defender/EDR 直接退出, 避免 MiniDumpWriteDump hang 导致系统崩溃
This commit is contained in:
@@ -27,28 +27,19 @@ func (p *CleanerPlugin) Scan(ctx context.Context, info *common.HostInfo, session
|
|||||||
var output strings.Builder
|
var output strings.Builder
|
||||||
var cleaned int
|
var cleaned int
|
||||||
|
|
||||||
// 清理工作目录下的 fscan 产物
|
// 清理 fscan 产物文件
|
||||||
workDir, _ := os.Getwd()
|
workDir, _ := os.Getwd()
|
||||||
cleaned += p.cleanFiles(&output, workDir, []string{
|
cleaned += p.cleanFiles(&output, workDir, []string{
|
||||||
"result.txt", "result.json", "result.csv",
|
"result.txt", "result.json", "result.csv",
|
||||||
"fscan_debug.log",
|
"fscan_debug.log",
|
||||||
})
|
})
|
||||||
|
|
||||||
// 清理临时目录
|
|
||||||
cleaned += p.cleanGlob(&output, os.TempDir(), "fscan_*")
|
cleaned += p.cleanGlob(&output, os.TempDir(), "fscan_*")
|
||||||
|
|
||||||
// 清理自身可执行文件(如果在工作目录)
|
// 清理持久化痕迹(平台特定)
|
||||||
if exe, err := os.Executable(); err == nil {
|
cleaned += cleanPersistence(&output)
|
||||||
base := filepath.Base(exe)
|
|
||||||
if strings.Contains(strings.ToLower(base), "fscan") && filepath.Dir(exe) == workDir {
|
|
||||||
cleaned += p.cleanFiles(&output, workDir, []string{base})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 平台特定清理
|
// 平台通用文件清理
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "windows":
|
|
||||||
cleaned += p.cleanWindows(&output)
|
|
||||||
case "linux", "darwin":
|
case "linux", "darwin":
|
||||||
cleaned += p.cleanUnix(&output)
|
cleaned += p.cleanUnix(&output)
|
||||||
}
|
}
|
||||||
@@ -86,22 +77,10 @@ func (p *CleanerPlugin) cleanGlob(output *strings.Builder, dir, pattern string)
|
|||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CleanerPlugin) cleanWindows(output *strings.Builder) int {
|
|
||||||
cleaned := 0
|
|
||||||
// Prefetch 中的 fscan 记录
|
|
||||||
cleaned += p.cleanGlob(output, `C:\Windows\Prefetch`, "FSCAN*.pf")
|
|
||||||
// Recent 中的 fscan 快捷方式
|
|
||||||
if profile := os.Getenv("USERPROFILE"); profile != "" {
|
|
||||||
cleaned += p.cleanGlob(output, filepath.Join(profile, "Recent"), "fscan*.lnk")
|
|
||||||
}
|
|
||||||
return cleaned
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *CleanerPlugin) cleanUnix(output *strings.Builder) int {
|
func (p *CleanerPlugin) cleanUnix(output *strings.Builder) int {
|
||||||
cleaned := 0
|
cleaned := 0
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
|
||||||
// 从 history 文件中删除 fscan 相关行
|
|
||||||
histFiles := []string{
|
histFiles := []string{
|
||||||
filepath.Join(homeDir, ".bash_history"),
|
filepath.Join(homeDir, ".bash_history"),
|
||||||
filepath.Join(homeDir, ".zsh_history"),
|
filepath.Join(homeDir, ".zsh_history"),
|
||||||
@@ -113,10 +92,8 @@ func (p *CleanerPlugin) cleanUnix(output *strings.Builder) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /tmp 下的 fscan 残留
|
|
||||||
cleaned += p.cleanGlob(output, "/tmp", "fscan_*")
|
cleaned += p.cleanGlob(output, "/tmp", "fscan_*")
|
||||||
cleaned += p.cleanGlob(output, "/tmp", ".fscan*")
|
cleaned += p.cleanGlob(output, "/tmp", ".fscan*")
|
||||||
|
|
||||||
return cleaned
|
return cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build (plugin_cleaner || !plugin_selective) && !windows && !no_local
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func cleanPersistence(output *strings.Builder) int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
//go:build (plugin_cleaner || !plugin_selective) && windows && !no_local
|
||||||
|
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cleanPersistence(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
|
||||||
|
// 1. 清理 Winlogon 劫持——恢复默认值
|
||||||
|
cleaned += fixWinlogon(output)
|
||||||
|
|
||||||
|
// 2. 清理 IFEO 映像劫持
|
||||||
|
cleaned += cleanIFEO(output)
|
||||||
|
|
||||||
|
// 3. 清理注册表 Run 键
|
||||||
|
cleaned += cleanRegistryRun(output)
|
||||||
|
|
||||||
|
// 4. 清理计划任务
|
||||||
|
cleaned += cleanScheduledTasks(output)
|
||||||
|
|
||||||
|
// 5. 清理服务
|
||||||
|
cleaned += cleanServices(output)
|
||||||
|
|
||||||
|
// 6. 清理启动文件夹
|
||||||
|
cleaned += cleanStartupFolders(output)
|
||||||
|
|
||||||
|
// 7. 清理 BITS 任务
|
||||||
|
cleaned += cleanBITS(output)
|
||||||
|
|
||||||
|
// 8. 清理 WMI 事件订阅
|
||||||
|
cleaned += cleanWMI(output)
|
||||||
|
|
||||||
|
// 9. 清理 Prefetch
|
||||||
|
cleaned += cleanPrefetch(output)
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func fixWinlogon(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
key := `HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon`
|
||||||
|
|
||||||
|
// 检查 Shell 是否被篡改
|
||||||
|
if out, err := exec.Command("reg", "query", key, "/v", "Shell").CombinedOutput(); err == nil {
|
||||||
|
val := extractRegValue(string(out))
|
||||||
|
if val != "explorer.exe" && val != "" {
|
||||||
|
exec.Command("reg", "add", key, "/v", "Shell", "/t", "REG_SZ", "/d", "explorer.exe", "/f").Run()
|
||||||
|
output.WriteString(fmt.Sprintf("[恢复] Winlogon Shell: %s → explorer.exe\n", val))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 Userinit 是否被篡改
|
||||||
|
if out, err := exec.Command("reg", "query", key, "/v", "Userinit").CombinedOutput(); err == nil {
|
||||||
|
val := extractRegValue(string(out))
|
||||||
|
defaultVal := `C:\Windows\system32\userinit.exe,`
|
||||||
|
if val != defaultVal && val != strings.TrimSuffix(defaultVal, ",") && val != "" {
|
||||||
|
exec.Command("reg", "add", key, "/v", "Userinit", "/t", "REG_SZ", "/d", defaultVal, "/f").Run()
|
||||||
|
output.WriteString(fmt.Sprintf("[恢复] Winlogon Userinit: %s → %s\n", val, defaultVal))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanIFEO(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
targets := []string{"sethc.exe", "utilman.exe", "narrator.exe"}
|
||||||
|
for _, t := range targets {
|
||||||
|
key := fmt.Sprintf(`HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\%s`, t)
|
||||||
|
if out, err := exec.Command("reg", "query", key, "/v", "Debugger").CombinedOutput(); err == nil && strings.Contains(string(out), "Debugger") {
|
||||||
|
exec.Command("reg", "delete", key, "/f").Run()
|
||||||
|
output.WriteString(fmt.Sprintf("[清理] IFEO: %s\n", t))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanRegistryRun(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
keys := []string{
|
||||||
|
`HKCU\Software\Microsoft\Windows\CurrentVersion\Run`,
|
||||||
|
`HKLM\Software\Microsoft\Windows\CurrentVersion\Run`,
|
||||||
|
`HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce`,
|
||||||
|
}
|
||||||
|
markers := []string{"fscan", "test_payload", "WindowsUpdate_", "SystemUpdate_", "SetupComplete_"}
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
out, err := exec.Command("reg", "query", key).CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
for _, m := range markers {
|
||||||
|
if strings.Contains(line, m) {
|
||||||
|
fields := strings.Fields(strings.TrimSpace(line))
|
||||||
|
if len(fields) > 0 {
|
||||||
|
exec.Command("reg", "delete", key, "/v", fields[0], "/f").Run()
|
||||||
|
output.WriteString(fmt.Sprintf("[清理] 注册表: %s\\%s\n", key, fields[0]))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanScheduledTasks(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
markers := []string{"WindowsUpdateCheck_", "SystemSecurityScan_", "MaintenanceTask_", "BackgroundService_"}
|
||||||
|
out, err := exec.Command("schtasks", "/query", "/fo", "csv", "/nh").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
for _, m := range markers {
|
||||||
|
if strings.Contains(line, m) {
|
||||||
|
parts := strings.Split(line, ",")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
name := strings.Trim(parts[0], "\"\\")
|
||||||
|
exec.Command("schtasks", "/delete", "/tn", name, "/f").Run()
|
||||||
|
output.WriteString(fmt.Sprintf("[清理] 计划任务: %s\n", name))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanServices(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
markers := []string{"WinDefendUpdate_", "SysHealthMon_"}
|
||||||
|
for _, m := range markers {
|
||||||
|
out, err := exec.Command("sc", "query", "state=", "all").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
if strings.Contains(line, "SERVICE_NAME") && strings.Contains(line, m) {
|
||||||
|
name := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "SERVICE_NAME:"))
|
||||||
|
exec.Command("sc", "stop", name).Run()
|
||||||
|
exec.Command("sc", "delete", name).Run()
|
||||||
|
output.WriteString(fmt.Sprintf("[清理] 服务: %s\n", name))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanStartupFolders(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
dirs := []string{
|
||||||
|
filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup"),
|
||||||
|
filepath.Join(os.Getenv("ProgramData"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup"),
|
||||||
|
}
|
||||||
|
for _, dir := range dirs {
|
||||||
|
matches, _ := filepath.Glob(filepath.Join(dir, "test_payload*"))
|
||||||
|
for _, f := range matches {
|
||||||
|
if os.Remove(f) == nil {
|
||||||
|
output.WriteString(fmt.Sprintf("[清理] 启动文件夹: %s\n", f))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanBITS(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
out, err := exec.Command("bitsadmin", "/list", "/allusers").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
if strings.Contains(line, "WindowsUpdate_") || strings.Contains(line, "fscan") {
|
||||||
|
// 提取 GUID
|
||||||
|
if idx := strings.Index(line, "{"); idx != -1 {
|
||||||
|
if end := strings.Index(line[idx:], "}"); end != -1 {
|
||||||
|
guid := line[idx : idx+end+1]
|
||||||
|
exec.Command("bitsadmin", "/cancel", guid).Run()
|
||||||
|
output.WriteString(fmt.Sprintf("[清理] BITS: %s\n", guid))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanWMI(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
ps := `
|
||||||
|
Get-WmiObject -Namespace root\subscription -Class __FilterToConsumerBinding | Where-Object { $_.Consumer -match 'SysExec_' -or $_.Consumer -match 'fscan' } | Remove-WmiObject
|
||||||
|
Get-WmiObject -Namespace root\subscription -Class CommandLineEventConsumer | Where-Object { $_.Name -match 'SysExec_' -or $_.Name -match 'fscan' } | Remove-WmiObject
|
||||||
|
Get-WmiObject -Namespace root\subscription -Class __EventFilter | Where-Object { $_.Name -match 'SysMon_' -or $_.Name -match 'fscan' } | Remove-WmiObject
|
||||||
|
Write-Output 'WMI_CLEANED'
|
||||||
|
`
|
||||||
|
out, err := exec.Command("powershell", "-NoProfile", "-Command", ps).CombinedOutput()
|
||||||
|
if err == nil && strings.Contains(string(out), "WMI_CLEANED") {
|
||||||
|
output.WriteString("[清理] WMI 事件订阅\n")
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPrefetch(output *strings.Builder) int {
|
||||||
|
cleaned := 0
|
||||||
|
matches, _ := filepath.Glob(`C:\Windows\Prefetch\FSCAN*.pf`)
|
||||||
|
for _, f := range matches {
|
||||||
|
if os.Remove(f) == nil {
|
||||||
|
output.WriteString(fmt.Sprintf("[清理] Prefetch: %s\n", f))
|
||||||
|
cleaned++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractRegValue(output string) string {
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.Contains(line, "REG_SZ") {
|
||||||
|
parts := strings.SplitN(line, "REG_SZ", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return strings.TrimSpace(parts[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user