v0.1 - initial commit
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user