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