Files
nexus/internal/leviathan/parsers/formats.go
T
2026-06-16 11:51:07 +01:00

2183 lines
65 KiB
Go

// Package parsers provides lightweight structure parsers for common
// binary formats used in Leviathan's structures / templates panel.
// Each parser returns a tree of Field values anchored at absolute
// offsets into the document.
package parsers
import (
"encoding/binary"
"errors"
"fmt"
"strings"
"unicode/utf16"
"unicode/utf8"
"github.com/andydixon/nexus/internal/leviathan/document"
)
// Field is one node of the parsed structure.
type Field struct {
Name string
Type string
Offset int64
Length int64
Value string
Note string
Children []*Field
}
// ToMap serialises a Field tree to JS-friendly maps.
func (f *Field) ToMap() map[string]any {
m := map[string]any{
"name": f.Name,
"type": f.Type,
"offset": f.Offset,
"length": f.Length,
"value": f.Value,
}
if f.Note != "" {
m["note"] = f.Note
}
if len(f.Children) > 0 {
kids := make([]map[string]any, len(f.Children))
for i, c := range f.Children {
kids[i] = c.ToMap()
}
m["children"] = kids
}
return m
}
// DetectAndParse picks the first matching parser for the document.
// It returns the parser name used and the tree.
func DetectAndParse(d *document.Document) (string, *Field, error) {
// Read enough to cover the tar ustar magic at offset 257.
head, err := d.Range(0, min64(265, d.Size()))
if err != nil {
return "", nil, err
}
switch {
case len(head) >= 8 && string(head[:8]) == "\x89PNG\r\n\x1a\n":
f, err := ParsePNG(d)
return "png", f, err
case len(head) >= 3 && head[0] == 0xff && head[1] == 0xd8 && head[2] == 0xff:
f, err := ParseJPEG(d)
return "jpeg", f, err
case len(head) >= 6 && (string(head[:6]) == "GIF87a" || string(head[:6]) == "GIF89a"):
f, err := ParseGIF(d)
return "gif", f, err
case len(head) >= 2 && string(head[:2]) == "BM":
f, err := ParseBMP(d)
return "bmp", f, err
case len(head) >= 4 && string(head[:4]) == "PK\x03\x04":
f, err := ParseZIP(d)
return "zip", f, err
case len(head) >= 4 && string(head[:4]) == "\x7fELF":
f, err := ParseELF(d)
return "elf", f, err
case len(head) >= 2 && string(head[:2]) == "MZ":
f, err := ParsePE(d)
return "pe", f, err
case len(head) >= 4 && string(head[:4]) == "RIFF":
f, err := ParseRIFF(d)
return "riff", f, err
case len(head) >= 16 && string(head[:16]) == "SQLite format 3\x00":
f, err := ParseSQLite(d)
return "sqlite", f, err
case len(head) >= 8 && string(head[:8]) == string(cfbMagic):
f, err := ParseCFB(d)
return "cfb", f, err
case len(head) >= 3 && head[0] == 0x1f && head[1] == 0x8b && head[2] == 8:
f, err := ParseGZIP(d)
return "gzip", f, err
case len(head) >= 4 && string(head[:4]) == "\x0a\x0d\x0d\x0a":
f, err := ParsePCAPNG(d)
return "pcapng", f, err
case len(head) >= 4 && isPCAPMagic(head):
f, err := ParsePCAP(d)
return "pcap", f, err
case len(head) >= 4 && (string(head[:4]) == "II\x2a\x00" || string(head[:4]) == "MM\x00\x2a"):
f, err := ParseTIFF(d)
return "tiff", f, err
case len(head) >= 265 && (string(head[257:262]) == "ustar"):
f, err := ParseTAR(d)
return "tar", f, err
}
// Mach-O (including fat/universal) — detection needs more than 8 bytes.
if IsMachO(head, d.Size()) {
f, err := ParseMachO(d)
return "macho", f, err
}
return "", nil, errors.New("no parser matched")
}
func isPCAPMagic(head []byte) bool {
m := binary.LittleEndian.Uint32(head[:4])
return m == 0xa1b2c3d4 || m == 0xd4c3b2a1 || m == 0xa1b23c4d || m == 0x4d3cb2a1
}
// ParsePNG walks chunks sequentially and returns a Field tree.
func ParsePNG(d *document.Document) (*Field, error) {
root := &Field{Name: "PNG", Type: "container", Offset: 0}
hdr, err := d.Range(0, 8)
if err != nil {
return nil, err
}
root.Children = append(root.Children, &Field{
Name: "signature", Type: "bytes", Offset: 0, Length: 8,
Value: fmt.Sprintf("% x", hdr),
})
off := int64(8)
size := d.Size()
for off+8 <= size {
hdr, err := d.Range(off, 8)
if err != nil {
return root, err
}
ln := int64(binary.BigEndian.Uint32(hdr[0:4]))
typ := string(hdr[4:8])
chunk := &Field{
Name: typ + "@" + fmt.Sprintf("0x%x", off),
Type: "chunk",
Offset: off,
Length: 12 + ln,
Value: fmt.Sprintf("length=%d", ln),
}
chunk.Children = []*Field{
{Name: "length", Type: "u32be", Offset: off, Length: 4, Value: fmt.Sprintf("%d", ln)},
{Name: "type", Type: "ascii", Offset: off + 4, Length: 4, Value: typ},
{Name: "data", Type: "bytes", Offset: off + 8, Length: ln, Value: fmt.Sprintf("%d bytes", ln)},
{Name: "crc", Type: "u32be", Offset: off + 8 + ln, Length: 4, Value: "crc32"},
}
root.Children = append(root.Children, chunk)
off += 12 + ln
if typ == "IEND" {
break
}
}
root.Length = off
return root, nil
}
// ParseJPEG walks marker segments.
func ParseJPEG(d *document.Document) (*Field, error) {
root := &Field{Name: "JPEG", Type: "container", Offset: 0}
off := int64(0)
size := d.Size()
for off < size {
b, err := d.Range(off, 2)
if err != nil || len(b) < 2 {
break
}
if b[0] != 0xff {
return root, errors.New("lost sync")
}
marker := b[1]
mark := &Field{
Name: jpegMarkerName(marker),
Type: "marker",
Offset: off,
Value: fmt.Sprintf("0x%02x", marker),
}
off += 2
if marker == 0xd8 || marker == 0xd9 || marker == 0x00 || (marker >= 0xd0 && marker <= 0xd7) {
mark.Length = 2
root.Children = append(root.Children, mark)
if marker == 0xd9 {
break
}
continue
}
lb, err := d.Range(off, 2)
if err != nil || len(lb) < 2 {
break
}
ln := int64(binary.BigEndian.Uint16(lb))
mark.Length = 2 + ln
mark.Children = []*Field{
{Name: "length", Type: "u16be", Offset: off, Length: 2, Value: fmt.Sprintf("%d", ln)},
{Name: "data", Type: "bytes", Offset: off + 2, Length: ln - 2, Value: fmt.Sprintf("%d bytes", ln-2)},
}
root.Children = append(root.Children, mark)
off += ln
if marker == 0xda { // SOS
// scan until next marker
for off < size {
bb, err := d.Range(off, 2)
if err != nil || len(bb) < 2 {
break
}
if bb[0] == 0xff && bb[1] != 0x00 && !(bb[1] >= 0xd0 && bb[1] <= 0xd7) {
break
}
off++
}
}
}
root.Length = off
return root, nil
}
func jpegMarkerName(m byte) string {
switch m {
case 0xd8:
return "SOI"
case 0xd9:
return "EOI"
case 0xda:
return "SOS"
case 0xdb:
return "DQT"
case 0xc4:
return "DHT"
case 0xe0:
return "APP0"
case 0xe1:
return "APP1"
case 0xfe:
return "COM"
}
if m >= 0xc0 && m <= 0xcf {
return fmt.Sprintf("SOF%d", int(m-0xc0))
}
if m >= 0xe0 && m <= 0xef {
return fmt.Sprintf("APP%d", int(m-0xe0))
}
return fmt.Sprintf("MRK%02x", m)
}
// ParseGIF walks the logical screen descriptor, global colour table,
// extension blocks, image descriptors and trailer.
func ParseGIF(d *document.Document) (*Field, error) {
size := d.Size()
if size < 13 {
return nil, errors.New("gif too short")
}
hdr, err := d.Range(0, 13)
if err != nil {
return nil, err
}
version := string(hdr[0:6])
if version != "GIF87a" && version != "GIF89a" {
return nil, errors.New("bad gif signature")
}
width := binary.LittleEndian.Uint16(hdr[6:8])
height := binary.LittleEndian.Uint16(hdr[8:10])
packed := hdr[10]
gctFlag := packed&0x80 != 0
colorRes := ((packed >> 4) & 0x07) + 1
sortFlag := packed&0x08 != 0
gctEntries := 0
if gctFlag {
gctEntries = 1 << ((packed & 0x07) + 1)
}
root := &Field{
Name: "GIF",
Type: "container",
Offset: 0,
Value: fmt.Sprintf("%s %dx%d", version, width, height),
Children: []*Field{
{Name: "signature", Type: "ascii", Offset: 0, Length: 6, Value: version},
{Name: "width", Type: "u16le", Offset: 6, Length: 2, Value: fmt.Sprintf("%d", width)},
{Name: "height", Type: "u16le", Offset: 8, Length: 2, Value: fmt.Sprintf("%d", height)},
{Name: "packed", Type: "u8", Offset: 10, Length: 1, Value: fmt.Sprintf("gct=%v colorRes=%d sort=%v entries=%d", gctFlag, colorRes, sortFlag, gctEntries)},
{Name: "backgroundColorIndex", Type: "u8", Offset: 11, Length: 1, Value: fmt.Sprintf("%d", hdr[11])},
{Name: "pixelAspectRatio", Type: "u8", Offset: 12, Length: 1, Value: fmt.Sprintf("%d", hdr[12])},
},
}
off := int64(13)
if gctEntries > 0 {
ln := int64(gctEntries * 3)
if off+ln > size {
return root, errors.New("gif global colour table exceeds file")
}
root.Children = append(root.Children, &Field{Name: "globalColorTable", Type: "palette", Offset: off, Length: ln, Value: fmt.Sprintf("%d RGB entries", gctEntries)})
off += ln
}
for off < size {
tag, err := d.Range(off, 1)
if err != nil || len(tag) < 1 {
break
}
switch tag[0] {
case 0x3b:
root.Children = append(root.Children, &Field{Name: "trailer", Type: "byte", Offset: off, Length: 1, Value: "0x3b"})
off++
root.Length = off
return root, nil
case 0x21:
block, next, err := parseGIFExtension(d, off, size)
root.Children = append(root.Children, block)
if err != nil {
root.Length = next
return root, err
}
off = next
case 0x2c:
block, next, err := parseGIFImage(d, off, size)
root.Children = append(root.Children, block)
if err != nil {
root.Length = next
return root, err
}
off = next
default:
root.Children = append(root.Children, &Field{Name: "unknown", Type: "byte", Offset: off, Length: 1, Value: fmt.Sprintf("0x%02x", tag[0])})
root.Length = off + 1
return root, fmt.Errorf("unknown gif block 0x%02x at 0x%x", tag[0], off)
}
}
root.Length = off
return root, nil
}
func parseGIFExtension(d *document.Document, off, size int64) (*Field, int64, error) {
hdr, err := d.Range(off, 2)
if err != nil || len(hdr) < 2 {
return &Field{Name: "extension", Type: "block", Offset: off}, off, errors.New("short gif extension")
}
label := hdr[1]
name := gifExtensionName(label)
next, subBlocks, err := skipGIFSubBlocks(d, off+2, size)
f := &Field{
Name: name,
Type: "extension",
Offset: off,
Length: next - off,
Value: fmt.Sprintf("label=0x%02x subBlocks=%d", label, subBlocks),
Children: []*Field{
{Name: "introducer", Type: "u8", Offset: off, Length: 1, Value: "0x21"},
{Name: "label", Type: "u8", Offset: off + 1, Length: 1, Value: fmt.Sprintf("0x%02x", label)},
},
}
return f, next, err
}
func parseGIFImage(d *document.Document, off, size int64) (*Field, int64, error) {
hdr, err := d.Range(off, 10)
if err != nil || len(hdr) < 10 {
return &Field{Name: "image", Type: "image", Offset: off}, off, errors.New("short gif image descriptor")
}
left := binary.LittleEndian.Uint16(hdr[1:3])
top := binary.LittleEndian.Uint16(hdr[3:5])
width := binary.LittleEndian.Uint16(hdr[5:7])
height := binary.LittleEndian.Uint16(hdr[7:9])
packed := hdr[9]
lctEntries := 0
if packed&0x80 != 0 {
lctEntries = 1 << ((packed & 0x07) + 1)
}
next := off + 10 + int64(lctEntries*3)
if next >= size {
return nil, next, errors.New("gif image data missing")
}
lzw, err := d.Range(next, 1)
if err != nil || len(lzw) < 1 {
return nil, next, errors.New("gif lzw minimum code size missing")
}
dataStart := next + 1
next, subBlocks, err := skipGIFSubBlocks(d, dataStart, size)
f := &Field{
Name: fmt.Sprintf("image@0x%x", off),
Type: "image",
Offset: off,
Length: next - off,
Value: fmt.Sprintf("%dx%d at %d,%d subBlocks=%d", width, height, left, top, subBlocks),
Children: []*Field{
{Name: "left", Type: "u16le", Offset: off + 1, Length: 2, Value: fmt.Sprintf("%d", left)},
{Name: "top", Type: "u16le", Offset: off + 3, Length: 2, Value: fmt.Sprintf("%d", top)},
{Name: "width", Type: "u16le", Offset: off + 5, Length: 2, Value: fmt.Sprintf("%d", width)},
{Name: "height", Type: "u16le", Offset: off + 7, Length: 2, Value: fmt.Sprintf("%d", height)},
{Name: "packed", Type: "u8", Offset: off + 9, Length: 1, Value: fmt.Sprintf("lctEntries=%d interlace=%v", lctEntries, packed&0x40 != 0)},
{Name: "lzwMinimumCodeSize", Type: "u8", Offset: dataStart - 1, Length: 1, Value: fmt.Sprintf("%d", lzw[0])},
{Name: "imageData", Type: "subblocks", Offset: dataStart, Length: next - dataStart, Value: fmt.Sprintf("%d sub-blocks", subBlocks)},
},
}
if lctEntries > 0 {
f.Children = append(f.Children, &Field{Name: "localColorTable", Type: "palette", Offset: off + 10, Length: int64(lctEntries * 3), Value: fmt.Sprintf("%d RGB entries", lctEntries)})
}
return f, next, err
}
func skipGIFSubBlocks(d *document.Document, off, size int64) (int64, int, error) {
count := 0
for off < size {
b, err := d.Range(off, 1)
if err != nil || len(b) < 1 {
return off, count, errors.New("short gif sub-block")
}
off++
ln := int64(b[0])
if ln == 0 {
return off, count, nil
}
if off+ln > size {
return off, count, errors.New("gif sub-block exceeds file")
}
off += ln
count++
}
return off, count, errors.New("unterminated gif sub-block chain")
}
func gifExtensionName(label byte) string {
switch label {
case 0xf9:
return "graphicControlExtension"
case 0xfe:
return "commentExtension"
case 0xff:
return "applicationExtension"
case 0x01:
return "plainTextExtension"
}
return fmt.Sprintf("extension%02x", label)
}
// ParseBMP reads the BMP file header and DIB header metadata.
func ParseBMP(d *document.Document) (*Field, error) {
size := d.Size()
if size < 26 {
return nil, errors.New("bmp too short")
}
hdr, err := d.Range(0, min64(size, 138))
if err != nil {
return nil, err
}
if len(hdr) < 26 || string(hdr[0:2]) != "BM" {
return nil, errors.New("bad bmp signature")
}
fileSize := binary.LittleEndian.Uint32(hdr[2:6])
pixelOff := binary.LittleEndian.Uint32(hdr[10:14])
dibSize := binary.LittleEndian.Uint32(hdr[14:18])
root := &Field{
Name: "BMP",
Type: "container",
Offset: 0,
Length: int64(fileSize),
Value: fmt.Sprintf("fileSize=%d pixelOffset=0x%x dib=%s", fileSize, pixelOff, bmpDIBName(dibSize)),
Children: []*Field{
{Name: "signature", Type: "ascii", Offset: 0, Length: 2, Value: "BM"},
{Name: "fileSize", Type: "u32le", Offset: 2, Length: 4, Value: fmt.Sprintf("%d", fileSize)},
{Name: "pixelDataOffset", Type: "u32le", Offset: 10, Length: 4, Value: fmt.Sprintf("0x%x", pixelOff)},
{Name: "dibHeaderSize", Type: "u32le", Offset: 14, Length: 4, Value: fmt.Sprintf("%d (%s)", dibSize, bmpDIBName(dibSize))},
},
}
if dibSize == 12 {
if len(hdr) < 26 {
return root, nil
}
width := binary.LittleEndian.Uint16(hdr[18:20])
height := binary.LittleEndian.Uint16(hdr[20:22])
planes := binary.LittleEndian.Uint16(hdr[22:24])
bpp := binary.LittleEndian.Uint16(hdr[24:26])
root.Children = append(root.Children,
&Field{Name: "width", Type: "u16le", Offset: 18, Length: 2, Value: fmt.Sprintf("%d", width)},
&Field{Name: "height", Type: "u16le", Offset: 20, Length: 2, Value: fmt.Sprintf("%d", height)},
&Field{Name: "planes", Type: "u16le", Offset: 22, Length: 2, Value: fmt.Sprintf("%d", planes)},
&Field{Name: "bitsPerPixel", Type: "u16le", Offset: 24, Length: 2, Value: fmt.Sprintf("%d", bpp)},
)
} else if dibSize >= 40 && len(hdr) >= 54 {
width := int32(binary.LittleEndian.Uint32(hdr[18:22]))
height := int32(binary.LittleEndian.Uint32(hdr[22:26]))
planes := binary.LittleEndian.Uint16(hdr[26:28])
bpp := binary.LittleEndian.Uint16(hdr[28:30])
compression := binary.LittleEndian.Uint32(hdr[30:34])
imageSize := binary.LittleEndian.Uint32(hdr[34:38])
xppm := int32(binary.LittleEndian.Uint32(hdr[38:42]))
yppm := int32(binary.LittleEndian.Uint32(hdr[42:46]))
coloursUsed := binary.LittleEndian.Uint32(hdr[46:50])
root.Children = append(root.Children,
&Field{Name: "width", Type: "i32le", Offset: 18, Length: 4, Value: fmt.Sprintf("%d", width)},
&Field{Name: "height", Type: "i32le", Offset: 22, Length: 4, Value: fmt.Sprintf("%d", height)},
&Field{Name: "planes", Type: "u16le", Offset: 26, Length: 2, Value: fmt.Sprintf("%d", planes)},
&Field{Name: "bitsPerPixel", Type: "u16le", Offset: 28, Length: 2, Value: fmt.Sprintf("%d", bpp)},
&Field{Name: "compression", Type: "u32le", Offset: 30, Length: 4, Value: fmt.Sprintf("%d (%s)", compression, bmpCompressionName(compression))},
&Field{Name: "imageSize", Type: "u32le", Offset: 34, Length: 4, Value: fmt.Sprintf("%d", imageSize)},
&Field{Name: "xPixelsPerMeter", Type: "i32le", Offset: 38, Length: 4, Value: fmt.Sprintf("%d", xppm)},
&Field{Name: "yPixelsPerMeter", Type: "i32le", Offset: 42, Length: 4, Value: fmt.Sprintf("%d", yppm)},
&Field{Name: "coloursUsed", Type: "u32le", Offset: 46, Length: 4, Value: fmt.Sprintf("%d", coloursUsed)},
)
}
if int64(pixelOff) < size {
root.Children = append(root.Children, &Field{Name: "pixelData", Type: "bytes", Offset: int64(pixelOff), Length: size - int64(pixelOff), Value: fmt.Sprintf("%d bytes", size-int64(pixelOff))})
}
return root, nil
}
func bmpDIBName(size uint32) string {
switch size {
case 12:
return "BITMAPCOREHEADER"
case 40:
return "BITMAPINFOHEADER"
case 52:
return "BITMAPV2INFOHEADER"
case 56:
return "BITMAPV3INFOHEADER"
case 108:
return "BITMAPV4HEADER"
case 124:
return "BITMAPV5HEADER"
}
return "unknown"
}
func bmpCompressionName(v uint32) string {
switch v {
case 0:
return "BI_RGB"
case 1:
return "BI_RLE8"
case 2:
return "BI_RLE4"
case 3:
return "BI_BITFIELDS"
case 4:
return "BI_JPEG"
case 5:
return "BI_PNG"
}
return "unknown"
}
// ParseZIP walks local file headers by seeking forward.
func ParseZIP(d *document.Document) (*Field, error) {
root := &Field{Name: "ZIP", Type: "container", Offset: 0, Length: d.Size()}
size := d.Size()
// 1) Local file headers, walked from the front.
off := int64(0)
entries := 0
for off+30 <= size && entries < 5000 {
sig, err := d.Range(off, 4)
if err != nil || string(sig) != "PK\x03\x04" {
break // reached the central directory / EOCD region
}
hdr, err := d.Range(off, 30)
if err != nil {
break
}
flags := binary.LittleEndian.Uint16(hdr[6:8])
method := binary.LittleEndian.Uint16(hdr[8:10])
dosTime := binary.LittleEndian.Uint16(hdr[10:12])
dosDate := binary.LittleEndian.Uint16(hdr[12:14])
crc := binary.LittleEndian.Uint32(hdr[14:18])
compSize := int64(binary.LittleEndian.Uint32(hdr[18:22]))
uncompSize := int64(binary.LittleEndian.Uint32(hdr[22:26]))
nameLen := int64(binary.LittleEndian.Uint16(hdr[26:28]))
extraLen := int64(binary.LittleEndian.Uint16(hdr[28:30]))
nameB, _ := d.Range(off+30, nameLen)
e := &Field{
Name: string(nameB),
Type: "entry",
Offset: off,
Length: 30 + nameLen + extraLen + compSize,
Value: fmt.Sprintf("%s comp=%d uncomp=%d", zipMethod(method), compSize, uncompSize),
Children: []*Field{
{Name: "signature", Type: "bytes", Offset: off, Length: 4, Value: "PK\\x03\\x04"},
{Name: "version_needed", Type: "u16le", Offset: off + 4, Length: 2, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint16(hdr[4:6]))},
{Name: "flags", Type: "u16le", Offset: off + 6, Length: 2, Value: zipFlags(flags)},
{Name: "method", Type: "u16le", Offset: off + 8, Length: 2, Value: zipMethod(method)},
{Name: "mod_time", Type: "dostime", Offset: off + 10, Length: 4, Value: zipDOSDateTime(dosDate, dosTime)},
{Name: "crc32", Type: "u32le", Offset: off + 14, Length: 4, Value: fmt.Sprintf("0x%08x", crc)},
{Name: "compressed_size", Type: "u32le", Offset: off + 18, Length: 4, Value: fmt.Sprintf("%d", compSize)},
{Name: "uncompressed_size", Type: "u32le", Offset: off + 22, Length: 4, Value: fmt.Sprintf("%d", uncompSize)},
{Name: "name", Type: "ascii", Offset: off + 30, Length: nameLen, Value: string(nameB)},
{Name: "data", Type: "bytes", Offset: off + 30 + nameLen + extraLen, Length: compSize, Value: fmt.Sprintf("%d bytes", compSize)},
},
}
root.Children = append(root.Children, e)
off += 30 + nameLen + extraLen + compSize
entries++
}
// 2) Central directory + end-of-central-directory, located from the back.
if cd := zipCentralDirectory(d); cd != nil {
root.Children = append(root.Children, cd...)
}
return root, nil
}
// zipCentralDirectory finds the EOCD record, decodes it, and walks the central
// directory it points at. Returns the EOCD field plus a "central_directory"
// container, or nil if no EOCD is present.
func zipCentralDirectory(d *document.Document) []*Field {
size := d.Size()
// EOCD is at most 22 + 65535 bytes from the end; scan a bounded tail.
scan := int64(22 + 65535)
if scan > size {
scan = size
}
tail, err := d.Range(size-scan, scan)
if err != nil {
return nil
}
eocdRel := int64(-1)
for i := len(tail) - 22; i >= 0; i-- {
if tail[i] == 'P' && tail[i+1] == 'K' && tail[i+2] == 0x05 && tail[i+3] == 0x06 {
eocdRel = int64(i)
break
}
}
if eocdRel < 0 {
return nil
}
eocdOff := size - scan + eocdRel
e, _ := d.Range(eocdOff, 22)
if len(e) < 22 {
return nil
}
total := binary.LittleEndian.Uint16(e[10:12])
cdSize := int64(binary.LittleEndian.Uint32(e[12:16]))
cdOff := int64(binary.LittleEndian.Uint32(e[16:20]))
commentLen := int64(binary.LittleEndian.Uint16(e[20:22]))
eocd := &Field{
Name: "end_of_central_directory", Type: "eocd", Offset: eocdOff, Length: 22 + commentLen,
Value: fmt.Sprintf("%d entries, cd@0x%x size=%d", total, cdOff, cdSize),
Children: []*Field{
{Name: "total_entries", Type: "u16le", Offset: eocdOff + 10, Length: 2, Value: fmt.Sprintf("%d", total)},
{Name: "cd_size", Type: "u32le", Offset: eocdOff + 12, Length: 4, Value: fmt.Sprintf("%d", cdSize)},
{Name: "cd_offset", Type: "u32le", Offset: eocdOff + 16, Length: 4, Value: fmt.Sprintf("0x%x", cdOff)},
{Name: "comment_length", Type: "u16le", Offset: eocdOff + 20, Length: 2, Value: fmt.Sprintf("%d", commentLen)},
},
}
out := []*Field{}
// ZIP64: a 0xffffffff offset or 0xffff count signals a ZIP64 EOCD locator.
if cdOff == 0xffffffff || total == 0xffff {
if eocdOff-20 >= 0 {
loc, _ := d.Range(eocdOff-20, 20)
if len(loc) == 20 && string(loc[0:4]) == "PK\x06\x07" {
z64Off := int64(binary.LittleEndian.Uint64(loc[8:16]))
eocd.Children = append(eocd.Children, &Field{Name: "zip64_locator", Type: "bytes", Offset: eocdOff - 20, Length: 20, Value: fmt.Sprintf("zip64 EOCD @0x%x", z64Off)})
if z64, _ := d.Range(z64Off, 56); len(z64) == 56 && string(z64[0:4]) == "PK\x06\x06" {
cdOff = int64(binary.LittleEndian.Uint64(z64[48:56]))
cdSize = int64(binary.LittleEndian.Uint64(z64[40:48]))
out = append(out, &Field{Name: "zip64_eocd", Type: "zip64", Offset: z64Off, Length: 56, Value: fmt.Sprintf("entries=%d cd@0x%x", binary.LittleEndian.Uint64(z64[32:40]), cdOff)})
}
}
}
}
// Walk central-directory file headers.
if cdOff >= 0 && cdOff < size {
cd := &Field{Name: "central_directory", Type: "directory", Offset: cdOff, Length: cdSize}
off := cdOff
end := size
if cdSize > 0 && cdOff+cdSize <= size {
end = cdOff + cdSize
}
for n := 0; off+46 <= end && n < 5000; n++ {
h, err := d.Range(off, 46)
if err != nil || string(h[0:4]) != "PK\x01\x02" {
break
}
method := binary.LittleEndian.Uint16(h[10:12])
crc := binary.LittleEndian.Uint32(h[16:20])
compSize := int64(binary.LittleEndian.Uint32(h[20:24]))
nameLen := int64(binary.LittleEndian.Uint16(h[28:30]))
extraLen := int64(binary.LittleEndian.Uint16(h[30:32]))
commLen := int64(binary.LittleEndian.Uint16(h[32:34]))
lho := binary.LittleEndian.Uint32(h[42:46])
nameB, _ := d.Range(off+46, nameLen)
cd.Children = append(cd.Children, &Field{
Name: string(nameB), Type: "cd_entry", Offset: off, Length: 46 + nameLen + extraLen + commLen,
Value: fmt.Sprintf("%s crc=0x%08x comp=%d lfh@0x%x", zipMethod(method), crc, compSize, lho),
})
off += 46 + nameLen + extraLen + commLen
}
out = append(out, cd)
}
out = append(out, eocd)
return out
}
func zipMethod(m uint16) string {
names := map[uint16]string{0: "store", 1: "shrink", 6: "implode", 8: "deflate", 9: "deflate64", 12: "bzip2", 14: "lzma", 95: "xz", 96: "jpeg", 97: "wavpack", 98: "ppmd", 99: "aes"}
if n, ok := names[m]; ok {
return fmt.Sprintf("%d (%s)", m, n)
}
return fmt.Sprintf("%d", m)
}
func zipFlags(f uint16) string {
var on []string
if f&0x0001 != 0 {
on = append(on, "encrypted")
}
if f&0x0008 != 0 {
on = append(on, "data-descriptor")
}
if f&0x0800 != 0 {
on = append(on, "utf8")
}
if len(on) == 0 {
return fmt.Sprintf("0x%04x", f)
}
return fmt.Sprintf("0x%04x (%s)", f, strings.Join(on, "|"))
}
// zipDOSDateTime decodes the MS-DOS packed date/time used by ZIP entries.
func zipDOSDateTime(date, t uint16) string {
if date == 0 && t == 0 {
return "0"
}
year := 1980 + int(date>>9)
month := int(date>>5) & 0x0f
day := int(date) & 0x1f
hour := int(t >> 11)
minute := int(t>>5) & 0x3f
second := (int(t) & 0x1f) * 2
return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second)
}
// ParseELF returns header, program header, and section header fields.
func ParseELF(d *document.Document) (*Field, error) {
hdr, err := d.Range(0, 64)
if err != nil || len(hdr) < 52 {
return nil, errors.New("elf too short")
}
class := hdr[4]
data := hdr[5]
kind := "32-bit"
if class == 2 {
kind = "64-bit"
}
endian := "little-endian"
if data == 2 {
endian = "big-endian"
}
root := &Field{
Name: "ELF",
Type: "container",
Offset: 0,
Length: d.Size(),
Value: kind + " " + endian,
Children: []*Field{
{Name: "ei_mag", Type: "bytes", Offset: 0, Length: 4, Value: "\\x7fELF"},
{Name: "ei_class", Type: "u8", Offset: 4, Length: 1, Value: kind},
{Name: "ei_data", Type: "u8", Offset: 5, Length: 1, Value: endian},
{Name: "ei_version", Type: "u8", Offset: 6, Length: 1, Value: fmt.Sprintf("%d", hdr[6])},
{Name: "ei_osabi", Type: "u8", Offset: 7, Length: 1, Value: fmt.Sprintf("%d", hdr[7])},
},
}
var e_type, e_machine uint16
var e_entry uint64
var e_phoff, e_shoff uint64
var e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx uint16
bo := elfByteOrder(data)
if class == 2 {
e_type = bo.Uint16(hdr[16:18])
e_machine = bo.Uint16(hdr[18:20])
e_entry = bo.Uint64(hdr[24:32])
e_phoff = bo.Uint64(hdr[32:40])
e_shoff = bo.Uint64(hdr[40:48])
e_phentsize = bo.Uint16(hdr[54:56])
e_phnum = bo.Uint16(hdr[56:58])
e_shentsize = bo.Uint16(hdr[58:60])
e_shnum = bo.Uint16(hdr[60:62])
e_shstrndx = bo.Uint16(hdr[62:64])
} else {
e_type = bo.Uint16(hdr[16:18])
e_machine = bo.Uint16(hdr[18:20])
e_entry = uint64(bo.Uint32(hdr[24:28]))
e_phoff = uint64(bo.Uint32(hdr[28:32]))
e_shoff = uint64(bo.Uint32(hdr[32:36]))
e_phentsize = bo.Uint16(hdr[42:44])
e_phnum = bo.Uint16(hdr[44:46])
e_shentsize = bo.Uint16(hdr[46:48])
e_shnum = bo.Uint16(hdr[48:50])
e_shstrndx = bo.Uint16(hdr[50:52])
}
root.Children = append(root.Children,
&Field{Name: "e_type", Value: fmt.Sprintf("%d (%s)", e_type, elfType(e_type))},
&Field{Name: "e_machine", Value: fmt.Sprintf("%d (%s)", e_machine, elfMachine(e_machine))},
&Field{Name: "e_entry", Value: fmt.Sprintf("0x%x", e_entry)},
&Field{Name: "e_phoff", Value: fmt.Sprintf("0x%x", e_phoff)},
&Field{Name: "e_shoff", Value: fmt.Sprintf("0x%x", e_shoff)},
&Field{Name: "e_phentsize", Value: fmt.Sprintf("%d", e_phentsize)},
&Field{Name: "e_phnum", Value: fmt.Sprintf("%d", e_phnum)},
&Field{Name: "e_shentsize", Value: fmt.Sprintf("%d", e_shentsize)},
&Field{Name: "e_shnum", Value: fmt.Sprintf("%d", e_shnum)},
&Field{Name: "e_shstrndx", Value: fmt.Sprintf("%d", e_shstrndx)},
)
root.Children = append(root.Children, elfProgramHeaders(d, bo, class, e_phoff, e_phentsize, e_phnum)...)
root.Children = append(root.Children, elfSectionHeaders(d, bo, class, e_shoff, e_shentsize, e_shnum, e_shstrndx)...)
return root, nil
}
func elfByteOrder(data byte) binary.ByteOrder {
if data == 2 {
return binary.BigEndian
}
return binary.LittleEndian
}
func elfProgramHeaders(d *document.Document, bo binary.ByteOrder, class byte, off uint64, entsize, count uint16) []*Field {
if off == 0 || entsize == 0 || count == 0 {
return nil
}
size := d.Size()
out := make([]*Field, 0, count)
for i := uint16(0); i < count; i++ {
at := int64(off) + int64(i)*int64(entsize)
if at < 0 || at+int64(entsize) > size {
break
}
buf, err := d.Range(at, int64(entsize))
if err != nil {
break
}
var typ, flags uint32
var pOffset, vaddr, filesz, memsz, align uint64
if class == 2 {
if len(buf) < 56 {
break
}
typ = bo.Uint32(buf[0:4])
flags = bo.Uint32(buf[4:8])
pOffset = bo.Uint64(buf[8:16])
vaddr = bo.Uint64(buf[16:24])
filesz = bo.Uint64(buf[32:40])
memsz = bo.Uint64(buf[40:48])
align = bo.Uint64(buf[48:56])
} else {
if len(buf) < 32 {
break
}
typ = bo.Uint32(buf[0:4])
pOffset = uint64(bo.Uint32(buf[4:8]))
vaddr = uint64(bo.Uint32(buf[8:12]))
filesz = uint64(bo.Uint32(buf[16:20]))
memsz = uint64(bo.Uint32(buf[20:24]))
flags = bo.Uint32(buf[24:28])
align = uint64(bo.Uint32(buf[28:32]))
}
out = append(out, &Field{
Name: fmt.Sprintf("program[%d]", i),
Type: "program_header",
Offset: at,
Length: int64(entsize),
Value: fmt.Sprintf("type=%s offset=0x%x vaddr=0x%x filesz=%d memsz=%d flags=%s align=0x%x", elfProgramType(typ), pOffset, vaddr, filesz, memsz, elfProgramFlags(flags), align),
})
}
return out
}
type elfSectionInfo struct {
nameOff uint32
typ uint32
flags, addr uint64
fileOff, secSize uint64
link, info uint32
align, entrySize uint64
at int64
length int64
}
func elfSectionHeaders(d *document.Document, bo binary.ByteOrder, class byte, off uint64, entsize, count, strIndex uint16) []*Field {
if off == 0 || entsize == 0 || count == 0 {
return nil
}
size := d.Size()
sections := make([]elfSectionInfo, 0, count)
for i := uint16(0); i < count; i++ {
at := int64(off) + int64(i)*int64(entsize)
if at < 0 || at+int64(entsize) > size {
break
}
buf, err := d.Range(at, int64(entsize))
if err != nil {
break
}
var s elfSectionInfo
s.at = at
s.length = int64(entsize)
if class == 2 {
if len(buf) < 64 {
break
}
s.nameOff = bo.Uint32(buf[0:4])
s.typ = bo.Uint32(buf[4:8])
s.flags = bo.Uint64(buf[8:16])
s.addr = bo.Uint64(buf[16:24])
s.fileOff = bo.Uint64(buf[24:32])
s.secSize = bo.Uint64(buf[32:40])
s.link = bo.Uint32(buf[40:44])
s.info = bo.Uint32(buf[44:48])
s.align = bo.Uint64(buf[48:56])
s.entrySize = bo.Uint64(buf[56:64])
} else {
if len(buf) < 40 {
break
}
s.nameOff = bo.Uint32(buf[0:4])
s.typ = bo.Uint32(buf[4:8])
s.flags = uint64(bo.Uint32(buf[8:12]))
s.addr = uint64(bo.Uint32(buf[12:16]))
s.fileOff = uint64(bo.Uint32(buf[16:20]))
s.secSize = uint64(bo.Uint32(buf[20:24]))
s.link = bo.Uint32(buf[24:28])
s.info = bo.Uint32(buf[28:32])
s.align = uint64(bo.Uint32(buf[32:36]))
s.entrySize = uint64(bo.Uint32(buf[36:40]))
}
sections = append(sections, s)
}
var names []byte
if int(strIndex) < len(sections) {
str := sections[strIndex]
if str.fileOff <= uint64(size) && str.fileOff+str.secSize <= uint64(size) && str.secSize <= 1<<20 {
names, _ = d.Range(int64(str.fileOff), int64(str.secSize))
}
}
sectionNames := make([]string, len(sections))
for i, s := range sections {
name := elfStringAt(names, s.nameOff)
if name == "" {
name = fmt.Sprintf("section[%d]", i)
}
sectionNames[i] = name
}
out := make([]*Field, 0, len(sections))
for i, s := range sections {
name := sectionNames[i]
children := []*Field{}
if s.typ == 6 {
children = elfDynamicEntries(d, bo, class, sections, s)
} else if s.typ == 2 || s.typ == 11 {
children = elfSymbolEntries(d, bo, class, sections, s)
}
out = append(out, &Field{
Name: name,
Type: "section_header",
Offset: s.at,
Length: s.length,
Value: fmt.Sprintf("type=%s offset=0x%x size=%d addr=0x%x flags=%s link=%d info=%d align=0x%x entsize=%d", elfSectionType(s.typ), s.fileOff, s.secSize, s.addr, elfSectionFlags(s.flags), s.link, s.info, s.align, s.entrySize),
Children: children,
})
}
return out
}
func elfSymbolEntries(d *document.Document, bo binary.ByteOrder, class byte, sections []elfSectionInfo, symtab elfSectionInfo) []*Field {
entrySize := symtab.entrySize
if entrySize == 0 {
if class == 2 {
entrySize = 24
} else {
entrySize = 16
}
}
if entrySize == 0 || symtab.secSize == 0 || symtab.fileOff+symtab.secSize > uint64(d.Size()) {
return nil
}
var strtab []byte
if int(symtab.link) < len(sections) {
linked := sections[symtab.link]
if linked.fileOff+linked.secSize <= uint64(d.Size()) && linked.secSize <= 8<<20 {
strtab, _ = d.Range(int64(linked.fileOff), int64(linked.secSize))
}
}
count := int(symtab.secSize / entrySize)
if count > 4096 {
count = 4096
}
out := []*Field{}
for i := 0; i < count; i++ {
at := int64(symtab.fileOff + uint64(i)*entrySize)
buf, err := d.Range(at, int64(entrySize))
if err != nil || len(buf) < int(entrySize) {
break
}
var nameOff uint32
var info, other byte
var shndx uint16
var value, size uint64
if class == 2 {
nameOff = bo.Uint32(buf[0:4])
info = buf[4]
other = buf[5]
shndx = bo.Uint16(buf[6:8])
value = bo.Uint64(buf[8:16])
size = bo.Uint64(buf[16:24])
} else {
nameOff = bo.Uint32(buf[0:4])
value = uint64(bo.Uint32(buf[4:8]))
size = uint64(bo.Uint32(buf[8:12]))
info = buf[12]
other = buf[13]
shndx = bo.Uint16(buf[14:16])
}
name := elfStringAt(strtab, nameOff)
if name == "" {
name = fmt.Sprintf("<unnamed:%d>", i)
}
out = append(out, &Field{
Name: fmt.Sprintf("symbol[%d] %s", i, name),
Type: "symbol",
Offset: at,
Length: int64(entrySize),
Value: fmt.Sprintf("name=%s bind=%s type=%s vis=%s shndx=%s value=0x%x size=%d",
name, elfSymbolBind(info>>4), elfSymbolType(info&0xf), elfSymbolVisibility(other&0x3), elfSymbolSection(shndx), value, size),
})
}
return out
}
func elfDynamicEntries(d *document.Document, bo binary.ByteOrder, class byte, sections []elfSectionInfo, dyn elfSectionInfo) []*Field {
entrySize := dyn.entrySize
if entrySize == 0 {
if class == 2 {
entrySize = 16
} else {
entrySize = 8
}
}
if entrySize == 0 || dyn.secSize == 0 || dyn.fileOff+dyn.secSize > uint64(d.Size()) {
return nil
}
var strtab []byte
if int(dyn.link) < len(sections) {
linked := sections[dyn.link]
if linked.fileOff+linked.secSize <= uint64(d.Size()) && linked.secSize <= 8<<20 {
strtab, _ = d.Range(int64(linked.fileOff), int64(linked.secSize))
}
}
out := []*Field{}
count := int(dyn.secSize / entrySize)
if count > 4096 {
count = 4096
}
for i := 0; i < count; i++ {
at := int64(dyn.fileOff + uint64(i)*entrySize)
buf, err := d.Range(at, int64(entrySize))
if err != nil || len(buf) < int(entrySize) {
break
}
var tag int64
var val uint64
if class == 2 {
tag = int64(bo.Uint64(buf[0:8]))
val = bo.Uint64(buf[8:16])
} else {
tag = int64(int32(bo.Uint32(buf[0:4])))
val = uint64(bo.Uint32(buf[4:8]))
}
value := fmt.Sprintf("%s=0x%x", elfDynamicTag(tag), val)
if tag == 1 && len(strtab) > 0 {
if s := elfStringAt(strtab, uint32(val)); s != "" {
value = "NEEDED=" + s
}
}
out = append(out, &Field{
Name: fmt.Sprintf("dynamic[%d]", i),
Type: "dynamic_entry",
Offset: at,
Length: int64(entrySize),
Value: value,
})
if tag == 0 {
break
}
}
return out
}
func elfSymbolBind(bind byte) string {
switch bind {
case 0:
return "LOCAL"
case 1:
return "GLOBAL"
case 2:
return "WEAK"
}
return fmt.Sprintf("0x%x", bind)
}
func elfSymbolType(typ byte) string {
switch typ {
case 0:
return "NOTYPE"
case 1:
return "OBJECT"
case 2:
return "FUNC"
case 3:
return "SECTION"
case 4:
return "FILE"
case 5:
return "COMMON"
case 6:
return "TLS"
}
return fmt.Sprintf("0x%x", typ)
}
func elfSymbolVisibility(vis byte) string {
switch vis {
case 0:
return "DEFAULT"
case 1:
return "INTERNAL"
case 2:
return "HIDDEN"
case 3:
return "PROTECTED"
}
return fmt.Sprintf("0x%x", vis)
}
func elfSymbolSection(idx uint16) string {
switch idx {
case 0:
return "UND"
case 0xfff1:
return "ABS"
case 0xfff2:
return "COMMON"
}
return fmt.Sprintf("%d", idx)
}
func elfDynamicTag(tag int64) string {
switch tag {
case 0:
return "NULL"
case 1:
return "NEEDED"
case 2:
return "PLTRELSZ"
case 3:
return "PLTGOT"
case 4:
return "HASH"
case 5:
return "STRTAB"
case 6:
return "SYMTAB"
case 7:
return "RELA"
case 8:
return "RELASZ"
case 9:
return "RELAENT"
case 10:
return "STRSZ"
case 11:
return "SYMENT"
case 12:
return "INIT"
case 13:
return "FINI"
case 14:
return "SONAME"
case 15:
return "RPATH"
case 16:
return "SYMBOLIC"
case 17:
return "REL"
case 18:
return "RELSZ"
case 19:
return "RELENT"
case 20:
return "PLTREL"
case 21:
return "DEBUG"
case 22:
return "TEXTREL"
case 23:
return "JMPREL"
case 29:
return "RUNPATH"
}
return fmt.Sprintf("0x%x", tag)
}
func elfStringAt(buf []byte, off uint32) string {
if int(off) >= len(buf) {
return ""
}
end := int(off)
for end < len(buf) && buf[end] != 0 {
end++
}
return string(buf[off:end])
}
func elfProgramType(t uint32) string {
switch t {
case 0:
return "NULL"
case 1:
return "LOAD"
case 2:
return "DYNAMIC"
case 3:
return "INTERP"
case 4:
return "NOTE"
case 5:
return "SHLIB"
case 6:
return "PHDR"
case 7:
return "TLS"
}
return fmt.Sprintf("0x%x", t)
}
func elfProgramFlags(flags uint32) string {
var out string
if flags&4 != 0 {
out += "R"
}
if flags&2 != 0 {
out += "W"
}
if flags&1 != 0 {
out += "X"
}
if out == "" {
return "-"
}
return out
}
func elfSectionType(t uint32) string {
switch t {
case 0:
return "NULL"
case 1:
return "PROGBITS"
case 2:
return "SYMTAB"
case 3:
return "STRTAB"
case 4:
return "RELA"
case 5:
return "HASH"
case 6:
return "DYNAMIC"
case 7:
return "NOTE"
case 8:
return "NOBITS"
case 9:
return "REL"
case 11:
return "DYNSYM"
case 14:
return "INIT_ARRAY"
case 15:
return "FINI_ARRAY"
case 17:
return "GROUP"
case 18:
return "SYMTAB_SHNDX"
}
return fmt.Sprintf("0x%x", t)
}
func elfSectionFlags(flags uint64) string {
var out string
if flags&0x1 != 0 {
out += "W"
}
if flags&0x2 != 0 {
out += "A"
}
if flags&0x4 != 0 {
out += "X"
}
if flags&0x10 != 0 {
out += "M"
}
if flags&0x20 != 0 {
out += "S"
}
if flags&0x40 != 0 {
out += "I"
}
if flags&0x400 != 0 {
out += "T"
}
if out == "" {
return "-"
}
return out
}
func elfType(t uint16) string {
switch t {
case 1:
return "REL"
case 2:
return "EXEC"
case 3:
return "DYN"
case 4:
return "CORE"
}
return "UNKNOWN"
}
func elfMachine(m uint16) string {
switch m {
case 0x03:
return "x86"
case 0x28:
return "ARM"
case 0x3e:
return "x86_64"
case 0xb7:
return "AArch64"
case 0x08:
return "MIPS"
case 0xf3:
return "RISC-V"
}
return "other"
}
// ParsePE walks the DOS header, PE header, and section headers.
func ParsePE(d *document.Document) (*Field, error) {
hdr, err := d.Range(0, 64)
if err != nil || len(hdr) < 64 {
return nil, errors.New("too short")
}
lfanew := int64(binary.LittleEndian.Uint32(hdr[0x3c:0x40]))
root := &Field{Name: "PE", Type: "container", Offset: 0}
root.Children = append(root.Children,
&Field{Name: "e_magic", Offset: 0, Length: 2, Value: "MZ"},
&Field{Name: "e_lfanew", Offset: 0x3c, Length: 4, Value: fmt.Sprintf("0x%x", lfanew)},
)
peHdr, err := d.Range(lfanew, 24)
if err != nil || len(peHdr) < 24 || string(peHdr[:4]) != "PE\x00\x00" {
return root, errors.New("no PE signature")
}
machine := binary.LittleEndian.Uint16(peHdr[4:6])
numSections := binary.LittleEndian.Uint16(peHdr[6:8])
timestamp := binary.LittleEndian.Uint32(peHdr[8:12])
sizeOptHdr := binary.LittleEndian.Uint16(peHdr[20:22])
characteristics := binary.LittleEndian.Uint16(peHdr[22:24])
root.Children = append(root.Children,
&Field{Name: "Signature", Offset: lfanew, Length: 4, Value: "PE\\x00\\x00"},
&Field{Name: "Machine", Offset: lfanew + 4, Length: 2, Value: fmt.Sprintf("0x%04x (%s)", machine, peMachine(machine))},
&Field{Name: "NumberOfSections", Offset: lfanew + 6, Length: 2, Value: fmt.Sprintf("%d", numSections)},
&Field{Name: "TimeDateStamp", Offset: lfanew + 8, Length: 4, Value: fmt.Sprintf("%d", timestamp)},
&Field{Name: "SizeOfOptionalHeader", Offset: lfanew + 20, Length: 2, Value: fmt.Sprintf("%d", sizeOptHdr)},
&Field{Name: "Characteristics", Offset: lfanew + 22, Length: 2, Value: fmt.Sprintf("0x%04x", characteristics)},
)
sections := make([]peSectionInfo, 0, numSections)
optOff := lfanew + 24
var dataDirs []peDataDirectory
peIs64 := false
if sizeOptHdr > 0 {
opt, err := d.Range(optOff, int64(sizeOptHdr))
if err == nil && len(opt) >= 2 {
optional, dirs := parsePEOptionalHeader(opt, optOff)
root.Children = append(root.Children, optional)
dataDirs = dirs
peIs64 = binary.LittleEndian.Uint16(opt[0:2]) == 0x20b
}
}
sectOff := lfanew + 24 + int64(sizeOptHdr)
for i := 0; i < int(numSections); i++ {
sh, err := d.Range(sectOff, 40)
if err != nil || len(sh) < 40 {
break
}
name := string(trimZero(sh[0:8]))
vsize := binary.LittleEndian.Uint32(sh[8:12])
vaddr := binary.LittleEndian.Uint32(sh[12:16])
rsize := binary.LittleEndian.Uint32(sh[16:20])
raddr := binary.LittleEndian.Uint32(sh[20:24])
flags := binary.LittleEndian.Uint32(sh[36:40])
sec := &Field{
Name: name, Type: "section", Offset: sectOff, Length: 40,
Value: fmt.Sprintf("vaddr=0x%x vsize=%d raw=0x%x rsize=%d flags=0x%x", vaddr, vsize, raddr, rsize, flags),
}
root.Children = append(root.Children, sec)
sections = append(sections, peSectionInfo{Name: name, VirtualAddress: vaddr, VirtualSize: vsize, RawOffset: raddr, RawSize: rsize, Characteristics: flags})
sectOff += 40
}
if len(dataDirs) > 0 && dataDirs[0].RVA != 0 && dataDirs[0].Size != 0 {
root.Children = append(root.Children, parsePEExports(d, sections, dataDirs[0])...)
}
if len(dataDirs) > 1 && dataDirs[1].RVA != 0 && dataDirs[1].Size != 0 {
root.Children = append(root.Children, parsePEImports(d, sections, dataDirs[1], peIs64)...)
}
if len(dataDirs) > 2 && dataDirs[2].RVA != 0 && dataDirs[2].Size != 0 {
root.Children = append(root.Children, parsePEResources(d, sections, dataDirs[2])...)
}
return root, nil
}
type peDataDirectory struct {
Name string
RVA uint32
Size uint32
}
type peSectionInfo struct {
Name string
VirtualAddress uint32
VirtualSize uint32
RawOffset uint32
RawSize uint32
Characteristics uint32
}
func parsePEOptionalHeader(opt []byte, off int64) (*Field, []peDataDirectory) {
magic := binary.LittleEndian.Uint16(opt[0:2])
is64 := magic == 0x20b
name := "PE32"
if is64 {
name = "PE32+"
}
field := &Field{Name: "optional_header", Type: "optional_header", Offset: off, Length: int64(len(opt)), Value: name}
if len(opt) >= 28 {
field.Children = append(field.Children,
&Field{Name: "Magic", Type: "u16le", Offset: off, Length: 2, Value: fmt.Sprintf("0x%04x (%s)", magic, name)},
&Field{Name: "AddressOfEntryPoint", Type: "u32le", Offset: off + 16, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[16:20]))},
&Field{Name: "BaseOfCode", Type: "u32le", Offset: off + 20, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[20:24]))},
)
}
if is64 && len(opt) >= 112 {
field.Children = append(field.Children,
&Field{Name: "ImageBase", Type: "u64le", Offset: off + 24, Length: 8, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint64(opt[24:32]))},
&Field{Name: "SectionAlignment", Type: "u32le", Offset: off + 32, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[32:36]))},
&Field{Name: "FileAlignment", Type: "u32le", Offset: off + 36, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[36:40]))},
&Field{Name: "SizeOfImage", Type: "u32le", Offset: off + 56, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[56:60]))},
&Field{Name: "Subsystem", Type: "u16le", Offset: off + 68, Length: 2, Value: peSubsystem(binary.LittleEndian.Uint16(opt[68:70]))},
)
return field, peDataDirectories(opt, off, 112)
}
if !is64 && len(opt) >= 96 {
field.Children = append(field.Children,
&Field{Name: "ImageBase", Type: "u32le", Offset: off + 28, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[28:32]))},
&Field{Name: "SectionAlignment", Type: "u32le", Offset: off + 32, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[32:36]))},
&Field{Name: "FileAlignment", Type: "u32le", Offset: off + 36, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[36:40]))},
&Field{Name: "SizeOfImage", Type: "u32le", Offset: off + 56, Length: 4, Value: fmt.Sprintf("0x%x", binary.LittleEndian.Uint32(opt[56:60]))},
&Field{Name: "Subsystem", Type: "u16le", Offset: off + 68, Length: 2, Value: peSubsystem(binary.LittleEndian.Uint16(opt[68:70]))},
)
return field, peDataDirectories(opt, off, 96)
}
return field, nil
}
func peDataDirectories(opt []byte, optOff int64, dirOff int) []peDataDirectory {
if len(opt) < dirOff {
return nil
}
names := []string{"export", "import", "resource", "exception", "certificate", "base_reloc", "debug", "architecture", "global_ptr", "tls", "load_config", "bound_import", "iat", "delay_import", "clr_runtime", "reserved"}
count := (len(opt) - dirOff) / 8
if count > len(names) {
count = len(names)
}
out := make([]peDataDirectory, 0, count)
for i := 0; i < count; i++ {
at := dirOff + i*8
rva := binary.LittleEndian.Uint32(opt[at : at+4])
size := binary.LittleEndian.Uint32(opt[at+4 : at+8])
out = append(out, peDataDirectory{Name: names[i], RVA: rva, Size: size})
}
return out
}
func parsePEImports(d *document.Document, sections []peSectionInfo, dir peDataDirectory, is64 bool) []*Field {
off, ok := peRVAToOffset(sections, dir.RVA)
if !ok {
return nil
}
out := []*Field{}
for i := 0; i < 256; i++ {
at := int64(off) + int64(i*20)
buf, err := d.Range(at, 20)
if err != nil || len(buf) < 20 {
break
}
oft := binary.LittleEndian.Uint32(buf[0:4])
timestamp := binary.LittleEndian.Uint32(buf[4:8])
forwarder := binary.LittleEndian.Uint32(buf[8:12])
nameRVA := binary.LittleEndian.Uint32(buf[12:16])
firstThunk := binary.LittleEndian.Uint32(buf[16:20])
if oft == 0 && timestamp == 0 && forwarder == 0 && nameRVA == 0 && firstThunk == 0 {
break
}
name := ""
if nameOff, ok := peRVAToOffset(sections, nameRVA); ok {
name = readCString(d, int64(nameOff), 256)
}
if name == "" {
name = fmt.Sprintf("rva_0x%x", nameRVA)
}
out = append(out, &Field{
Name: fmt.Sprintf("import[%d]", i),
Type: "import_descriptor",
Offset: at,
Length: 20,
Value: fmt.Sprintf("dll=%s oft=0x%x first_thunk=0x%x timestamp=0x%x", name, oft, firstThunk, timestamp),
Children: parsePEImportThunks(d, sections, oft, firstThunk, is64),
})
}
return out
}
func parsePEExports(d *document.Document, sections []peSectionInfo, dir peDataDirectory) []*Field {
off, ok := peRVAToOffset(sections, dir.RVA)
if !ok {
return nil
}
buf, err := d.Range(int64(off), 40)
if err != nil || len(buf) < 40 {
return nil
}
nameRVA := binary.LittleEndian.Uint32(buf[12:16])
ordinalBase := binary.LittleEndian.Uint32(buf[16:20])
numberOfFunctions := binary.LittleEndian.Uint32(buf[20:24])
numberOfNames := binary.LittleEndian.Uint32(buf[24:28])
functionsRVA := binary.LittleEndian.Uint32(buf[28:32])
namesRVA := binary.LittleEndian.Uint32(buf[32:36])
ordinalsRVA := binary.LittleEndian.Uint32(buf[36:40])
module := ""
if nameOff, ok := peRVAToOffset(sections, nameRVA); ok {
module = readCString(d, int64(nameOff), 512)
}
if module == "" {
module = fmt.Sprintf("rva_0x%x", nameRVA)
}
root := &Field{
Name: "exports",
Type: "export_directory",
Offset: int64(off),
Length: 40,
Value: fmt.Sprintf("module=%s ordinal_base=%d functions=%d names=%d", module, ordinalBase, numberOfFunctions, numberOfNames),
}
if numberOfNames > 4096 {
numberOfNames = 4096
}
namesOff, namesOK := peRVAToOffset(sections, namesRVA)
ordinalsOff, ordOK := peRVAToOffset(sections, ordinalsRVA)
functionsOff, funcsOK := peRVAToOffset(sections, functionsRVA)
if namesOK && ordOK && funcsOK {
for i := uint32(0); i < numberOfNames; i++ {
namePtrAt := int64(namesOff) + int64(i)*4
namePtr, err := d.Range(namePtrAt, 4)
if err != nil || len(namePtr) < 4 {
break
}
symNameRVA := binary.LittleEndian.Uint32(namePtr)
symName := ""
if symNameOff, ok := peRVAToOffset(sections, symNameRVA); ok {
symName = readCString(d, int64(symNameOff), 512)
}
if symName == "" {
symName = fmt.Sprintf("rva_0x%x", symNameRVA)
}
ordAt := int64(ordinalsOff) + int64(i)*2
ordBuf, err := d.Range(ordAt, 2)
if err != nil || len(ordBuf) < 2 {
break
}
index := uint32(binary.LittleEndian.Uint16(ordBuf))
if index >= numberOfFunctions {
continue
}
funcAt := int64(functionsOff) + int64(index)*4
funcBuf, err := d.Range(funcAt, 4)
if err != nil || len(funcBuf) < 4 {
break
}
funcRVA := binary.LittleEndian.Uint32(funcBuf)
value := fmt.Sprintf("name=%s ordinal=%d rva=0x%x", symName, ordinalBase+index, funcRVA)
if funcOff, ok := peRVAToOffset(sections, funcRVA); ok {
value += fmt.Sprintf(" file=0x%x", funcOff)
}
root.Children = append(root.Children, &Field{
Name: fmt.Sprintf("export[%d] %s", i, symName),
Type: "export",
Offset: namePtrAt,
Length: 4,
Value: value,
})
}
}
return []*Field{root}
}
func parsePEResources(d *document.Document, sections []peSectionInfo, dir peDataDirectory) []*Field {
baseOff, ok := peRVAToOffset(sections, dir.RVA)
if !ok {
return nil
}
root := &Field{
Name: "resources",
Type: "resource_directory",
Offset: int64(baseOff),
Length: int64(dir.Size),
Value: fmt.Sprintf("rva=0x%x size=%d", dir.RVA, dir.Size),
}
seen := map[uint32]bool{}
root.Children = parsePEResourceDirectory(d, sections, dir.RVA, baseOff, 0, 0, seen, "")
return []*Field{root}
}
func parsePEResourceDirectory(d *document.Document, sections []peSectionInfo, baseRVA, baseOff, rel uint32, depth int, seen map[uint32]bool, path string) []*Field {
if depth > 8 || seen[rel] {
return nil
}
seen[rel] = true
defer delete(seen, rel)
at := int64(baseOff + rel)
buf, err := d.Range(at, 16)
if err != nil || len(buf) < 16 {
return nil
}
named := binary.LittleEndian.Uint16(buf[12:14])
ids := binary.LittleEndian.Uint16(buf[14:16])
count := int(named) + int(ids)
if count > 1024 {
count = 1024
}
header := &Field{
Name: fmt.Sprintf("directory depth=%d", depth),
Type: "resource_directory_header",
Offset: at,
Length: 16,
Value: fmt.Sprintf("named=%d ids=%d timestamp=0x%x version=%d.%d", named, ids, binary.LittleEndian.Uint32(buf[4:8]), binary.LittleEndian.Uint16(buf[10:12]), binary.LittleEndian.Uint16(buf[8:10])),
}
out := []*Field{header}
for i := 0; i < count; i++ {
entryAt := at + 16 + int64(i)*8
ent, err := d.Range(entryAt, 8)
if err != nil || len(ent) < 8 {
break
}
nameRaw := binary.LittleEndian.Uint32(ent[0:4])
targetRaw := binary.LittleEndian.Uint32(ent[4:8])
name := peResourceName(d, baseOff, nameRaw, depth)
targetRel := targetRaw & 0x7fffffff
nextPath := path
if nextPath == "" {
nextPath = name
} else {
nextPath += "/" + name
}
entry := &Field{
Name: fmt.Sprintf("entry[%d] %s", i, name),
Type: "resource_entry",
Offset: entryAt,
Length: 8,
Value: fmt.Sprintf("name=0x%x target=0x%x path=%s", nameRaw, targetRaw, nextPath),
}
if targetRaw&0x80000000 != 0 {
entry.Children = parsePEResourceDirectory(d, sections, baseRVA, baseOff, targetRel, depth+1, seen, nextPath)
} else {
entry.Children = parsePEResourceDataEntry(d, sections, baseOff, targetRel, nextPath)
}
out = append(out, entry)
}
return out
}
func parsePEResourceDataEntry(d *document.Document, sections []peSectionInfo, baseOff, rel uint32, path string) []*Field {
at := int64(baseOff + rel)
buf, err := d.Range(at, 16)
if err != nil || len(buf) < 16 {
return nil
}
dataRVA := binary.LittleEndian.Uint32(buf[0:4])
size := binary.LittleEndian.Uint32(buf[4:8])
codePage := binary.LittleEndian.Uint32(buf[8:12])
dataOff, mapped := peRVAToOffset(sections, dataRVA)
value := fmt.Sprintf("data_rva=0x%x size=%d codepage=%d", dataRVA, size, codePage)
if mapped {
value += fmt.Sprintf(" file=0x%x", dataOff)
}
entry := &Field{Name: "data", Type: "resource_data", Offset: at, Length: 16, Value: value}
if mapped && size > 0 {
entry.Children = append(entry.Children, peResourcePayloadFields(d, int64(dataOff), size, path, codePage)...)
}
return []*Field{entry}
}
func peResourcePayloadFields(d *document.Document, off int64, size uint32, path string, codePage uint32) []*Field {
if size > 16<<20 {
return []*Field{{Name: "payload", Type: "resource_payload", Offset: off, Length: int64(size), Value: "too large to decode"}}
}
buf, err := d.Range(off, int64(size))
if err != nil {
return nil
}
out := []*Field{{Name: "payload", Type: "resource_payload", Offset: off, Length: int64(size), Value: fmt.Sprintf("%d bytes", size)}}
parts := strings.Split(path, "/")
typ := ""
if len(parts) > 0 {
typ = parts[0]
}
switch {
case strings.HasPrefix(typ, "RT_VERSION"):
out = append(out, decodePEVersionResource(buf, off)...)
case strings.HasPrefix(typ, "RT_STRING"):
out = append(out, decodePEStringTable(buf, off)...)
case strings.HasPrefix(typ, "RT_MANIFEST"):
out = append(out, &Field{Name: "manifest", Type: "manifest", Offset: off, Length: int64(size), Value: peResourceTextPreview(buf, codePage)})
default:
if txt := peResourceTextPreview(buf, codePage); txt != "" {
out = append(out, &Field{Name: "text_preview", Type: "text", Offset: off, Length: int64(size), Value: txt})
}
}
return out
}
func peResourceName(d *document.Document, baseOff, raw uint32, depth int) string {
if raw&0x80000000 != 0 {
rel := raw & 0x7fffffff
buf, err := d.Range(int64(baseOff+rel), 2)
if err != nil || len(buf) < 2 {
return fmt.Sprintf("name@0x%x", rel)
}
n := int(binary.LittleEndian.Uint16(buf))
if n <= 0 || n > 512 {
return fmt.Sprintf("name@0x%x", rel)
}
rawName, err := d.Range(int64(baseOff+rel+2), int64(n*2))
if err != nil || len(rawName) < n*2 {
return fmt.Sprintf("name@0x%x", rel)
}
return decodeUTF16LE(rawName)
}
id := raw & 0xffff
if depth == 0 {
return peResourceTypeName(id)
}
if depth == 2 {
return peLanguageName(id)
}
return fmt.Sprintf("#%d", id)
}
func peResourceTypeName(id uint32) string {
names := map[uint32]string{
1: "RT_CURSOR", 2: "RT_BITMAP", 3: "RT_ICON", 4: "RT_MENU", 5: "RT_DIALOG", 6: "RT_STRING",
7: "RT_FONTDIR", 8: "RT_FONT", 9: "RT_ACCELERATOR", 10: "RT_RCDATA", 11: "RT_MESSAGETABLE",
12: "RT_GROUP_CURSOR", 14: "RT_GROUP_ICON", 16: "RT_VERSION", 17: "RT_DLGINCLUDE",
19: "RT_PLUGPLAY", 20: "RT_VXD", 21: "RT_ANICURSOR", 22: "RT_ANIICON", 23: "RT_HTML", 24: "RT_MANIFEST",
}
if name, ok := names[id]; ok {
return fmt.Sprintf("%s(%d)", name, id)
}
return fmt.Sprintf("#%d", id)
}
func peLanguageName(id uint32) string {
switch id {
case 0:
return "LANG_NEUTRAL"
case 0x0409:
return "en-US"
case 0x0809:
return "en-GB"
}
return fmt.Sprintf("lang-0x%04x", id)
}
func decodePEStringTable(buf []byte, off int64) []*Field {
out := []*Field{}
pos := 0
for i := 0; i < 16 && pos+2 <= len(buf); i++ {
n := int(binary.LittleEndian.Uint16(buf[pos : pos+2]))
pos += 2
if pos+n*2 > len(buf) {
break
}
if n > 0 {
out = append(out, &Field{Name: fmt.Sprintf("string[%d]", i), Type: "resource_string", Offset: off + int64(pos), Length: int64(n * 2), Value: decodeUTF16LE(buf[pos : pos+n*2])})
}
pos += n * 2
}
return out
}
func decodePEVersionResource(buf []byte, off int64) []*Field {
if len(buf) < 6 {
return nil
}
key, keyEnd := readPEVersionKey(buf, 6)
out := []*Field{{Name: "version_info", Type: "version_info", Offset: off, Length: int64(len(buf)), Value: fmt.Sprintf("key=%s length=%d valueLength=%d type=%d", key, binary.LittleEndian.Uint16(buf[0:2]), binary.LittleEndian.Uint16(buf[2:4]), binary.LittleEndian.Uint16(buf[4:6]))}}
valueOff := align4(keyEnd)
if key == "VS_VERSION_INFO" && valueOff+52 <= len(buf) {
fixed := buf[valueOff : valueOff+52]
if binary.LittleEndian.Uint32(fixed[0:4]) == 0xfeef04bd {
fileMS := binary.LittleEndian.Uint32(fixed[8:12])
fileLS := binary.LittleEndian.Uint32(fixed[12:16])
prodMS := binary.LittleEndian.Uint32(fixed[16:20])
prodLS := binary.LittleEndian.Uint32(fixed[20:24])
out = append(out, &Field{Name: "fixed_file_info", Type: "version_fixed", Offset: off + int64(valueOff), Length: 52, Value: fmt.Sprintf("file=%d.%d.%d.%d product=%d.%d.%d.%d flags=0x%x type=0x%x subtype=0x%x",
fileMS>>16, fileMS&0xffff, fileLS>>16, fileLS&0xffff,
prodMS>>16, prodMS&0xffff, prodLS>>16, prodLS&0xffff,
binary.LittleEndian.Uint32(fixed[28:32]), binary.LittleEndian.Uint32(fixed[36:40]), binary.LittleEndian.Uint32(fixed[40:44]))})
}
}
if strings.Contains(string(buf), "StringFileInfo") || strings.Contains(string(buf), "VarFileInfo") {
text := decodeUTF16LEASCII(buf)
for _, label := range []string{"CompanyName", "FileDescription", "FileVersion", "ProductName", "ProductVersion", "OriginalFilename"} {
if strings.Contains(text, label) {
out = append(out, &Field{Name: label, Type: "version_string_hint", Offset: off, Length: int64(len(buf)), Value: label})
}
}
}
return out
}
func readPEVersionKey(buf []byte, pos int) (string, int) {
start := pos
for pos+1 < len(buf) {
if buf[pos] == 0 && buf[pos+1] == 0 {
return decodeUTF16LE(buf[start:pos]), pos + 2
}
pos += 2
}
return "", pos
}
func align4(n int) int {
return (n + 3) &^ 3
}
func peResourceTextPreview(buf []byte, codePage uint32) string {
if len(buf) == 0 {
return ""
}
if len(buf) >= 2 {
if buf[0] == 0xff && buf[1] == 0xfe {
return cleanResourcePreview(decodeUTF16LE(buf[2:]))
}
if buf[0] == 0xfe && buf[1] == 0xff {
return cleanResourcePreview(decodeUTF16BE(buf[2:]))
}
}
if codePage == 1200 {
return cleanResourcePreview(decodeUTF16LE(buf))
}
if utf8.Valid(buf) {
return cleanResourcePreview(string(buf))
}
return ""
}
func cleanResourcePreview(s string) string {
s = strings.TrimSpace(strings.ReplaceAll(s, "\x00", ""))
if len(s) > 512 {
s = s[:512] + "..."
}
if strings.IndexFunc(s, func(r rune) bool { return r < 0x09 }) >= 0 {
return ""
}
return s
}
func decodeUTF16LE(buf []byte) string {
u := make([]uint16, 0, len(buf)/2)
for i := 0; i+1 < len(buf); i += 2 {
u = append(u, binary.LittleEndian.Uint16(buf[i:i+2]))
}
return string(utf16.Decode(u))
}
func decodeUTF16BE(buf []byte) string {
u := make([]uint16, 0, len(buf)/2)
for i := 0; i+1 < len(buf); i += 2 {
u = append(u, binary.BigEndian.Uint16(buf[i:i+2]))
}
return string(utf16.Decode(u))
}
func decodeUTF16LEASCII(buf []byte) string {
var b strings.Builder
for i := 0; i+1 < len(buf); i += 2 {
r := binary.LittleEndian.Uint16(buf[i : i+2])
if r >= 0x20 && r < 0x7f {
b.WriteByte(byte(r))
} else {
b.WriteByte(' ')
}
}
return b.String()
}
func parsePEImportThunks(d *document.Document, sections []peSectionInfo, oft, firstThunk uint32, is64 bool) []*Field {
thunkRVA := oft
if thunkRVA == 0 {
thunkRVA = firstThunk
}
thunkOff, ok := peRVAToOffset(sections, thunkRVA)
if !ok {
return nil
}
width := int64(4)
ordinalMask := uint64(0x80000000)
if is64 {
width = 8
ordinalMask = 0x8000000000000000
}
out := []*Field{}
for i := 0; i < 4096; i++ {
at := int64(thunkOff) + int64(i)*width
buf, err := d.Range(at, width)
if err != nil || int64(len(buf)) < width {
break
}
var thunk uint64
if is64 {
thunk = binary.LittleEndian.Uint64(buf)
} else {
thunk = uint64(binary.LittleEndian.Uint32(buf))
}
if thunk == 0 {
break
}
value := ""
if thunk&ordinalMask != 0 {
value = fmt.Sprintf("ordinal=%d", thunk&0xffff)
} else if nameOff, ok := peRVAToOffset(sections, uint32(thunk)); ok {
name := readImportByName(d, int64(nameOff))
if name != "" {
value = "name=" + name
} else {
value = fmt.Sprintf("name_rva=0x%x", thunk)
}
} else {
value = fmt.Sprintf("thunk=0x%x", thunk)
}
out = append(out, &Field{
Name: fmt.Sprintf("thunk[%d]", i),
Type: "import_thunk",
Offset: at,
Length: width,
Value: value,
})
}
return out
}
func readImportByName(d *document.Document, off int64) string {
buf, err := d.Range(off, 258)
if err != nil || len(buf) < 3 {
return ""
}
return readCStringFrom(buf[2:])
}
func peRVAToOffset(sections []peSectionInfo, rva uint32) (uint32, bool) {
for _, s := range sections {
span := s.VirtualSize
if s.RawSize > span {
span = s.RawSize
}
if span == 0 {
continue
}
if rva >= s.VirtualAddress && rva < s.VirtualAddress+span {
return s.RawOffset + (rva - s.VirtualAddress), true
}
}
return 0, false
}
func readCString(d *document.Document, off int64, max int64) string {
buf, err := d.Range(off, max)
if err != nil {
return ""
}
return readCStringFrom(buf)
}
func readCStringFrom(buf []byte) string {
for i, b := range buf {
if b == 0 {
return string(buf[:i])
}
}
return string(buf)
}
func peSubsystem(v uint16) string {
switch v {
case 1:
return "native"
case 2:
return "windows_gui"
case 3:
return "windows_cui"
case 7:
return "posix_cui"
case 9:
return "windows_ce_gui"
case 10:
return "efi_application"
case 11:
return "efi_boot_service_driver"
case 12:
return "efi_runtime_driver"
}
return fmt.Sprintf("0x%x", v)
}
func peMachine(m uint16) string {
switch m {
case 0x14c:
return "i386"
case 0x8664:
return "x86_64"
case 0x1c0:
return "ARM"
case 0xaa64:
return "ARM64"
case 0x200:
return "IA64"
}
return "?"
}
func trimZero(b []byte) []byte {
for i, c := range b {
if c == 0 {
return b[:i]
}
}
return b
}
// ParseRIFF walks RIFF chunks.
func ParseRIFF(d *document.Document) (*Field, error) {
size := d.Size()
if size < 12 {
return nil, errors.New("too short")
}
hdr, err := d.Range(0, 12)
if err != nil {
return nil, err
}
total := int64(binary.LittleEndian.Uint32(hdr[4:8]))
form := string(hdr[8:12])
end := total + 8
if end > size {
end = size
}
root := &Field{
Name: "RIFF", Type: "container", Offset: 0, Length: total + 8,
Value: fmt.Sprintf("form=%s (%s)", form, riffFormName(form)),
}
root.Children = riffChunks(d, 12, end, 0)
return root, nil
}
// riffChunks walks the chunks in [off,end), recursing into LIST containers and
// decoding the common WAV/AVI chunks.
func riffChunks(d *document.Document, off, end int64, depth int) []*Field {
var out []*Field
for off+8 <= end && depth < 16 {
ch, err := d.Range(off, 8)
if err != nil || len(ch) < 8 {
break
}
id := string(ch[0:4])
ln := int64(binary.LittleEndian.Uint32(ch[4:8]))
body := off + 8
chunkEnd := body + ln
if chunkEnd > end {
chunkEnd = end
}
f := &Field{Name: riffChunkLabel(id), Type: "chunk", Offset: off, Length: 8 + ln, Value: fmt.Sprintf("length=%d", ln)}
switch id {
case "LIST", "RIFF":
if ln >= 4 {
lb, _ := d.Range(body, 4)
lt := string(lb)
f.Type = "list"
f.Value = fmt.Sprintf("type=%s (%s)", lt, riffFormName(lt))
f.Children = riffChunks(d, body+4, chunkEnd, depth+1)
}
case "fmt ":
if fb, e := d.Range(body, min64(16, ln)); e == nil && len(fb) >= 16 {
f.Children = []*Field{
{Name: "audio_format", Type: "u16le", Offset: body, Length: 2, Value: wavFormat(binary.LittleEndian.Uint16(fb[0:2]))},
{Name: "channels", Type: "u16le", Offset: body + 2, Length: 2, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint16(fb[2:4]))},
{Name: "sample_rate", Type: "u32le", Offset: body + 4, Length: 4, Value: fmt.Sprintf("%d Hz", binary.LittleEndian.Uint32(fb[4:8]))},
{Name: "byte_rate", Type: "u32le", Offset: body + 8, Length: 4, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint32(fb[8:12]))},
{Name: "block_align", Type: "u16le", Offset: body + 12, Length: 2, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint16(fb[12:14]))},
{Name: "bits_per_sample", Type: "u16le", Offset: body + 14, Length: 2, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint16(fb[14:16]))},
}
}
case "avih":
if ab, e := d.Range(body, min64(40, ln)); e == nil && len(ab) >= 16 {
f.Children = []*Field{
{Name: "microsec_per_frame", Type: "u32le", Offset: body, Length: 4, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint32(ab[0:4]))},
{Name: "max_bytes_per_sec", Type: "u32le", Offset: body + 4, Length: 4, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint32(ab[4:8]))},
{Name: "total_frames", Type: "u32le", Offset: body + 16, Length: 4, Value: fmt.Sprintf("%d", binary.LittleEndian.Uint32(ab[16:20]))},
}
}
}
out = append(out, f)
off = chunkEnd
if ln%2 != 0 {
off++ // chunks are word-aligned
}
}
return out
}
func riffFormName(f string) string {
switch f {
case "WAVE":
return "audio/wav"
case "AVI ":
return "video/avi"
case "WEBP":
return "image/webp"
case "ANI ":
return "animated cursor"
case "RMID":
return "MIDI"
case "INFO":
return "metadata"
case "movi":
return "movie data"
case "hdrl":
return "header list"
case "strl":
return "stream list"
default:
return "?"
}
}
func riffChunkLabel(id string) string {
return strings.TrimRight(id, " ")
}
func wavFormat(f uint16) string {
names := map[uint16]string{1: "PCM", 2: "ADPCM", 3: "IEEE float", 6: "A-law", 7: "mu-law", 0xfffe: "extensible"}
if n, ok := names[f]; ok {
return fmt.Sprintf("%d (%s)", f, n)
}
return fmt.Sprintf("%d", f)
}
func min64(a, b int64) int64 {
if a < b {
return a
}
return b
}