223 lines
6.9 KiB
Go
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
|
|
}
|