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

223 lines
6.9 KiB
Go

package winpwn
import (
"bytes"
"debug/pe"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
)
// PEFile is a read-only-by-default handle on a PE32/PE32+ image, the winpwn
// analogue of pwntools' ELF. Unlike ELF, a PE's bytes can come from two
// meaningfully different places: a file on disk (OpenPE) or a loaded
// module's live address space inside a running process (OpenPEFromProcess).
// Every accessor in this package (checksec, IAT/EAT, gadget scanning,
// patching) is written against the r/w fields below so both backings get
// every feature for free.
type PEFile struct {
File *pe.File
r io.ReaderAt
w io.WriterAt // nil when opened read-only
closer io.Closer // nil if there is nothing to close
size int64 // total backing size, for whole-image operations (checksum recompute)
// live is true when r/w address a process's memory (OpenPEFromProcess)
// rather than a file's bytes (OpenPE/OpenPEForWrite). See
// RVAToFileOffset for why this changes the RVA translation.
live bool
}
func OpenPE(path string) (*PEFile, error) {
fd, err := os.Open(path)
if err != nil {
return nil, err
}
f, err := pe.NewFile(fd)
if err != nil {
fd.Close()
return nil, err
}
info, err := fd.Stat()
if err != nil {
fd.Close()
return nil, err
}
return &PEFile{File: f, r: fd, closer: fd, size: info.Size()}, nil
}
func (p *PEFile) Close() {
if p.closer != nil {
p.closer.Close()
}
}
// IsLive reports whether this PEFile is backed by a running process's
// address space (OpenPEFromProcess) rather than a file on disk.
func (p *PEFile) IsLive() bool {
return p.live
}
// peHeader normalizes the PE32 (32-bit) vs PE32+ (64-bit) optional header
// split into the fields callers actually need, so the rest of the package
// doesn't have to type-switch on pe.OptionalHeader32/64 everywhere.
type peHeader struct {
is64 bool
imageBase uint64
addressOfEntryPoint uint32
sizeOfImage uint32
dllCharacteristics uint16
dataDirectory [16]pe.DataDirectory
}
func (p *PEFile) header() (peHeader, error) {
switch oh := p.File.OptionalHeader.(type) {
case *pe.OptionalHeader64:
return peHeader{
is64: true,
imageBase: oh.ImageBase,
addressOfEntryPoint: oh.AddressOfEntryPoint,
sizeOfImage: oh.SizeOfImage,
dllCharacteristics: oh.DllCharacteristics,
dataDirectory: oh.DataDirectory,
}, nil
case *pe.OptionalHeader32:
return peHeader{
is64: false,
imageBase: uint64(oh.ImageBase),
addressOfEntryPoint: oh.AddressOfEntryPoint,
sizeOfImage: oh.SizeOfImage,
dllCharacteristics: oh.DllCharacteristics,
dataDirectory: oh.DataDirectory,
}, nil
default:
return peHeader{}, errors.New("unrecognized PE optional header (not PE32 or PE32+)")
}
}
// Is64Bit reports whether this is a PE32+ (x64) image.
func (p *PEFile) Is64Bit() (bool, error) {
h, err := p.header()
if err != nil {
return false, err
}
return h.is64, nil
}
// ImageBase returns the preferred load address from the PE optional header
// (the Go analogue of pwntools' ELF.address when ASLR is disabled). For a
// live-process-backed PEFile this is still the header's *preferred* base,
// not necessarily where the module actually landed -- use the base passed
// to OpenPEFromProcess (e.g. from ResolveModuleBase) for the real address.
func (p *PEFile) ImageBase() (uint64, error) {
h, err := p.header()
if err != nil {
return 0, err
}
return h.imageBase, nil
}
// EntryPoint returns the absolute address of the entry point
// (ImageBase + AddressOfEntryPoint).
func (p *PEFile) EntryPoint() (uint64, error) {
h, err := p.header()
if err != nil {
return 0, err
}
return h.imageBase + uint64(h.addressOfEntryPoint), nil
}
// RVAToFileOffset converts a relative virtual address into the offset to
// pass to this PEFile's backing ReaderAt/WriterAt.
//
// For a disk-backed PEFile (OpenPE/OpenPEForWrite) this walks the section
// table to translate an RVA into a PointerToRawData-relative file offset --
// necessary because SectionAlignment and FileAlignment differ, so a
// section's position in the loaded image and its position on disk aren't
// the same number.
//
// For a live-process-backed PEFile (OpenPEFromProcess) the backing
// ReaderAt/WriterAt already treats offset 0 as the module's base address,
// so "offset" already *is* the RVA: that's the entire definition of a
// relative virtual address once the image is actually loaded. This is the
// identity function in that case.
func (p *PEFile) RVAToFileOffset(rva uint32) int64 {
if p.live {
return int64(rva)
}
for _, sec := range p.File.Sections {
if rva >= sec.VirtualAddress && rva < sec.VirtualAddress+sec.VirtualSize {
return int64(rva - sec.VirtualAddress + sec.Offset)
}
}
return 0
}
// readCString reads a null-terminated ASCII string starting at the given
// read offset (file offset, or RVA for a live-backed PEFile -- see
// RVAToFileOffset).
func (p *PEFile) readCString(offset int64) (string, error) {
var out []byte
buf := make([]byte, 1)
for {
if _, err := p.r.ReadAt(buf, offset); err != nil {
if err == io.EOF {
break
}
return "", err
}
if buf[0] == 0 {
break
}
out = append(out, buf[0])
offset++
}
return string(out), nil
}
// readStructAt fills v (a pointer to a fixed-size struct of fixed-width
// fields) by reading binary.Size(v) bytes at offset through this PEFile's
// backing ReaderAt. The one read-offset helper every struct-shaped PE
// directory parse in this package goes through, so disk and live-process
// backings share the exact same parsing code.
func (p *PEFile) readStructAt(offset int64, v any) error {
size := binary.Size(v)
if size < 0 {
return errors.New("readStructAt: unsupported type")
}
buf := make([]byte, size)
if _, err := p.r.ReadAt(buf, offset); err != nil {
return err
}
return binary.Read(bytes.NewReader(buf), binary.LittleEndian, v)
}
// writeAt writes data at offset through this PEFile's backing WriterAt,
// failing clearly if the PEFile was opened read-only.
func (p *PEFile) writeAt(offset int64, data []byte) error {
if p.w == nil {
return errors.New("PEFile is read-only; open with OpenPEForWrite or OpenPEFromProcess(write) for write access")
}
_, err := p.w.WriteAt(data, offset)
return err
}
// GetProcAddress looks up a function's RVA by name, the bare-bones analogue
// of the real WinAPI call of the same name. It's a thin wrapper over the
// canonical export-table walk in GetExport (exports.go); use GetExport
// directly when you need forwarder resolution or ordinal information too.
func (p *PEFile) GetProcAddress(funcName string) (uint64, error) {
exp, err := p.GetExport(funcName)
if err != nil {
return 0, err
}
if exp.RVA == 0 {
return 0, fmt.Errorf("export %q is a forwarder (%s.%s), not a local RVA -- resolve it in the target DLL instead",
funcName, exp.ForwardTarget, funcName)
}
return uint64(exp.RVA), nil
}