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