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 }