Files
go_pwner/symbols_windows.go
T
2026-07-18 21:37:15 +03:00

227 lines
6.7 KiB
Go

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