//go:build windows package winpwn import ( "fmt" "strings" "sync" "unsafe" "golang.org/x/sys/windows" ) // ListLoadedModules walks pid's PEB → Ldr → InMemoryOrderModuleList and // returns the load address of every currently-loaded module, keyed by // lower-cased base name ("kernel32.dll", "ntdll.dll", etc.). // // This is the single-call equivalent of calling ResolveModuleBase for every // DLL in the process — use it when you need more than one or two bases, or // when you want to enumerate what's loaded without knowing names in advance. func ListLoadedModules(pid uint32) (map[string]uintptr, error) { h, err := windows.OpenProcess( windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, pid) if err != nil { return nil, err } defer windows.CloseHandle(h) return ldrWalkAll(h, pid) } // ldrWalkAll performs the PEB→Ldr→InMemoryOrderModuleList walk and collects // every entry, keyed by lower-cased base name. Shared by ListLoadedModules // and ProcessSymbols.loadAll. func ldrWalkAll(h windows.Handle, pid uint32) (map[string]uintptr, error) { var pbi windows.PROCESS_BASIC_INFORMATION var retLen uint32 if err := windows.NtQueryInformationProcess(h, windows.ProcessBasicInformation, unsafe.Pointer(&pbi), uint32(unsafe.Sizeof(pbi)), &retLen); err != nil { return nil, fmt.Errorf("NtQueryInformationProcess: %w", err) } pebAddr := uintptr(unsafe.Pointer(pbi.PebBaseAddress)) if pebAddr == 0 { return nil, fmt.Errorf("PEB is null for pid %d (not yet initialized?)", pid) } peb, err := readRemoteStruct[windows.PEB](h, pebAddr) if err != nil { return nil, fmt.Errorf("read PEB: %w", err) } ldrAddr := uintptr(unsafe.Pointer(peb.Ldr)) if ldrAddr == 0 { return nil, fmt.Errorf("PEB.Ldr is null (loader not yet run in pid %d)", pid) } ldr, err := readRemoteStruct[windows.PEB_LDR_DATA](h, ldrAddr) if err != nil { return nil, fmt.Errorf("read PEB_LDR_DATA: %w", err) } entryLinksOffset := unsafe.Offsetof(windows.LDR_DATA_TABLE_ENTRY{}.InMemoryOrderLinks) headAddr := ldrAddr + unsafe.Offsetof(windows.PEB_LDR_DATA{}.InMemoryOrderModuleList) out := make(map[string]uintptr) cur := uintptr(unsafe.Pointer(ldr.InMemoryOrderModuleList.Flink)) for cur != 0 && cur != headAddr { entryAddr := cur - entryLinksOffset entry, err := readRemoteStruct[windows.LDR_DATA_TABLE_ENTRY](h, entryAddr) if err != nil { return nil, fmt.Errorf("read LDR_DATA_TABLE_ENTRY: %w", err) } if name, err := readRemoteUTF16(h, entry.FullDllName); err == nil { key := strings.ToLower(moduleBaseName(name)) if key != "" { out[key] = entry.DllBase } } cur = uintptr(unsafe.Pointer(entry.InMemoryOrderLinks.Flink)) } return out, nil } // SymbolVA resolves the virtual address of a named export from a module // currently loaded in pid. It combines ResolveModuleBase + OpenPEFromProcess // + GetProcAddress into a single call — the Go equivalent of pwintools' // p.symbols["kernel32.dll"]["WinExec"]. func SymbolVA(pid uint32, dll, name string) (uintptr, error) { base, err := ResolveModuleBase(pid, dll) if err != nil { return 0, err } pf, err := OpenPEFromProcess(pid, base) if err != nil { return 0, fmt.Errorf("open %s in pid %d: %w", dll, pid, err) } defer pf.Close() rva, err := pf.GetProcAddress(name) if err != nil { return 0, fmt.Errorf("%s!%s: %w", dll, name, err) } return base + uintptr(rva), nil } // ProcessSymbols resolves and caches loaded-module bases and exported symbol // VAs for a running process — the Go equivalent of pwintools' p.libs / // p.symbols. Caches one PEFile per DLL so repeated symbol lookups in the // same module are cheap. // // Usage: // // tube, _ := winpwn.Spawn("chal.exe") // sym := winpwn.NewProcessSymbols(tube.PID()) // defer sym.Close() // // k32, _ := sym.Base("kernel32.dll") // winexec, _ := sym.Symbol("kernel32.dll", "WinExec") // mods, _ := sym.Modules() // all loaded DLLs type ProcessSymbols struct { pid uint32 mu sync.Mutex cache map[string]*symModule // keyed by lower-cased base name } type symModule struct { base uintptr pf *PEFile } // NewProcessSymbols creates a ProcessSymbols for the given PID. No I/O // happens until the first Base/Symbol call. func NewProcessSymbols(pid uint32) *ProcessSymbols { return &ProcessSymbols{pid: pid, cache: make(map[string]*symModule)} } // Base returns the load address of the named module (e.g. "kernel32.dll"), // the Go equivalent of pwintools' p.libs["kernel32.dll"]. func (ps *ProcessSymbols) Base(dll string) (uint64, error) { m, err := ps.loadModule(dll) if err != nil { return 0, err } return uint64(m.base), nil } // Symbol returns the virtual address of name exported from dll // (e.g. "kernel32.dll", "WinExec"), the equivalent of pwintools' // p.symbols["kernel32.dll"]["WinExec"]. func (ps *ProcessSymbols) Symbol(dll, name string) (uint64, error) { m, err := ps.loadModule(dll) if err != nil { return 0, err } rva, err := m.pf.GetProcAddress(name) if err != nil { return 0, fmt.Errorf("%s!%s: %w", dll, name, err) } return uint64(m.base) + uint64(rva), nil } // Modules returns a snapshot of every module currently loaded in the process, // keyed by lower-cased base name — the equivalent of pwintools' p.libs dict. func (ps *ProcessSymbols) Modules() (map[string]uint64, error) { raw, err := ListLoadedModules(ps.pid) if err != nil { return nil, err } out := make(map[string]uint64, len(raw)) for k, v := range raw { out[k] = uint64(v) } return out, nil } // AllSymbols returns every named export from dll as a map of name → VA. // Useful for quick "what's available" exploration without knowing exact names. func (ps *ProcessSymbols) AllSymbols(dll string) (map[string]uint64, error) { m, err := ps.loadModule(dll) if err != nil { return nil, err } exports, err := m.pf.ListExports() if err != nil { return nil, err } out := make(map[string]uint64, len(exports)) for _, e := range exports { if e.Name != "" && e.RVA != 0 { out[e.Name] = uint64(m.base) + uint64(e.RVA) } } return out, nil } // Close releases all cached PEFile handles. Safe to call more than once. func (ps *ProcessSymbols) Close() { ps.mu.Lock() defer ps.mu.Unlock() for _, m := range ps.cache { if m.pf != nil { m.pf.Close() } } ps.cache = nil } func (ps *ProcessSymbols) loadModule(dll string) (*symModule, error) { key := strings.ToLower(moduleBaseName(dll)) ps.mu.Lock() defer ps.mu.Unlock() if ps.cache == nil { return nil, fmt.Errorf("ProcessSymbols already closed") } if m, ok := ps.cache[key]; ok { return m, nil } base, err := ResolveModuleBase(ps.pid, dll) if err != nil { return nil, err } pf, err := OpenPEFromProcess(ps.pid, base) if err != nil { return nil, fmt.Errorf("open %s in pid %d: %w", dll, ps.pid, err) } m := &symModule{base: base, pf: pf} ps.cache[key] = m return m, nil }