313 lines
11 KiB
Go
313 lines
11 KiB
Go
package winpwn
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
// minidumpSignature is MINIDUMP_HEADER.Signature ('MDMP' read as a
|
|
// little-endian ULONG32), the magic number every .dmp file starts with.
|
|
const minidumpSignature = 0x504D444D
|
|
|
|
// MinidumpStreamType mirrors winnt.h's MINIDUMP_STREAM_TYPE. Only the
|
|
// values this package decodes natively are named here; RawStream accepts
|
|
// any numeric stream type for everything else (SystemInfoStream,
|
|
// ThreadListStream, Memory64ListStream, ...), the same way dbghelp's
|
|
// MiniDumpReadDumpStream takes an arbitrary stream number.
|
|
type MinidumpStreamType uint32
|
|
|
|
const (
|
|
StreamThreadList MinidumpStreamType = 3
|
|
StreamModuleList MinidumpStreamType = 4
|
|
StreamMemoryList MinidumpStreamType = 5
|
|
StreamException MinidumpStreamType = 6
|
|
StreamSystemInfo MinidumpStreamType = 7
|
|
StreamMemory64List MinidumpStreamType = 9
|
|
)
|
|
|
|
// minidumpHeader mirrors MINIDUMP_HEADER (winnt.h), 32 bytes, no padding:
|
|
// every field here is naturally aligned at its own offset already.
|
|
type minidumpHeader struct {
|
|
Signature uint32
|
|
Version uint32
|
|
NumberOfStreams uint32
|
|
StreamDirectoryRva uint32
|
|
CheckSum uint32
|
|
TimeDateStamp uint32
|
|
Flags uint64
|
|
}
|
|
|
|
// minidumpLocationDescriptor mirrors MINIDUMP_LOCATION_DESCRIPTOR: despite
|
|
// the name this Rva is a plain file offset, not an RVA relative to a
|
|
// loaded image -- a minidump is never "loaded", it's just read.
|
|
type minidumpLocationDescriptor struct {
|
|
DataSize uint32
|
|
Rva uint32
|
|
}
|
|
|
|
// minidumpDirectory mirrors MINIDUMP_DIRECTORY, 12 bytes.
|
|
type minidumpDirectory struct {
|
|
StreamType uint32
|
|
Location minidumpLocationDescriptor
|
|
}
|
|
|
|
// Minidump is a read-only handle on a Windows .dmp file, parsed directly
|
|
// from the public MINIDUMP_* structures (winnt.h) instead of calling
|
|
// dbghelp.dll's MiniDumpReadDumpStream. Same spirit as the rest of this
|
|
// package's PE/ROP parsing (see pe.go, gadgets.go, and the "reimplemented
|
|
// directly from the spec instead" note on checksec in the README): the
|
|
// format is just bytes with a documented, stable layout, and parsing it
|
|
// directly means this works without GOOS=windows or dbghelp.dll present,
|
|
// and is unit-testable against a synthetic in-memory buffer instead of
|
|
// needing a real crash dump on disk.
|
|
type Minidump struct {
|
|
r io.ReaderAt
|
|
closer io.Closer
|
|
header minidumpHeader
|
|
streams []minidumpDirectory
|
|
}
|
|
|
|
// OpenMinidump opens and parses a .dmp file's header and stream directory.
|
|
func OpenMinidump(path string) (*Minidump, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m, err := newMinidump(f)
|
|
if err != nil {
|
|
f.Close()
|
|
return nil, err
|
|
}
|
|
m.closer = f
|
|
return m, nil
|
|
}
|
|
|
|
// newMinidump parses from any io.ReaderAt (a file, or an in-memory
|
|
// bytes.Reader for tests/already-loaded buffers) -- OpenMinidump is just
|
|
// this plus a file open/close.
|
|
func newMinidump(r io.ReaderAt) (*Minidump, error) {
|
|
m := &Minidump{r: r}
|
|
|
|
if err := readStructAt(r, 0, &m.header); err != nil {
|
|
return nil, fmt.Errorf("read MINIDUMP_HEADER: %w", err)
|
|
}
|
|
if m.header.Signature != minidumpSignature {
|
|
return nil, fmt.Errorf("not a minidump file (signature 0x%X, want 0x%X)", m.header.Signature, minidumpSignature)
|
|
}
|
|
|
|
m.streams = make([]minidumpDirectory, m.header.NumberOfStreams)
|
|
for i := range m.streams {
|
|
const sizeofDirectory = 12
|
|
off := int64(m.header.StreamDirectoryRva) + int64(i)*sizeofDirectory
|
|
if err := readStructAt(r, off, &m.streams[i]); err != nil {
|
|
return nil, fmt.Errorf("read MINIDUMP_DIRECTORY[%d]: %w", i, err)
|
|
}
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func (m *Minidump) Close() error {
|
|
if m.closer != nil {
|
|
return m.closer.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Minidump) findStream(t MinidumpStreamType) (minidumpDirectory, bool) {
|
|
for _, d := range m.streams {
|
|
if d.StreamType == uint32(t) {
|
|
return d, true
|
|
}
|
|
}
|
|
return minidumpDirectory{}, false
|
|
}
|
|
|
|
// RawStream returns the raw bytes of the first stream of type t -- the
|
|
// direct analogue of MiniDumpReadDumpStream for any stream this package
|
|
// doesn't decode natively (SystemInfoStream, ThreadListStream,
|
|
// Memory64ListStream, ...). The caller is responsible for knowing that
|
|
// stream's layout.
|
|
func (m *Minidump) RawStream(t MinidumpStreamType) ([]byte, error) {
|
|
dir, ok := m.findStream(t)
|
|
if !ok {
|
|
return nil, fmt.Errorf("stream type %d not present in this minidump", t)
|
|
}
|
|
buf := make([]byte, dir.Location.DataSize)
|
|
if _, err := m.r.ReadAt(buf, int64(dir.Location.Rva)); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
// MinidumpModule is one entry of MINIDUMP_MODULE_LIST: a loaded module's
|
|
// name and the base address it was loaded at -- exactly what you need to
|
|
// rebase a crash address back into the binary you can actually open in a
|
|
// disassembler.
|
|
type MinidumpModule struct {
|
|
Name string
|
|
BaseOfImage uint64
|
|
SizeOfImage uint32
|
|
TimeDateStamp uint32
|
|
}
|
|
|
|
// sizeofMinidumpModule is sizeof(MINIDUMP_MODULE): BaseOfImage(8) +
|
|
// SizeOfImage(4) + CheckSum(4) + TimeDateStamp(4) + ModuleNameRva(4) +
|
|
// VS_FIXEDFILEINFO(52) + CvRecord(8) + MiscRecord(8) + Reserved0(8) +
|
|
// Reserved1(8) = 108. Decoded by fixed offset below rather than a matching
|
|
// Go struct, since only a handful of its fields are useful here and
|
|
// VS_FIXEDFILEINFO's 13 DWORDs aren't worth modeling just to skip over.
|
|
const sizeofMinidumpModule = 108
|
|
|
|
// Modules walks MINIDUMP_MODULE_LIST and resolves each module's name
|
|
// string, the analogue of pwntools' Corefile module list but for a Windows
|
|
// crash dump.
|
|
func (m *Minidump) Modules() ([]MinidumpModule, error) {
|
|
dir, ok := m.findStream(StreamModuleList)
|
|
if !ok {
|
|
return nil, errors.New("ModuleListStream not present in this minidump")
|
|
}
|
|
|
|
var count uint32
|
|
if err := readUint32At(m.r, int64(dir.Location.Rva), &count); err != nil {
|
|
return nil, fmt.Errorf("read MINIDUMP_MODULE_LIST.NumberOfModules: %w", err)
|
|
}
|
|
|
|
base := int64(dir.Location.Rva) + 4
|
|
out := make([]MinidumpModule, 0, count)
|
|
for i := uint32(0); i < count; i++ {
|
|
buf := make([]byte, sizeofMinidumpModule)
|
|
if _, err := m.r.ReadAt(buf, base+int64(i)*sizeofMinidumpModule); err != nil {
|
|
return nil, fmt.Errorf("read MINIDUMP_MODULE[%d]: %w", i, err)
|
|
}
|
|
|
|
nameRva := binary.LittleEndian.Uint32(buf[20:24])
|
|
name, err := m.readMinidumpString(nameRva)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read module name for MINIDUMP_MODULE[%d]: %w", i, err)
|
|
}
|
|
|
|
out = append(out, MinidumpModule{
|
|
Name: name,
|
|
BaseOfImage: binary.LittleEndian.Uint64(buf[0:8]),
|
|
SizeOfImage: binary.LittleEndian.Uint32(buf[8:12]),
|
|
TimeDateStamp: binary.LittleEndian.Uint32(buf[16:20]),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// readMinidumpString reads a MINIDUMP_STRING at the given offset: a
|
|
// ULONG32 byte length (excluding the length field and the terminator)
|
|
// followed by a UTF-16LE buffer.
|
|
func (m *Minidump) readMinidumpString(offset uint32) (string, error) {
|
|
var length uint32
|
|
if err := readUint32At(m.r, int64(offset), &length); err != nil {
|
|
return "", err
|
|
}
|
|
buf := make([]byte, length)
|
|
if _, err := m.r.ReadAt(buf, int64(offset)+4); err != nil {
|
|
return "", err
|
|
}
|
|
units := make([]uint16, length/2)
|
|
for i := range units {
|
|
units[i] = binary.LittleEndian.Uint16(buf[i*2:])
|
|
}
|
|
return string(utf16.Decode(units)), nil
|
|
}
|
|
|
|
// exceptionMaxParameters is EXCEPTION_MAXIMUM_PARAMETERS (winnt.h): the
|
|
// fixed size of MINIDUMP_EXCEPTION.ExceptionInformation.
|
|
const exceptionMaxParameters = 15
|
|
|
|
// MinidumpException is MINIDUMP_EXCEPTION_STREAM flattened to the fields a
|
|
// crash-triage script actually wants: which thread, what kind of fault
|
|
// (ExceptionCode -- e.g. 0xC0000005 for an access violation, the same
|
|
// value Tube.Interactive already reports for a locally observed crash),
|
|
// and where.
|
|
type MinidumpException struct {
|
|
ThreadID uint32
|
|
ExceptionCode uint32
|
|
ExceptionFlags uint32
|
|
ExceptionAddress uint64
|
|
// Parameters holds the first NumberParameters entries of
|
|
// ExceptionInformation -- e.g. for an access violation, Parameters[0]
|
|
// is the access type (read/write/execute) and Parameters[1] is the
|
|
// faulting address.
|
|
Parameters []uint64
|
|
}
|
|
|
|
// sizeofMinidumpExceptionStream is sizeof(MINIDUMP_EXCEPTION_STREAM):
|
|
// ThreadId(4) + alignment(4) + MINIDUMP_EXCEPTION(152) +
|
|
// ThreadContext location descriptor(8) = 168.
|
|
const sizeofMinidumpExceptionStream = 168
|
|
|
|
// Exception decodes MINIDUMP_EXCEPTION_STREAM, if present (a minidump
|
|
// taken from a still-running, non-crashed process has no exception
|
|
// stream). The register context blob referenced by
|
|
// MINIDUMP_EXCEPTION_STREAM.ThreadContext is not decoded here -- CONTEXT's
|
|
// layout differs by architecture and has internal padding/XSAVE-area
|
|
// subtleties not worth getting wrong; use RawStream(StreamException) and
|
|
// slice past sizeofMinidumpExceptionStream's ThreadContext location if you
|
|
// need the raw register bytes for a specific architecture.
|
|
func (m *Minidump) Exception() (*MinidumpException, error) {
|
|
dir, ok := m.findStream(StreamException)
|
|
if !ok {
|
|
return nil, errors.New("ExceptionStream not present in this minidump (the process may not have crashed)")
|
|
}
|
|
|
|
buf := make([]byte, sizeofMinidumpExceptionStream)
|
|
if _, err := m.r.ReadAt(buf, int64(dir.Location.Rva)); err != nil {
|
|
return nil, fmt.Errorf("read MINIDUMP_EXCEPTION_STREAM: %w", err)
|
|
}
|
|
|
|
threadID := binary.LittleEndian.Uint32(buf[0:4])
|
|
// MINIDUMP_EXCEPTION starts right after ThreadId + a 4-byte alignment pad.
|
|
exc := buf[8:]
|
|
numParams := binary.LittleEndian.Uint32(exc[24:28])
|
|
if numParams > exceptionMaxParameters {
|
|
numParams = exceptionMaxParameters
|
|
}
|
|
params := make([]uint64, numParams)
|
|
for i := range params {
|
|
params[i] = binary.LittleEndian.Uint64(exc[32+i*8:])
|
|
}
|
|
|
|
return &MinidumpException{
|
|
ThreadID: threadID,
|
|
ExceptionCode: binary.LittleEndian.Uint32(exc[0:4]),
|
|
ExceptionFlags: binary.LittleEndian.Uint32(exc[4:8]),
|
|
ExceptionAddress: binary.LittleEndian.Uint64(exc[16:24]),
|
|
Parameters: params,
|
|
}, nil
|
|
}
|
|
|
|
// readStructAt fills v (a pointer to a fixed-size struct of fixed-width
|
|
// fields) by reading binary.Size(v) bytes at offset -- the minidump.go
|
|
// analogue of PEFile.readStructAt in pe.go, kept separate since Minidump
|
|
// isn't a PEFile and has no reason to share its receiver.
|
|
func readStructAt(r io.ReaderAt, offset int64, v any) error {
|
|
size := binary.Size(v)
|
|
if size < 0 {
|
|
return errors.New("readStructAt: unsupported type")
|
|
}
|
|
buf := make([]byte, size)
|
|
if _, err := r.ReadAt(buf, offset); err != nil {
|
|
return err
|
|
}
|
|
return binary.Read(bytes.NewReader(buf), binary.LittleEndian, v)
|
|
}
|
|
|
|
func readUint32At(r io.ReaderAt, offset int64, out *uint32) error {
|
|
var buf [4]byte
|
|
if _, err := r.ReadAt(buf[:], offset); err != nil {
|
|
return err
|
|
}
|
|
*out = binary.LittleEndian.Uint32(buf[:])
|
|
return nil
|
|
}
|