// Command pwninit is a task-bootstrapper for winpwn, the analogue of the // pwninit tool pwntools users reach for: point it at a challenge directory // and it (1) prints the recon you'd otherwise run by hand -- arch, // checksec, sections -- so a new task is legible in one command, and (2) // scaffolds a minimal solve script (go.mod wired up via a replace directive // + a bare Spawn/Interactive main.go) so `go run .` works from that // directory immediately, instead of copying the workspace template by hand. package main import ( "errors" "fmt" "os" "os/exec" "path/filepath" "strings" "winpwn" ) // defaultLibPath is where the winpwn source lives on this machine. There's // no published module to `go get`, so every generated go.mod needs a // `replace winpwn => ` pointing at a real winpwn checkout; override // with the WINPWN_HOME environment variable if it ever moves. const defaultLibPath = `C:\tools\go_pwner` func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintf(os.Stderr, "pwninit: %v\n", err) os.Exit(1) } } func run(args []string) error { force := false targetArg := "" for _, a := range args { switch a { case "-force": force = true case "-h", "--help", "help": usage() return nil default: targetArg = a } } cwd, err := os.Getwd() if err != nil { return err } target, err := resolveTarget(cwd, targetArg) if err != nil { return err } targetBase := filepath.Base(target) fmt.Printf("[pwninit] target: %s\n\n", targetBase) if err := printRecon(target); err != nil { return err } libPath, err := resolveLibPath() if err != nil { return err } if err := scaffold(cwd, targetBase, libPath, force); err != nil { return err } fmt.Println("\n[pwninit] wrote go.mod + main.go -- next: go run .") return nil } func usage() { fmt.Fprint(os.Stderr, `pwninit -- scaffold a winpwn solve script for the challenge in this directory Usage: pwninit auto-detect the single .exe/.dll in the current directory pwninit use this file explicitly pwninit -force overwrite an existing go.mod/main.go in this directory Prints checksec/sections recon immediately, then writes a go.mod (replace winpwn => `+defaultLibPath+`, override via WINPWN_HOME) and a minimal main.go (Spawn + Interactive, nothing else assumed) ready for "go run .". `) } // resolveTarget returns explicit if set, otherwise the sole .exe/.dll in // dir -- erroring with the full candidate list if that's ambiguous, the way // a human would want to know *why* auto-detection refused to guess. func resolveTarget(dir, explicit string) (string, error) { if explicit != "" { if _, err := os.Stat(explicit); err != nil { return "", fmt.Errorf("target %q: %w", explicit, err) } return explicit, nil } var candidates []string for _, pattern := range []string{"*.exe", "*.dll"} { matches, _ := filepath.Glob(filepath.Join(dir, pattern)) candidates = append(candidates, matches...) } switch len(candidates) { case 0: return "", errors.New("no .exe/.dll found in this directory -- pass the target explicitly: pwninit ") case 1: return candidates[0], nil default: names := make([]string, len(candidates)) for i, c := range candidates { names[i] = filepath.Base(c) } return "", fmt.Errorf("multiple binaries found (%s) -- pass the target explicitly: pwninit ", strings.Join(names, ", ")) } } // printRecon prints the "understand this task in one command" block: arch, // checksec, and section permissions/entropy. func printRecon(target string) error { pf, err := winpwn.OpenPE(target) if err != nil { return err } defer pf.Close() is64, err := pf.Is64Bit() if err != nil { return err } base, _ := pf.ImageBase() entry, _ := pf.EntryPoint() arch := "x86" if is64 { arch = "x64" } fmt.Printf("Arch: %s\n", arch) fmt.Printf("ImageBase: 0x%X\n", base) fmt.Printf("EntryPoint: 0x%X\n", entry) r, err := pf.Checksec() if err != nil { return err } fmt.Println() fmt.Printf("ASLR: %s\n", yesNo(r.ASLR)) fmt.Printf("HighEntropyVA: %s\n", yesNo(r.HighEntropyVA)) fmt.Printf("DEP/NX: %s\n", yesNo(r.DEP)) fmt.Printf("CFG: %s\n", yesNo(r.CFG)) if r.SEHApplicable { fmt.Printf("SafeSEH: %s\n", yesNo(r.SafeSEH)) } else { fmt.Printf("SafeSEH: N/A (x64 uses table-based SEH)\n") } fmt.Printf("GS (heuristic): %s\n", yesNo(r.GSHeuristic)) fmt.Printf("Authenticode: %s\n", yesNo(r.AuthenticodeSigned)) fmt.Println("\n--- sections ---") for _, sec := range pf.Sections() { perm := "" if sec.IsReadable() { perm += "R" } else { perm += "-" } if sec.IsWritable() { perm += "W" } else { perm += "-" } if sec.IsExecutable() { perm += "X" } else { perm += "-" } entropy, _ := sec.Entropy() fmt.Printf(" %-8s %s VA=0x%-8X Offset=0x%-8X Size=0x%-8X entropy=%.2f\n", sec.Name, perm, sec.VirtualAddress, sec.Offset, sec.VirtualSize, entropy) } if libs, err := pf.ImportedLibs(); err == nil { fmt.Println("\n--- imported libs (live image base -- stable until next reboot) ---") for _, lib := range libs { if lib.Err != nil { fmt.Printf(" %-24s (failed to load: %v)\n", lib.Name, lib.Err) continue } fmt.Printf(" %-24s 0x%016X\n", lib.Name, lib.Base) } } return nil } func yesNo(b bool) string { if b { return "Yes" } return "No" } // resolveLibPath finds a real winpwn checkout to point the generated // go.mod's replace directive at, honoring WINPWN_HOME over the hardcoded // default so this still works if the library ever moves. func resolveLibPath() (string, error) { path := os.Getenv("WINPWN_HOME") if path == "" { path = defaultLibPath } goModPath := filepath.Join(path, "go.mod") data, err := os.ReadFile(goModPath) if err != nil { return "", fmt.Errorf("can't find winpwn at %q (%w) -- set WINPWN_HOME to override", path, err) } if !strings.Contains(string(data), "module winpwn") { return "", fmt.Errorf("%q doesn't look like the winpwn module (go.mod has no \"module winpwn\")", path) } abs, err := filepath.Abs(path) if err != nil { return "", err } return filepath.ToSlash(abs), nil } // scaffold writes go.mod + main.go into dir and runs `go mod tidy` (with // GOPROXY=off: winpwn's transitive deps are already in the local module // cache from building winpwn itself, so this never needs network access) // to fill in the indirect requires/go.sum instead of hardcoding version // strings that would drift the moment winpwn's own go.mod changes. func scaffold(dir, targetBase, libPath string, force bool) error { goModPath := filepath.Join(dir, "go.mod") mainGoPath := filepath.Join(dir, "main.go") if !force { for _, p := range []string{goModPath, mainGoPath} { if _, err := os.Stat(p); err == nil { return fmt.Errorf("%s already exists -- pass -force to overwrite", p) } } } moduleName := sanitizeModuleName(filepath.Base(dir)) goMod := fmt.Sprintf("module %s\n\ngo 1.26.2\n\nrequire winpwn v0.0.0\n\nreplace winpwn => %s\n", moduleName, libPath) if err := os.WriteFile(goModPath, []byte(goMod), 0644); err != nil { return err } if err := os.WriteFile(mainGoPath, []byte(mainGoSkeleton(targetBase)), 0644); err != nil { return err } cmd := exec.Command("go", "mod", "tidy") cmd.Dir = dir cmd.Env = append(os.Environ(), "GOPROXY=off") if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("go mod tidy: %w\n%s", err, out) } return nil } // sanitizeModuleName turns a directory name into something `go build` // accepts as a module path -- Go module paths reject spaces and most // punctuation, and CTF task directories are rarely named with that in mind. func sanitizeModuleName(name string) string { var sb strings.Builder for _, r := range name { switch { case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-': sb.WriteRune(r) default: sb.WriteRune('_') } } if sb.Len() == 0 { return "solve" } return sb.String() } func mainGoSkeleton(targetBase string) string { return fmt.Sprintf(`package main import ( "log" "winpwn" ) const target = %q func main() { tube, err := winpwn.Spawn(target) if err != nil { log.Fatalf("Spawn: %%v", err) } // TODO: exploit here tube.Interactive() } `, targetBase) }