// Additional structure parsers that round out the structures panel into a // forensic-grade set: gzip, tar, SQLite, TIFF, pcap/pcapng and the OLE/CFB // compound-file container used by legacy Office documents and MSI installers. // // Each parser follows the same contract as the parsers in formats.go: it // returns a *Field tree anchored at absolute document offsets, never panics on // truncated input, and bounds every loop so a hostile file cannot wedge the UI. package parsers import ( "encoding/binary" "errors" "fmt" "strconv" "strings" "time" "github.com/andydixon/nexus/internal/leviathan/document" ) // leaf builds a terminal Field with no children. func leaf(name, typ string, off, length int64, value string) *Field { return &Field{Name: name, Type: typ, Offset: off, Length: length, Value: value} } // ---------- gzip (RFC 1952) ---------- // ParseGZIP decodes the gzip member header, optional name/comment/extra fields // and the trailing CRC-32 / ISIZE footer. func ParseGZIP(d *document.Document) (*Field, error) { size := d.Size() hdr, err := d.Range(0, min64(10, size)) if err != nil || len(hdr) < 10 { return nil, errors.New("gzip too short") } if hdr[0] != 0x1f || hdr[1] != 0x8b { return nil, errors.New("not gzip") } root := &Field{Name: "GZIP", Type: "container", Offset: 0, Length: size} method := hdr[2] methodName := "deflate" if method != 8 { methodName = fmt.Sprintf("unknown(%d)", method) } flg := hdr[3] mtime := binary.LittleEndian.Uint32(hdr[4:8]) root.Children = append(root.Children, leaf("id", "bytes", 0, 2, "\\x1f\\x8b"), leaf("method", "u8", 2, 1, methodName), leaf("flags", "u8", 3, 1, gzipFlags(flg)), leaf("mtime", "u32le", 4, 4, gzipTime(mtime)), leaf("extra_flags", "u8", 8, 1, fmt.Sprintf("%d", hdr[8])), leaf("os", "u8", 9, 1, gzipOS(hdr[9])), ) off := int64(10) // FEXTRA if flg&0x04 != 0 && off+2 <= size { xb, _ := d.Range(off, 2) xlen := int64(binary.LittleEndian.Uint16(xb)) root.Children = append(root.Children, leaf("extra", "bytes", off, 2+xlen, fmt.Sprintf("%d bytes", xlen))) off += 2 + xlen } // FNAME if flg&0x08 != 0 { s, n := readCStringField(d, off, size) root.Children = append(root.Children, leaf("name", "asciiz", off, n, s)) off += n } // FCOMMENT if flg&0x10 != 0 { s, n := readCStringField(d, off, size) root.Children = append(root.Children, leaf("comment", "asciiz", off, n, s)) off += n } // FHCRC if flg&0x02 != 0 && off+2 <= size { hb, _ := d.Range(off, 2) root.Children = append(root.Children, leaf("header_crc16", "u16le", off, 2, fmt.Sprintf("0x%04x", binary.LittleEndian.Uint16(hb)))) off += 2 } if size >= off+8 { root.Children = append(root.Children, leaf("compressed_data", "bytes", off, size-off-8, fmt.Sprintf("%d bytes", size-off-8))) ft, _ := d.Range(size-8, 8) root.Children = append(root.Children, leaf("crc32", "u32le", size-8, 4, fmt.Sprintf("0x%08x", binary.LittleEndian.Uint32(ft[0:4]))), leaf("isize", "u32le", size-4, 4, fmt.Sprintf("%d bytes (mod 2^32)", binary.LittleEndian.Uint32(ft[4:8]))), ) } return root, nil } func gzipFlags(f byte) string { var on []string for _, b := range []struct { mask byte name string }{{0x01, "TEXT"}, {0x02, "HCRC"}, {0x04, "EXTRA"}, {0x08, "NAME"}, {0x10, "COMMENT"}} { if f&b.mask != 0 { on = append(on, b.name) } } if len(on) == 0 { return fmt.Sprintf("0x%02x", f) } return fmt.Sprintf("0x%02x (%s)", f, strings.Join(on, "|")) } func gzipOS(b byte) string { names := map[byte]string{0: "FAT", 1: "Amiga", 2: "VMS", 3: "Unix", 6: "HPFS", 7: "Macintosh", 11: "NTFS", 255: "unknown"} if n, ok := names[b]; ok { return fmt.Sprintf("%d (%s)", b, n) } return fmt.Sprintf("%d", b) } func gzipTime(t uint32) string { if t == 0 { return "0 (no timestamp)" } return fmt.Sprintf("%d (%s)", t, time.Unix(int64(t), 0).UTC().Format("2006-01-02 15:04:05 UTC")) } // ---------- tar (POSIX ustar) ---------- // ParseTAR walks the 512-byte record headers of a ustar/GNU tar archive. func ParseTAR(d *document.Document) (*Field, error) { size := d.Size() root := &Field{Name: "TAR", Type: "container", Offset: 0} off := int64(0) entries := 0 for off+512 <= size && entries < 10000 { blk, err := d.Range(off, 512) if err != nil { break } if allZero(blk) { root.Children = append(root.Children, leaf("end_of_archive", "bytes", off, 512, "zero block")) off += 512 break } name := trimZeroStr(blk[0:100]) prefix := trimZeroStr(blk[345:500]) full := name if prefix != "" { full = prefix + "/" + name } fsize := octal(blk[124:136]) mtime := octal(blk[136:148]) typ := blk[156] e := &Field{ Name: full, Type: "entry", Offset: off, Length: 512 + roundUp(fsize, 512), Value: fmt.Sprintf("%s size=%d", tarType(typ), fsize), Children: []*Field{ leaf("name", "asciiz", off, 100, name), leaf("mode", "octal", off+100, 8, fmt.Sprintf("0%o", octal(blk[100:108]))), leaf("uid", "octal", off+108, 8, fmt.Sprintf("%d", octal(blk[108:116]))), leaf("gid", "octal", off+116, 8, fmt.Sprintf("%d", octal(blk[116:124]))), leaf("size", "octal", off+124, 12, fmt.Sprintf("%d", fsize)), leaf("mtime", "octal", off+136, 12, tarTime(mtime)), leaf("typeflag", "u8", off+156, 1, tarType(typ)), leaf("magic", "ascii", off+257, 8, trimZeroStr(blk[257:265])), leaf("uname", "asciiz", off+265, 32, trimZeroStr(blk[265:297])), leaf("gname", "asciiz", off+297, 32, trimZeroStr(blk[297:329])), }, } if prefix != "" { e.Children = append(e.Children, leaf("prefix", "asciiz", off+345, 155, prefix)) } root.Children = append(root.Children, e) off += 512 + roundUp(fsize, 512) entries++ } root.Length = off if len(root.Children) == 0 { return nil, errors.New("no tar entries") } return root, nil } func tarType(t byte) string { switch t { case 0, '0': return "file" case '1': return "hardlink" case '2': return "symlink" case '3': return "char-device" case '4': return "block-device" case '5': return "directory" case '6': return "fifo" case '7': return "contiguous" case 'x': return "pax-header" case 'g': return "pax-global" case 'L': return "gnu-longname" case 'K': return "gnu-longlink" default: return fmt.Sprintf("type-%q", t) } } func tarTime(t int64) string { if t == 0 { return "0" } return fmt.Sprintf("%d (%s)", t, time.Unix(t, 0).UTC().Format("2006-01-02 15:04:05 UTC")) } // ---------- SQLite 3 ---------- // ParseSQLite decodes the 100-byte SQLite database header. func ParseSQLite(d *document.Document) (*Field, error) { hdr, err := d.Range(0, min64(100, d.Size())) if err != nil || len(hdr) < 100 { return nil, errors.New("sqlite header too short") } if string(hdr[0:16]) != "SQLite format 3\x00" { return nil, errors.New("not a SQLite database") } be := binary.BigEndian pageSize := int64(be.Uint16(hdr[16:18])) if pageSize == 1 { pageSize = 65536 } enc := map[uint32]string{1: "UTF-8", 2: "UTF-16le", 3: "UTF-16be"}[be.Uint32(hdr[56:60])] if enc == "" { enc = "unknown" } root := &Field{ Name: "SQLite", Type: "container", Offset: 0, Length: d.Size(), Value: fmt.Sprintf("page=%d pages=%d %s", pageSize, be.Uint32(hdr[28:32]), enc), Children: []*Field{ leaf("magic", "ascii", 0, 16, "SQLite format 3"), leaf("page_size", "u16be", 16, 2, fmt.Sprintf("%d", pageSize)), leaf("write_version", "u8", 18, 1, sqliteFmtVer(hdr[18])), leaf("read_version", "u8", 19, 1, sqliteFmtVer(hdr[19])), leaf("reserved_space", "u8", 20, 1, fmt.Sprintf("%d", hdr[20])), leaf("change_counter", "u32be", 24, 4, fmt.Sprintf("%d", be.Uint32(hdr[24:28]))), leaf("page_count", "u32be", 28, 4, fmt.Sprintf("%d", be.Uint32(hdr[28:32]))), leaf("freelist_head", "u32be", 32, 4, fmt.Sprintf("%d", be.Uint32(hdr[32:36]))), leaf("freelist_pages", "u32be", 36, 4, fmt.Sprintf("%d", be.Uint32(hdr[36:40]))), leaf("schema_cookie", "u32be", 40, 4, fmt.Sprintf("%d", be.Uint32(hdr[40:44]))), leaf("schema_format", "u32be", 44, 4, fmt.Sprintf("%d", be.Uint32(hdr[44:48]))), leaf("text_encoding", "u32be", 56, 4, enc), leaf("user_version", "u32be", 60, 4, fmt.Sprintf("%d", be.Uint32(hdr[60:64]))), leaf("vacuum_mode", "u32be", 64, 4, fmt.Sprintf("%d", be.Uint32(hdr[64:68]))), leaf("application_id", "u32be", 68, 4, fmt.Sprintf("0x%08x", be.Uint32(hdr[68:72]))), leaf("version_valid_for", "u32be", 92, 4, fmt.Sprintf("%d", be.Uint32(hdr[92:96]))), leaf("sqlite_version", "u32be", 96, 4, sqliteVersion(be.Uint32(hdr[96:100]))), }, } return root, nil } func sqliteFmtVer(b byte) string { switch b { case 1: return "1 (legacy/rollback)" case 2: return "2 (WAL)" default: return fmt.Sprintf("%d", b) } } func sqliteVersion(v uint32) string { // SQLITE_VERSION_NUMBER = major*1_000_000 + minor*1000 + patch. return fmt.Sprintf("%d (%d.%d.%d)", v, v/1000000, (v/1000)%1000, v%1000) } // ---------- TIFF ---------- // ParseTIFF decodes the TIFF header and the entries of each image file // directory (IFD), following the next-IFD chain. func ParseTIFF(d *document.Document) (*Field, error) { size := d.Size() hdr, err := d.Range(0, min64(8, size)) if err != nil || len(hdr) < 8 { return nil, errors.New("tiff too short") } var bo binary.ByteOrder switch string(hdr[0:2]) { case "II": bo = binary.LittleEndian case "MM": bo = binary.BigEndian default: return nil, errors.New("not TIFF") } if bo.Uint16(hdr[2:4]) != 42 { return nil, errors.New("bad TIFF magic") } order := "little-endian" if string(hdr[0:2]) == "MM" { order = "big-endian" } root := &Field{ Name: "TIFF", Type: "container", Offset: 0, Length: size, Value: order, Children: []*Field{ leaf("byte_order", "ascii", 0, 2, order), leaf("magic", "u16", 2, 2, "42"), leaf("first_ifd", "u32", 4, 4, fmt.Sprintf("0x%x", bo.Uint32(hdr[4:8]))), }, } ifdOff := int64(bo.Uint32(hdr[4:8])) for n := 0; ifdOff > 0 && ifdOff+2 <= size && n < 64; n++ { cb, err := d.Range(ifdOff, 2) if err != nil { break } count := int64(bo.Uint16(cb)) ifd := &Field{ Name: fmt.Sprintf("IFD[%d]", n), Type: "directory", Offset: ifdOff, Length: 2 + count*12 + 4, Value: fmt.Sprintf("%d entries", count), } for i := int64(0); i < count && i < 4096; i++ { eoff := ifdOff + 2 + i*12 eb, err := d.Range(eoff, 12) if err != nil || len(eb) < 12 { break } tag := bo.Uint16(eb[0:2]) typ := bo.Uint16(eb[2:4]) cnt := bo.Uint32(eb[4:8]) val := bo.Uint32(eb[8:12]) ifd.Children = append(ifd.Children, leaf( fmt.Sprintf("tag 0x%04x %s", tag, tiffTag(tag)), "ifd_entry", eoff, 12, fmt.Sprintf("type=%s count=%d value=%d", tiffType(typ), cnt, val))) } root.Children = append(root.Children, ifd) nb, err := d.Range(ifdOff+2+count*12, 4) if err != nil || len(nb) < 4 { break } ifdOff = int64(bo.Uint32(nb)) } return root, nil } func tiffType(t uint16) string { names := map[uint16]string{1: "BYTE", 2: "ASCII", 3: "SHORT", 4: "LONG", 5: "RATIONAL", 6: "SBYTE", 7: "UNDEFINED", 8: "SSHORT", 9: "SLONG", 10: "SRATIONAL", 11: "FLOAT", 12: "DOUBLE"} if n, ok := names[t]; ok { return n } return fmt.Sprintf("type%d", t) } func tiffTag(t uint16) string { names := map[uint16]string{ 256: "ImageWidth", 257: "ImageLength", 258: "BitsPerSample", 259: "Compression", 262: "PhotometricInterpretation", 273: "StripOffsets", 277: "SamplesPerPixel", 278: "RowsPerStrip", 279: "StripByteCounts", 282: "XResolution", 283: "YResolution", 296: "ResolutionUnit", 305: "Software", 306: "DateTime", 315: "Artist", 271: "Make", 272: "Model", 274: "Orientation", 34665: "ExifIFD", 34853: "GPSIFD", } if n, ok := names[t]; ok { return n } return "" } // ---------- pcap / pcapng ---------- // ParsePCAP decodes a classic libpcap global header and walks packet records. func ParsePCAP(d *document.Document) (*Field, error) { size := d.Size() hdr, err := d.Range(0, min64(24, size)) if err != nil || len(hdr) < 24 { return nil, errors.New("pcap too short") } magic := binary.LittleEndian.Uint32(hdr[0:4]) var bo binary.ByteOrder nano := false switch magic { case 0xa1b2c3d4: bo = binary.LittleEndian case 0xd4c3b2a1: bo = binary.BigEndian case 0xa1b23c4d: bo, nano = binary.LittleEndian, true case 0x4d3cb2a1: bo, nano = binary.BigEndian, true default: return nil, errors.New("not pcap") } res := "microsecond" if nano { res = "nanosecond" } linktype := bo.Uint32(hdr[20:24]) root := &Field{ Name: "PCAP", Type: "container", Offset: 0, Length: size, Value: fmt.Sprintf("%s, %s", res, pcapLinkType(linktype)), Children: []*Field{ leaf("magic", "u32", 0, 4, fmt.Sprintf("0x%08x (%s)", magic, res)), leaf("version", "u16", 4, 4, fmt.Sprintf("%d.%d", bo.Uint16(hdr[4:6]), bo.Uint16(hdr[6:8]))), leaf("thiszone", "i32", 8, 4, fmt.Sprintf("%d", int32(bo.Uint32(hdr[8:12])))), leaf("sigfigs", "u32", 12, 4, fmt.Sprintf("%d", bo.Uint32(hdr[12:16]))), leaf("snaplen", "u32", 16, 4, fmt.Sprintf("%d", bo.Uint32(hdr[16:20]))), leaf("linktype", "u32", 20, 4, fmt.Sprintf("%d (%s)", linktype, pcapLinkType(linktype))), }, } off := int64(24) for n := 0; off+16 <= size && n < 100000; n++ { rb, err := d.Range(off, 16) if err != nil { break } ts := bo.Uint32(rb[0:4]) us := bo.Uint32(rb[4:8]) inclLen := int64(bo.Uint32(rb[8:12])) origLen := bo.Uint32(rb[12:16]) root.Children = append(root.Children, leaf( fmt.Sprintf("packet[%d]", n), "record", off, 16+inclLen, fmt.Sprintf("ts=%d.%06d incl=%d orig=%d", ts, us, inclLen, origLen))) off += 16 + inclLen } return root, nil } // ParsePCAPNG decodes the pcapng block structure (section header + block walk). func ParsePCAPNG(d *document.Document) (*Field, error) { size := d.Size() hb, err := d.Range(0, min64(12, size)) if err != nil || len(hb) < 12 { return nil, errors.New("pcapng too short") } if binary.LittleEndian.Uint32(hb[0:4]) != 0x0a0d0d0a { return nil, errors.New("not pcapng") } // Byte-order magic lives in the SHB body at offset 8. var bo binary.ByteOrder switch binary.LittleEndian.Uint32(hb[8:12]) { case 0x1a2b3c4d: bo = binary.LittleEndian case 0x4d3c2b1a: bo = binary.BigEndian default: return nil, errors.New("bad pcapng byte-order magic") } root := &Field{Name: "PCAPNG", Type: "container", Offset: 0, Length: size} off := int64(0) for n := 0; off+12 <= size && n < 100000; n++ { bh, err := d.Range(off, 12) if err != nil { break } btype := bo.Uint32(bh[0:4]) blen := int64(bo.Uint32(bh[4:8])) if blen < 12 || off+blen > size { break } root.Children = append(root.Children, leaf( fmt.Sprintf("block[%d] %s", n, pcapngBlock(btype)), "block", off, blen, fmt.Sprintf("type=0x%08x length=%d", btype, blen))) off += blen } return root, nil } func pcapngBlock(t uint32) string { switch t { case 0x0a0d0d0a: return "SectionHeader" case 0x00000001: return "InterfaceDescription" case 0x00000003: return "SimplePacket" case 0x00000004: return "NameResolution" case 0x00000005: return "InterfaceStatistics" case 0x00000006: return "EnhancedPacket" default: return "block" } } func pcapLinkType(t uint32) string { names := map[uint32]string{0: "NULL", 1: "ETHERNET", 6: "TOKEN_RING", 9: "PPP", 101: "RAW", 105: "IEEE802_11", 113: "LINUX_SLL", 127: "IEEE802_11_RADIOTAP", 228: "IPV4", 229: "IPV6", 276: "LINUX_SLL2"} if n, ok := names[t]; ok { return n } return fmt.Sprintf("linktype%d", t) } // ---------- OLE / Compound File Binary (MS-CFB) ---------- var cfbMagic = []byte{0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1} // ParseCFB decodes the compound-file header and lists the entries in the first // directory sector (legacy .doc/.xls/.ppt/.msi container). func ParseCFB(d *document.Document) (*Field, error) { size := d.Size() hdr, err := d.Range(0, min64(512, size)) if err != nil || len(hdr) < 76 { return nil, errors.New("cfb too short") } if string(hdr[0:8]) != string(cfbMagic) { return nil, errors.New("not a compound file") } le := binary.LittleEndian major := le.Uint16(hdr[26:28]) sectorShift := le.Uint16(hdr[30:32]) sectorSize := int64(1) << sectorShift firstDir := le.Uint32(hdr[48:52]) root := &Field{ Name: "CFB/OLE", Type: "container", Offset: 0, Length: size, Value: fmt.Sprintf("v%d.%d sector=%d", major, le.Uint16(hdr[24:26]), sectorSize), Children: []*Field{ leaf("signature", "bytes", 0, 8, "D0 CF 11 E0 A1 B1 1A E1"), leaf("minor_version", "u16le", 24, 2, fmt.Sprintf("%d", le.Uint16(hdr[24:26]))), leaf("major_version", "u16le", 26, 2, fmt.Sprintf("%d", major)), leaf("byte_order", "u16le", 28, 2, fmt.Sprintf("0x%04x", le.Uint16(hdr[28:30]))), leaf("sector_shift", "u16le", 30, 2, fmt.Sprintf("%d (%d bytes)", sectorShift, sectorSize)), leaf("mini_sector_shift", "u16le", 32, 2, fmt.Sprintf("%d (%d bytes)", le.Uint16(hdr[32:34]), int64(1)< size { return nil } dir := &Field{Name: "directory", Type: "directory", Offset: off} per := sectorSize / 128 count := int64(0) for i := int64(0); i < per; i++ { eoff := off + i*128 eb, err := d.Range(eoff, 128) if err != nil || len(eb) < 128 { break } nameLen := int64(binary.LittleEndian.Uint16(eb[64:66])) objType := eb[66] if objType == 0 { // unallocated entry continue } name := "" if nameLen >= 2 && nameLen <= 64 { name = decodeUTF16LE(eb[0 : nameLen-2]) } streamSize := binary.LittleEndian.Uint64(eb[120:128]) dir.Children = append(dir.Children, leaf( cfbEntryLabel(name, count), "dir_entry", eoff, 128, fmt.Sprintf("%s size=%d", cfbObjType(objType), streamSize))) count++ } dir.Length = sectorSize dir.Value = fmt.Sprintf("%d entries", count) if count == 0 { return nil } return dir } func cfbEntryLabel(name string, i int64) string { if name == "" { return fmt.Sprintf("entry[%d]", i) } // Strip the control prefix Office uses on some stream names for display. clean := strings.Map(func(r rune) rune { if r < 0x20 { return -1 } return r }, name) if clean == "" { clean = name } return fmt.Sprintf("entry[%d] %s", i, clean) } func cfbObjType(t byte) string { switch t { case 1: return "storage" case 2: return "stream" case 5: return "root" default: return fmt.Sprintf("type%d", t) } } // ---------- shared helpers ---------- func readCStringField(d *document.Document, off, size int64) (string, int64) { var sb strings.Builder n := int64(0) for off+n < size && n < 4096 { b, err := d.At(off + n) if err != nil { break } n++ if b == 0 { break } sb.WriteByte(b) } return sb.String(), n } func allZero(b []byte) bool { for _, x := range b { if x != 0 { return false } } return true } func trimZeroStr(b []byte) string { return strings.TrimRight(string(b), "\x00 ") } // octal parses a space/NUL-padded octal ASCII field (tar number encoding). func octal(b []byte) int64 { s := strings.TrimSpace(strings.Trim(string(b), "\x00")) if s == "" { return 0 } v, err := strconv.ParseInt(s, 8, 64) if err != nil { return 0 } return v } func roundUp(n, to int64) int64 { if n <= 0 { return 0 } return ((n + to - 1) / to) * to }