v0.1 - initial commit
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
// 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 => <path>` 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 <target> 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 <target>")
|
||||
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 <target>",
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,573 @@
|
||||
// Command winpwn is a thin CLI wrapper around the winpwn library, the
|
||||
// analogue of pwntools' `pwn` command -- for the quick "just tell me the
|
||||
// answer" cases (checksec, an offset, a gadget search) where spinning up a
|
||||
// whole solve script is overkill. The library (package winpwn, used the
|
||||
// way pwntools itself is: `import "winpwn"` in a real solve script) is
|
||||
// still the primary interface; this is additive, not a replacement.
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"winpwn"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
var err error
|
||||
switch cmd {
|
||||
case "checksec":
|
||||
err = cmdChecksec(args)
|
||||
case "cyclic":
|
||||
err = cmdCyclic(args)
|
||||
case "hex":
|
||||
err = cmdHex(args)
|
||||
case "unhex":
|
||||
err = cmdUnhex(args)
|
||||
case "hexdump":
|
||||
err = cmdHexdump(args)
|
||||
case "rop":
|
||||
err = cmdRop(args)
|
||||
case "bytes":
|
||||
err = cmdBytes(args)
|
||||
case "disasm":
|
||||
err = cmdDisasm(args)
|
||||
case "exports":
|
||||
err = cmdExports(args)
|
||||
case "imports":
|
||||
err = cmdImports(args)
|
||||
case "heap":
|
||||
err = cmdHeap(args)
|
||||
case "help", "-h", "--help":
|
||||
usage()
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "winpwn: unknown subcommand %q\n\n", cmd)
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "winpwn: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprint(os.Stderr, `winpwn -- quick-answer CLI for the winpwn library (PE/Windows pwn toolkit)
|
||||
|
||||
Usage:
|
||||
winpwn checksec <file>
|
||||
winpwn cyclic <length> [-n N] generate a de Bruijn pattern
|
||||
winpwn cyclic -l <subseq> [-n N] find subseq's offset (subseq may be "0x..." or literal bytes)
|
||||
winpwn hex read raw bytes from stdin, print hex
|
||||
winpwn unhex read hex from stdin, print raw bytes
|
||||
winpwn hexdump <file>
|
||||
winpwn rop <file> -search "pop rcx ; ret"
|
||||
winpwn rop <file> -regex "^pop r.* ; ret$"
|
||||
winpwn bytes <file> <hex> find every VA where <hex> occurs (e.g. ebfe for jmp $)
|
||||
winpwn disasm <file> <hexaddr> <count>
|
||||
winpwn exports <file>
|
||||
winpwn imports <file>
|
||||
winpwn heap <pid> dump all heaps in a live process
|
||||
winpwn heap <pid> -walk also walk all NT Heap entries (slow on large heaps)
|
||||
`)
|
||||
}
|
||||
|
||||
func cmdChecksec(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: winpwn checksec <file>")
|
||||
}
|
||||
pf, err := winpwn.OpenPE(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pf.Close()
|
||||
|
||||
r, err := pf.Checksec()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
arch := "x86"
|
||||
if r.Is64Bit {
|
||||
arch = "x64"
|
||||
}
|
||||
fmt.Printf("Arch: %s\n", arch)
|
||||
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.Printf(".NET (CLR): %s\n", yesNo(r.DotNET))
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
libs, err := pf.ImportedLibs()
|
||||
if err != nil {
|
||||
fmt.Printf("\n(imported libs unavailable: %v)\n", err)
|
||||
return 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"
|
||||
}
|
||||
|
||||
func cmdCyclic(args []string) error {
|
||||
n := 4
|
||||
var find string
|
||||
var rest []string
|
||||
|
||||
for i := 0; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "-n":
|
||||
i++
|
||||
if i >= len(args) {
|
||||
return errors.New("-n requires a value")
|
||||
}
|
||||
v, err := strconv.Atoi(args[i])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid -n value: %w", err)
|
||||
}
|
||||
n = v
|
||||
case "-l":
|
||||
i++
|
||||
if i >= len(args) {
|
||||
return errors.New("-l requires a value")
|
||||
}
|
||||
find = args[i]
|
||||
default:
|
||||
rest = append(rest, args[i])
|
||||
}
|
||||
}
|
||||
|
||||
if find != "" {
|
||||
off := winpwn.CyclicFindN(parseSubseq(find, n), n)
|
||||
if off == -1 {
|
||||
return fmt.Errorf("subsequence %q not found in the n=%d cyclic pattern", find, n)
|
||||
}
|
||||
fmt.Println(off)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(rest) != 1 {
|
||||
return errors.New("usage: winpwn cyclic <length> [-n N] | winpwn cyclic -l <subseq> [-n N]")
|
||||
}
|
||||
length, err := strconv.Atoi(rest[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid length: %w", err)
|
||||
}
|
||||
fmt.Println(string(winpwn.CyclicN(length, n)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseSubseq accepts either a literal byte string ("aaab") or a hex-packed
|
||||
// integer ("0x62616161", as read back from a crashed register/return
|
||||
// address) and packs the latter little-endian at width n -- the CLI
|
||||
// equivalent of pwntools' cyclic_find() accepting either bytes or an int.
|
||||
func parseSubseq(s string, n int) []byte {
|
||||
if v, ok := strings.CutPrefix(s, "0x"); ok {
|
||||
if u, err := strconv.ParseUint(v, 16, 64); err == nil {
|
||||
if n == 8 {
|
||||
return winpwn.P64(u)
|
||||
}
|
||||
return winpwn.P32(uint32(u))
|
||||
}
|
||||
}
|
||||
return []byte(s)
|
||||
}
|
||||
|
||||
func cmdHex(args []string) error {
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(winpwn.Enhex(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdUnhex(args []string) error {
|
||||
data, err := io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := winpwn.Unhex(strings.TrimSpace(string(data)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = os.Stdout.Write(raw)
|
||||
return err
|
||||
}
|
||||
|
||||
func cmdHexdump(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: winpwn hexdump <file>")
|
||||
}
|
||||
data, err := os.ReadFile(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(winpwn.Hexdump(data))
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdRop(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("usage: winpwn rop <file> -search PATTERN | -regex PATTERN")
|
||||
}
|
||||
target := args[0]
|
||||
var search, pattern string
|
||||
for i := 1; i < len(args); i++ {
|
||||
switch args[i] {
|
||||
case "-search":
|
||||
i++
|
||||
if i >= len(args) {
|
||||
return errors.New("-search requires a value")
|
||||
}
|
||||
search = args[i]
|
||||
case "-regex":
|
||||
i++
|
||||
if i >= len(args) {
|
||||
return errors.New("-regex requires a value")
|
||||
}
|
||||
pattern = args[i]
|
||||
}
|
||||
}
|
||||
if search == "" && pattern == "" {
|
||||
return errors.New("specify -search or -regex (a full unfiltered gadget dump isn't supported from the CLI -- it can be tens of thousands of entries; use the library's NewROP+r.Search from a script instead)")
|
||||
}
|
||||
|
||||
rop, err := winpwn.NewROP(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rop.Close()
|
||||
|
||||
var gadgets []winpwn.Gadget
|
||||
if search != "" {
|
||||
gadgets, err = rop.Search(search)
|
||||
} else {
|
||||
gadgets, err = rop.SearchRegex(pattern)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, g := range gadgets {
|
||||
fmt.Printf("0x%016X: %s\n", g.Address, g.Instructions)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdBytes(args []string) error {
|
||||
if len(args) != 2 {
|
||||
return errors.New("usage: winpwn bytes <file> <hex> (e.g. winpwn bytes kernel32.dll ebfe for jmp $)")
|
||||
}
|
||||
pattern, err := winpwn.Unhex(normalizeHex(args[1]))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid hex pattern %q: %w", args[1], err)
|
||||
}
|
||||
|
||||
pf, err := winpwn.OpenPE(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pf.Close()
|
||||
|
||||
base, err := pf.ImageBase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rvas, err := pf.SearchBytes(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rva := range rvas {
|
||||
fmt.Printf("0x%016X (RVA 0x%X)\n", base+rva, rva)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeHex strips the separators tools commonly paste hex bytes with
|
||||
// ("\xeb\xfe", "EB FE", "eb-fe", "eb:fe") down to a bare hex.DecodeString-
|
||||
// compatible string, so winpwn bytes accepts whatever got copied out of
|
||||
// x64dbg/rp++/a disassembler without the user reformatting it by hand.
|
||||
func normalizeHex(s string) string {
|
||||
replacer := strings.NewReplacer("\\x", "", " ", "", "-", "", ":", "", ",", "")
|
||||
return replacer.Replace(s)
|
||||
}
|
||||
|
||||
func cmdDisasm(args []string) error {
|
||||
if len(args) != 3 {
|
||||
return errors.New("usage: winpwn disasm <file> <hexaddr> <count>")
|
||||
}
|
||||
target := args[0]
|
||||
addr, err := strconv.ParseUint(strings.TrimPrefix(args[1], "0x"), 16, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid address: %w", err)
|
||||
}
|
||||
count, err := strconv.Atoi(args[2])
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid count: %w", err)
|
||||
}
|
||||
|
||||
rop, err := winpwn.NewROP(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rop.Close()
|
||||
|
||||
lines, err := rop.Disassemble(addr, count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, l := range lines {
|
||||
fmt.Println(l)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdExports(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: winpwn exports <file>")
|
||||
}
|
||||
pf, err := winpwn.OpenPE(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pf.Close()
|
||||
|
||||
exports, err := pf.ListExports()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base, err := pf.ImageBase()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, e := range exports {
|
||||
name := e.Name
|
||||
if name == "" {
|
||||
name = "(no name)"
|
||||
}
|
||||
if e.ForwardTarget != "" {
|
||||
fmt.Printf("%-40s ordinal=%-5d -> %s\n", name, e.Ordinal, e.ForwardTarget)
|
||||
} else {
|
||||
fmt.Printf("%-40s ordinal=%-5d 0x%016X\n", name, e.Ordinal, base+uint64(e.RVA))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdImports(args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: winpwn imports <file>")
|
||||
}
|
||||
pf, err := winpwn.OpenPE(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer pf.Close()
|
||||
|
||||
imports, err := pf.ListImports()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, im := range imports {
|
||||
name := im.Name
|
||||
if name == "" {
|
||||
name = fmt.Sprintf("ordinal#%d", im.Ordinal)
|
||||
}
|
||||
fmt.Printf("%-20s %-40s IAT=0x%08X\n", im.DLL, name, im.IATRVA)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdHeap(args []string) error {
|
||||
if len(args) < 1 {
|
||||
return errors.New("usage: winpwn heap <pid> [-walk]")
|
||||
}
|
||||
pidU, err := strconv.ParseUint(args[0], 10, 32)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pid %q: %w", args[0], err)
|
||||
}
|
||||
pid := uint32(pidU)
|
||||
|
||||
walkEntries := len(args) >= 2 && args[1] == "-walk"
|
||||
|
||||
heaps, err := winpwn.ListProcessHeaps(pid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ListProcessHeaps: %w", err)
|
||||
}
|
||||
fmt.Printf("pid %d: %d heap(s)\n", pid, len(heaps))
|
||||
|
||||
mem, err := winpwn.OpenProcessMemory(pid, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenProcessMemory: %w", err)
|
||||
}
|
||||
defer mem.Close()
|
||||
|
||||
for i, haddr := range heaps {
|
||||
kind, err := winpwn.DetectHeapKind(mem, haddr)
|
||||
if err != nil {
|
||||
fmt.Printf("\n[%d] 0x%016x error: %v\n", i, haddr, err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("\n[%d] 0x%016x %s\n", i, haddr, kind)
|
||||
|
||||
switch kind {
|
||||
case winpwn.HeapKindNT:
|
||||
printNTHeap(mem, haddr, walkEntries)
|
||||
case winpwn.HeapKindSegment:
|
||||
printSegmentHeap(mem, haddr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNTHeap(r *winpwn.ProcessMemory, addr uint64, walkEntries bool) {
|
||||
h, err := winpwn.ReadHeap(r, addr)
|
||||
if err != nil {
|
||||
fmt.Printf(" ReadHeap error: %v\n", err)
|
||||
return
|
||||
}
|
||||
feType := "none"
|
||||
if h.FrontEndHeapType == winpwn.FrontEndHeapLFH {
|
||||
feType = fmt.Sprintf("LFH @ 0x%x", h.FrontEndHeap)
|
||||
} else if h.FrontEndHeapType == winpwn.FrontEndHeapLookaside {
|
||||
feType = "Lookaside"
|
||||
}
|
||||
fmt.Printf(" flags=0x%08x encoding=%v front-end=%s\n",
|
||||
h.Flags, h.EncodingActive(), feType)
|
||||
|
||||
segs, _ := h.Segments(r)
|
||||
fmt.Printf(" segments: %d\n", len(segs)+1) // +1 for segment0 always present
|
||||
|
||||
if !walkEntries {
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := h.WalkAllHeapEntries(r)
|
||||
if err != nil {
|
||||
fmt.Printf(" WalkAllHeapEntries: %v\n", err)
|
||||
}
|
||||
stats := winpwn.SummariseEntries(entries)
|
||||
fmt.Printf(" entries: total=%d busy=%d free=%d busy_bytes=%d free_bytes=%d\n",
|
||||
stats.TotalEntries, stats.BusyEntries, stats.FreeEntries,
|
||||
stats.BusyBytes, stats.FreeBytes)
|
||||
|
||||
pairs := winpwn.AdjacentBusyPairs(entries)
|
||||
if len(pairs) > 0 {
|
||||
fmt.Printf(" adjacent busy pairs: %d\n", len(pairs))
|
||||
shown := pairs
|
||||
if len(shown) > 5 {
|
||||
shown = shown[:5]
|
||||
}
|
||||
for _, p := range shown {
|
||||
fmt.Printf(" 0x%x (size %d) <-> 0x%x (size %d)\n",
|
||||
p[0].UserData(), p[0].UserSize(),
|
||||
p[1].UserData(), p[1].UserSize())
|
||||
}
|
||||
}
|
||||
|
||||
if h.FrontEndHeapType == winpwn.FrontEndHeapLFH {
|
||||
buckets, err := winpwn.ReadLFHBuckets(r, h.FrontEndHeap)
|
||||
if err != nil {
|
||||
fmt.Printf(" ReadLFHBuckets: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" LFH active buckets:")
|
||||
n := 0
|
||||
for _, b := range buckets {
|
||||
if b.BlockUnits == 0 {
|
||||
continue
|
||||
}
|
||||
fmt.Printf(" [%d]=%db", b.Index, b.BlockSize())
|
||||
n++
|
||||
if n >= 8 {
|
||||
fmt.Printf(" ...")
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func printSegmentHeap(r *winpwn.ProcessMemory, addr uint64) {
|
||||
h, err := winpwn.ReadSegmentHeap(r, addr)
|
||||
if err != nil {
|
||||
fmt.Printf(" ReadSegmentHeap error: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf(" GlobalFlags=0x%08x\n", h.GlobalFlags)
|
||||
fmt.Printf(" VS context @ 0x%x: committed=%d free=%d subsegments=%d\n",
|
||||
h.VS.Addr, h.VS.CommittedUnits, h.VS.FreeUnits, h.VS.SubsegmentCount)
|
||||
if len(h.LFH.ActiveBuckets) > 0 {
|
||||
fmt.Printf(" LFH active buckets (total-blocks):")
|
||||
for j, b := range h.LFH.ActiveBuckets {
|
||||
fmt.Printf(" [%d]=%d", b.Index, b.TotalBlockCount)
|
||||
if j >= 7 {
|
||||
fmt.Printf(" ...")
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Println()
|
||||
} else {
|
||||
fmt.Printf(" LFH: no active buckets\n")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user