227 lines
6.7 KiB
Go
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
|
|
}
|