1144 lines
31 KiB
Go
1144 lines
31 KiB
Go
package binsnoop
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// gzip
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const gzipDescription = "gzip compressed data"
|
|
|
|
func gzipMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x1f, 0x8b, 0x08},
|
|
}
|
|
}
|
|
|
|
func gzipParser(data []byte, offset int) (*SignatureResult, error) {
|
|
const (
|
|
flagCRC = 0b0000_0010
|
|
flagExtra = 0b0000_0100
|
|
flagName = 0b0000_1000
|
|
flagComment = 0b0001_0000
|
|
flagReserved = 0b1110_0000
|
|
|
|
deflateCompression = 8
|
|
crcSize = 2
|
|
nullByteSize = 1
|
|
)
|
|
|
|
knownOSIDs := map[int]string{
|
|
0: "FAT filesystem (MS-DOS, OS/2, NT/Win32",
|
|
1: "Amiga",
|
|
2: "VMS (or OpenVMS)",
|
|
3: "Unix",
|
|
4: "VM/CMS",
|
|
5: "Atari TOS",
|
|
6: "HPFS filesystem (OS/2, NT)",
|
|
7: "Macintosh",
|
|
8: "Z-System",
|
|
9: "CP/M",
|
|
10: "TOPS-20",
|
|
11: "NTFS filesystem (NT)",
|
|
12: "QDOS",
|
|
13: "Acorn RISCOS",
|
|
255: "unknown",
|
|
}
|
|
|
|
gzipHeaderFields := []StructField{
|
|
{"magic", "u16"},
|
|
{"compression_method", "u8"},
|
|
{"flags", "u8"},
|
|
{"timestamp", "u32"},
|
|
{"extra_flags", "u8"},
|
|
{"osid", "u8"},
|
|
}
|
|
|
|
gzipExtraHeaderFields := []StructField{
|
|
{"id", "u16"},
|
|
{"extra_data_len", "u16"},
|
|
}
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("gzip: offset out of bounds")
|
|
}
|
|
|
|
headerData := data[offset:]
|
|
hdr, err := Parse(headerData, gzipHeaderFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gzip: failed to parse header")
|
|
}
|
|
|
|
if hdr["flags"]&flagReserved != 0 {
|
|
return nil, fmt.Errorf("gzip: reserved flags set")
|
|
}
|
|
if hdr["compression_method"] != deflateCompression {
|
|
return nil, fmt.Errorf("gzip: invalid compression method %d", hdr["compression_method"])
|
|
}
|
|
osName, ok := knownOSIDs[hdr["osid"]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("gzip: unknown OS ID %d", hdr["osid"])
|
|
}
|
|
|
|
headerSize := StructSize(gzipHeaderFields)
|
|
|
|
// Handle optional extra data
|
|
if hdr["flags"]&flagExtra != 0 {
|
|
extraHeaderSize := StructSize(gzipExtraHeaderFields)
|
|
extraStart := headerSize
|
|
extraEnd := extraStart + extraHeaderSize
|
|
if extraEnd > len(headerData) {
|
|
return nil, fmt.Errorf("gzip: not enough data for extra header")
|
|
}
|
|
extraHdr, err := Parse(headerData[extraStart:], gzipExtraHeaderFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gzip: failed to parse extra header")
|
|
}
|
|
headerSize += extraHeaderSize + extraHdr["extra_data_len"]
|
|
}
|
|
|
|
// Handle optional original file name (NULL-terminated)
|
|
originalName := ""
|
|
if hdr["flags"]&flagName != 0 {
|
|
if headerSize >= len(headerData) {
|
|
return nil, fmt.Errorf("gzip: not enough data for original name")
|
|
}
|
|
originalName = GetCString(headerData[headerSize:])
|
|
headerSize += len(originalName) + nullByteSize
|
|
}
|
|
|
|
// Handle optional comment (NULL-terminated)
|
|
if hdr["flags"]&flagComment != 0 {
|
|
if headerSize >= len(headerData) {
|
|
return nil, fmt.Errorf("gzip: not enough data for comment")
|
|
}
|
|
comment := GetCString(headerData[headerSize:])
|
|
headerSize += len(comment) + nullByteSize
|
|
}
|
|
|
|
// Handle optional header CRC
|
|
if hdr["flags"]&flagCRC != 0 {
|
|
headerSize += crcSize
|
|
}
|
|
|
|
if len(headerData) < headerSize {
|
|
return nil, fmt.Errorf("gzip: data shorter than header")
|
|
}
|
|
|
|
originalFileNameText := ""
|
|
if originalName != "" {
|
|
originalFileNameText = fmt.Sprintf(" original file name: \"%s\",", originalName)
|
|
}
|
|
|
|
description := fmt.Sprintf("%s,%s operating system: %s, timestamp: %s",
|
|
gzipDescription,
|
|
originalFileNameText,
|
|
osName,
|
|
EpochToString(uint32(hdr["timestamp"])),
|
|
)
|
|
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: 0, // size unknown without decompression
|
|
Description: description,
|
|
Confidence: ConfidenceMedium,
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// zip
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const zipDescription = "ZIP archive"
|
|
|
|
func zipMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x50, 0x4b, 0x03, 0x04}, // PK\x03\x04
|
|
}
|
|
}
|
|
|
|
func zipParser(data []byte, offset int) (*SignatureResult, error) {
|
|
return parseZipLike(data, offset, zipDescription)
|
|
}
|
|
|
|
// parseZipLike is shared between zip and dahua_zip parsers.
|
|
func parseZipLike(data []byte, offset int, descPrefix string) (*SignatureResult, error) {
|
|
const (
|
|
unusedFlagsMask = 0b11010111_10000000
|
|
compressionEncrypted = 99
|
|
)
|
|
|
|
zipLocalFileFields := []StructField{
|
|
{"magic", "u32"},
|
|
{"version", "u16"},
|
|
{"flags", "u16"},
|
|
{"compression", "u16"},
|
|
{"modification_time", "u16"},
|
|
{"modification_date", "u16"},
|
|
{"crc", "u32"},
|
|
{"compressed_size", "u32"},
|
|
{"uncompressed_size", "u32"},
|
|
{"file_name_len", "u16"},
|
|
{"extra_field_len", "u16"},
|
|
}
|
|
|
|
allowedCompressionMethods := map[int]bool{
|
|
0: true, 1: true, 2: true, 3: true, 4: true, 5: true, 6: true,
|
|
8: true, 9: true, 10: true, 12: true, 14: true, 18: true,
|
|
19: true, 20: true, 93: true, 94: true, 95: true, 96: true,
|
|
97: true, 98: true, compressionEncrypted: true,
|
|
}
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("zip: offset out of bounds")
|
|
}
|
|
|
|
zipData := data[offset:]
|
|
hdr, err := Parse(zipData, zipLocalFileFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("zip: failed to parse local file header")
|
|
}
|
|
|
|
if hdr["flags"]&unusedFlagsMask != 0 {
|
|
return nil, fmt.Errorf("zip: unused flags set")
|
|
}
|
|
if !allowedCompressionMethods[hdr["compression"]] {
|
|
return nil, fmt.Errorf("zip: unsupported compression method %d", hdr["compression"])
|
|
}
|
|
|
|
versionMajor := hdr["version"] / 10
|
|
versionMinor := hdr["version"] % 10
|
|
|
|
localHeaderSize := StructSize(zipLocalFileFields) + hdr["file_name_len"] + hdr["extra_field_len"]
|
|
dataSize := hdr["compressed_size"]
|
|
if dataSize == 0 {
|
|
dataSize = hdr["uncompressed_size"]
|
|
}
|
|
firstEntryTotalSize := localHeaderSize + dataSize
|
|
|
|
// Try to find the end-of-central-directory header
|
|
eocdMagic := []byte("PK\x05\x06\x00\x00\x00\x00")
|
|
eocdFields := []StructField{
|
|
{"magic", "u32"},
|
|
{"disk_number", "u16"},
|
|
{"central_directory_disk_number", "u16"},
|
|
{"central_directory_disk_entries", "u16"},
|
|
{"central_directory_total_entries", "u16"},
|
|
{"central_directory_size", "u32"},
|
|
{"central_directory_offset", "u32"},
|
|
{"comment_length", "u16"},
|
|
}
|
|
|
|
totalSize := 0
|
|
fileCount := 0
|
|
missingEOCD := false
|
|
|
|
// Search for EOCD in data after offset
|
|
searchData := data[offset:]
|
|
eocdIdx := -1
|
|
for i := 0; i+len(eocdMagic) <= len(searchData); i++ {
|
|
if bytes.Equal(searchData[i:i+len(eocdMagic)], eocdMagic) {
|
|
eocdData := searchData[i:]
|
|
eocdHdr, err := Parse(eocdData, eocdFields, "little")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if eocdHdr["central_directory_disk_entries"] == eocdHdr["central_directory_total_entries"] &&
|
|
eocdHdr["central_directory_total_entries"] > 0 {
|
|
eocdSize := StructSize(eocdFields) + eocdHdr["comment_length"]
|
|
eocdIdx = i
|
|
totalSize = eocdIdx + eocdSize
|
|
fileCount = eocdHdr["central_directory_total_entries"]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if eocdIdx < 0 {
|
|
// No EOCD found — walk local file entries
|
|
missingEOCD = true
|
|
availableData := len(data) - offset
|
|
var previousOffset *int
|
|
nextOffset := offset + firstEntryTotalSize
|
|
|
|
for IsOffsetSafe(availableData, nextOffset, previousOffset) {
|
|
entryHdr, err := Parse(data[nextOffset:], zipLocalFileFields, "little")
|
|
if err != nil {
|
|
totalSize = nextOffset - offset
|
|
break
|
|
}
|
|
if entryHdr["flags"]&unusedFlagsMask != 0 || !allowedCompressionMethods[entryHdr["compression"]] {
|
|
totalSize = nextOffset - offset
|
|
break
|
|
}
|
|
entryHeaderSize := StructSize(zipLocalFileFields) + entryHdr["file_name_len"] + entryHdr["extra_field_len"]
|
|
entryDataSize := entryHdr["compressed_size"]
|
|
if entryDataSize == 0 {
|
|
entryDataSize = entryHdr["uncompressed_size"]
|
|
}
|
|
prev := nextOffset
|
|
previousOffset = &prev
|
|
nextOffset += entryHeaderSize + entryDataSize
|
|
}
|
|
if totalSize == 0 {
|
|
totalSize = nextOffset - offset
|
|
}
|
|
}
|
|
|
|
if totalSize <= 0 {
|
|
return nil, fmt.Errorf("zip: could not determine archive size")
|
|
}
|
|
|
|
var description string
|
|
if missingEOCD {
|
|
description = fmt.Sprintf("%s, version: %d.%d, missing end-of-central-directory header, total size: %d bytes",
|
|
descPrefix, versionMajor, versionMinor, totalSize)
|
|
} else {
|
|
description = fmt.Sprintf("%s, version: %d.%d, file count: %d, total size: %d bytes",
|
|
descPrefix, versionMajor, versionMinor, fileCount, totalSize)
|
|
}
|
|
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: totalSize,
|
|
Description: description,
|
|
Confidence: ConfidenceHigh,
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// tarball
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const tarballDescription = "POSIX tar archive"
|
|
|
|
const (
|
|
tarballBlockSize = 512
|
|
tarballMagicOffset = 257
|
|
tarballMagicSize = 5
|
|
tarballSizeOffset = 124
|
|
tarballSizeLen = 11
|
|
tarballMinExpectedHdrs = 10
|
|
tarballChecksumStart = 148
|
|
tarballChecksumEnd = 156
|
|
)
|
|
|
|
var tarballUniversalMagic = []byte("ustar")
|
|
|
|
func tarballMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x75, 0x73, 0x74, 0x61, 0x72, 0x00}, // ustar\x00
|
|
{0x75, 0x73, 0x74, 0x61, 0x72, 0x20, 0x20, 0x00}, // ustar\x20\x20\x00
|
|
}
|
|
}
|
|
|
|
func tarballParser(data []byte, offset int) (*SignatureResult, error) {
|
|
// The magic bytes are at offset 257 within a tar header block
|
|
tarballStartOffset := offset - tarballMagicOffset
|
|
|
|
if tarballStartOffset < 0 || tarballStartOffset >= len(data) {
|
|
return nil, fmt.Errorf("tarball: invalid start offset")
|
|
}
|
|
|
|
var tarballTotalSize int
|
|
var validHeaderCount int
|
|
nextHeaderStart := tarballStartOffset
|
|
var previousHeaderStart *int
|
|
availableData := len(data)
|
|
|
|
for IsOffsetSafe(availableData, nextHeaderStart, previousHeaderStart) {
|
|
nextHeaderEnd := nextHeaderStart + tarballBlockSize
|
|
if nextHeaderEnd > len(data) {
|
|
break
|
|
}
|
|
headerBlock := data[nextHeaderStart:nextHeaderEnd]
|
|
|
|
if !tarballHeaderChecksumValid(headerBlock) {
|
|
break
|
|
}
|
|
|
|
validHeaderCount++
|
|
|
|
entrySize, err := tarballEntrySize(headerBlock)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
tarballTotalSize += entrySize
|
|
prev := nextHeaderStart
|
|
previousHeaderStart = &prev
|
|
nextHeaderStart += entrySize
|
|
}
|
|
|
|
if tarballTotalSize < tarballBlockSize {
|
|
return nil, fmt.Errorf("tarball: archive too small")
|
|
}
|
|
|
|
confidence := ConfidenceMedium
|
|
if validHeaderCount >= tarballMinExpectedHdrs {
|
|
confidence = ConfidenceHigh
|
|
}
|
|
|
|
return &SignatureResult{
|
|
Offset: tarballStartOffset,
|
|
Size: tarballTotalSize,
|
|
Description: fmt.Sprintf("%s, file count: %d", tarballDescription, validHeaderCount),
|
|
Confidence: confidence,
|
|
}, nil
|
|
}
|
|
|
|
func tarballHeaderChecksumValid(headerBlock []byte) bool {
|
|
checksumValueBytes := headerBlock[tarballChecksumStart:tarballChecksumEnd]
|
|
reportedChecksum := tarballOctal(checksumValueBytes)
|
|
var sum int
|
|
for i, b := range headerBlock {
|
|
if i >= tarballChecksumStart && i < tarballChecksumEnd {
|
|
sum += 0x20
|
|
} else {
|
|
sum += int(b)
|
|
}
|
|
}
|
|
return sum == reportedChecksum
|
|
}
|
|
|
|
func tarballEntrySize(entryData []byte) (int, error) {
|
|
entryMagic := entryData[tarballMagicOffset : tarballMagicOffset+tarballMagicSize]
|
|
if !bytes.Equal(entryMagic, tarballUniversalMagic) {
|
|
return 0, fmt.Errorf("tarball: invalid entry magic")
|
|
}
|
|
|
|
entrySizeBytes := entryData[tarballSizeOffset : tarballSizeOffset+tarballSizeLen]
|
|
reportedEntrySize := tarballOctal(entrySizeBytes)
|
|
|
|
blockCount := 1 + int(math.Ceil(float64(reportedEntrySize)/float64(tarballBlockSize)))
|
|
return blockCount * tarballBlockSize, nil
|
|
}
|
|
|
|
func tarballOctal(octalStr []byte) int {
|
|
var num int
|
|
for _, c := range octalStr {
|
|
if c < 0x30 || c > 0x39 {
|
|
break
|
|
}
|
|
num = num*8 + int(c) - 0x30
|
|
}
|
|
return num
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7-zip (sevenzip)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const sevenzipDescription = "7-zip archive data"
|
|
|
|
func sevenzipMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c}, // 7z\xbc\xaf\x27\x1c
|
|
}
|
|
}
|
|
|
|
func sevenzipParser(data []byte, offset int) (*SignatureResult, error) {
|
|
const (
|
|
sevenzipCRCStart = 12
|
|
sevenzipHeaderSize = 32
|
|
)
|
|
|
|
sevenzipFields := []StructField{
|
|
{"magic_p1", "u16"},
|
|
{"magic_p2", "u32"},
|
|
{"major_version", "u8"},
|
|
{"minor_version", "u8"},
|
|
{"header_crc", "u32"},
|
|
{"next_header_offset", "u64"},
|
|
{"next_header_size", "u64"},
|
|
{"next_header_crc", "u32"},
|
|
}
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("7zip: offset out of bounds")
|
|
}
|
|
|
|
szData := data[offset:]
|
|
hdr, err := Parse(szData, sevenzipFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("7zip: failed to parse header")
|
|
}
|
|
|
|
// Validate the header CRC (over bytes 12..32)
|
|
if sevenzipHeaderSize > len(szData) {
|
|
return nil, fmt.Errorf("7zip: not enough data for header CRC")
|
|
}
|
|
crcData := szData[sevenzipCRCStart:sevenzipHeaderSize]
|
|
calculatedHeaderCRC := CRC32(crcData)
|
|
if calculatedHeaderCRC != uint32(hdr["header_crc"]) {
|
|
return nil, fmt.Errorf("7zip: header CRC mismatch")
|
|
}
|
|
|
|
// Validate the next_header CRC
|
|
nextCRCStart := offset + sevenzipHeaderSize + hdr["next_header_offset"]
|
|
nextCRCEnd := nextCRCStart + hdr["next_header_size"]
|
|
if nextCRCEnd > len(data) {
|
|
return nil, fmt.Errorf("7zip: not enough data for next header CRC")
|
|
}
|
|
nextCRCData := data[nextCRCStart:nextCRCEnd]
|
|
calculatedNextCRC := CRC32(nextCRCData)
|
|
if int(calculatedNextCRC) != hdr["next_header_crc"] {
|
|
return nil, fmt.Errorf("7zip: next header CRC mismatch")
|
|
}
|
|
|
|
totalSize := sevenzipHeaderSize + hdr["next_header_offset"] + hdr["next_header_size"]
|
|
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: totalSize,
|
|
Description: fmt.Sprintf("%s, version %d.%d, total size: %d bytes", sevenzipDescription, hdr["major_version"], hdr["minor_version"], totalSize),
|
|
Confidence: ConfidenceHigh,
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RAR
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const rarDescription = "RAR archive"
|
|
|
|
func rarMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x52, 0x61, 0x72, 0x21, 0x1a, 0x07}, // Rar!\x1A\x07
|
|
}
|
|
}
|
|
|
|
func rarParser(data []byte, offset int) (*SignatureResult, error) {
|
|
archiveHeaderFields := []StructField{
|
|
{"magic_p1", "u32"},
|
|
{"magic_p2", "u16"},
|
|
{"version", "u8"},
|
|
}
|
|
|
|
// version field 0 => RARv4, 1 => RARv5
|
|
versionMap := map[int]int{0: 4, 1: 5}
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("rar: offset out of bounds")
|
|
}
|
|
|
|
hdr, err := Parse(data[offset:], archiveHeaderFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("rar: failed to parse header")
|
|
}
|
|
|
|
version, ok := versionMap[hdr["version"]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("rar: unknown version %d", hdr["version"])
|
|
}
|
|
|
|
// EOF markers for RARv4 and RARv5
|
|
eofMarkers := map[int][]byte{
|
|
4: {0xc4, 0x3d, 0x7b, 0x00, 0x40, 0x07, 0x00},
|
|
5: {0x1d, 0x77, 0x56, 0x51, 0x03, 0x05, 0x04, 0x00},
|
|
}
|
|
|
|
totalSize := 0
|
|
extraDesc := ""
|
|
|
|
if marker, ok := eofMarkers[version]; ok {
|
|
searchData := data[offset:]
|
|
idx := bytes.Index(searchData, marker)
|
|
if idx >= 0 {
|
|
totalSize = idx + len(marker)
|
|
} else {
|
|
extraDesc = " (failed to locate RAR EOF)"
|
|
}
|
|
}
|
|
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: totalSize,
|
|
Description: fmt.Sprintf("%s, version: %d, total size: %d bytes%s", rarDescription, version, totalSize, extraDesc),
|
|
Confidence: ConfidenceMedium,
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CAB (Microsoft Cabinet)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const cabDescription = "Microsoft Cabinet archive"
|
|
|
|
func cabMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x4d, 0x53, 0x43, 0x46, 0x00, 0x00, 0x00, 0x00}, // MSCF\x00\x00\x00\x00
|
|
}
|
|
}
|
|
|
|
func cabParser(data []byte, offset int) (*SignatureResult, error) {
|
|
const (
|
|
majorVersion = 1
|
|
minorVersion = 3
|
|
cabStructSize = 40
|
|
cabExtraStructSize = 20
|
|
flagExtraDataPresent = 4
|
|
)
|
|
|
|
cabHeaderFields := []StructField{
|
|
{"magic", "u32"},
|
|
{"reserved1", "u32"},
|
|
{"size", "u32"},
|
|
{"reserved2", "u32"},
|
|
{"first_file_offset", "u32"},
|
|
{"reserved3", "u32"},
|
|
{"minor_version", "u8"},
|
|
{"major_version", "u8"},
|
|
{"folder_count", "u16"},
|
|
{"file_count", "u16"},
|
|
{"flags", "u16"},
|
|
{"id", "u16"},
|
|
{"set_number", "u16"},
|
|
{"extra_header_size", "u16"},
|
|
{"cbCFFolder", "u8"},
|
|
{"cbCFData", "u8"},
|
|
}
|
|
|
|
cabExtraHeaderFields := []StructField{
|
|
{"unknown1", "u32"},
|
|
{"data_offset", "u32"},
|
|
{"data_size", "u32"},
|
|
{"unknown2", "u32"},
|
|
{"unknown3", "u32"},
|
|
}
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("cab: offset out of bounds")
|
|
}
|
|
|
|
hdr, err := Parse(data[offset:], cabHeaderFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cab: failed to parse header")
|
|
}
|
|
|
|
// All reserved fields must be 0
|
|
if hdr["reserved1"] != 0 || hdr["reserved2"] != 0 || hdr["reserved3"] != 0 {
|
|
return nil, fmt.Errorf("cab: reserved fields not zero")
|
|
}
|
|
|
|
// Version must be 1.3
|
|
if hdr["major_version"] != majorVersion || hdr["minor_version"] != minorVersion {
|
|
return nil, fmt.Errorf("cab: invalid version %d.%d", hdr["major_version"], hdr["minor_version"])
|
|
}
|
|
|
|
totalSize := hdr["size"]
|
|
headerSize := cabStructSize
|
|
|
|
// Handle extra data flag
|
|
if hdr["flags"]&flagExtraDataPresent != 0 && hdr["extra_header_size"] == cabExtraStructSize {
|
|
extraStart := cabStructSize
|
|
extraEnd := extraStart + cabExtraStructSize
|
|
if offset+extraEnd > len(data) {
|
|
return nil, fmt.Errorf("cab: not enough data for extra header")
|
|
}
|
|
extraHdr, err := Parse(data[offset+extraStart:], cabExtraHeaderFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cab: failed to parse extra header")
|
|
}
|
|
if extraHdr["data_offset"] != hdr["size"] {
|
|
return nil, fmt.Errorf("cab: extra data offset mismatch")
|
|
}
|
|
totalSize += extraHdr["data_size"]
|
|
_ = headerSize
|
|
} else if hdr["flags"]&flagExtraDataPresent != 0 {
|
|
return nil, fmt.Errorf("cab: extra data flag set but wrong extra header size")
|
|
}
|
|
|
|
availableData := len(data) - offset
|
|
if totalSize > availableData {
|
|
return nil, fmt.Errorf("cab: reported size exceeds available data")
|
|
}
|
|
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: totalSize,
|
|
Description: fmt.Sprintf("%s, file count: %d, folder count: %d, header size: %d, total size: %d bytes", cabDescription, hdr["file_count"], hdr["folder_count"], cabStructSize, totalSize),
|
|
Confidence: ConfidenceMedium,
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Debian package (deb)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const debDescription = "Debian package file"
|
|
|
|
func debMagic() [][]byte {
|
|
return [][]byte{
|
|
// "!<arch>\ndebian-binary "
|
|
{0x21, 0x3c, 0x61, 0x72, 0x63, 0x68, 0x3e, 0x0a,
|
|
0x64, 0x65, 0x62, 0x69, 0x61, 0x6e, 0x2d, 0x62,
|
|
0x69, 0x6e, 0x61, 0x72, 0x79, 0x20, 0x20, 0x20},
|
|
}
|
|
}
|
|
|
|
func debParser(data []byte, offset int) (*SignatureResult, error) {
|
|
const (
|
|
endMarkerSize = 2
|
|
dataFileSizeLen = 10
|
|
dataFileSizeOffset = 48
|
|
controlFileSizeEnd = 130
|
|
controlFileSizeStart = 120
|
|
)
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("deb: offset out of bounds")
|
|
}
|
|
|
|
debData := data[offset:]
|
|
|
|
if len(debData) < controlFileSizeEnd {
|
|
return nil, fmt.Errorf("deb: not enough data for control file size")
|
|
}
|
|
|
|
// Read control file size as ASCII decimal
|
|
controlFileSizeBytes := debData[controlFileSizeStart:controlFileSizeEnd]
|
|
controlFileSizeStr := strings.TrimSpace(string(controlFileSizeBytes))
|
|
controlFileSize, err := strconv.Atoi(controlFileSizeStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("deb: invalid control file size: %s", controlFileSizeStr)
|
|
}
|
|
|
|
// Calculate data file size location
|
|
dataFileSizeStart := controlFileSizeEnd + endMarkerSize + controlFileSize + dataFileSizeOffset
|
|
dataFileSizeEnd := dataFileSizeStart + dataFileSizeLen
|
|
|
|
if dataFileSizeEnd > len(debData) {
|
|
return nil, fmt.Errorf("deb: not enough data for data file size")
|
|
}
|
|
|
|
dataFileSizeBytes := debData[dataFileSizeStart:dataFileSizeEnd]
|
|
dataFileSizeStr := strings.TrimSpace(string(dataFileSizeBytes))
|
|
dataFileSize, err := strconv.Atoi(dataFileSizeStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("deb: invalid data file size: %s", dataFileSizeStr)
|
|
}
|
|
|
|
totalSize := dataFileSizeEnd + endMarkerSize + dataFileSize
|
|
|
|
if totalSize > len(debData) {
|
|
return nil, fmt.Errorf("deb: reported size exceeds available data")
|
|
}
|
|
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: totalSize,
|
|
Description: fmt.Sprintf("%s, total size: %d bytes", debDescription, totalSize),
|
|
Confidence: ConfidenceHigh,
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CPIO
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const cpioDescription = "CPIO ASCII archive"
|
|
|
|
const cpioHeaderSize = 110
|
|
|
|
func cpioMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x30, 0x37, 0x30, 0x37, 0x30, 0x31}, // 070701
|
|
{0x30, 0x37, 0x30, 0x37, 0x30, 0x32}, // 070702
|
|
}
|
|
}
|
|
|
|
func cpioParser(data []byte, offset int) (*SignatureResult, error) {
|
|
const eofMarker = "TRAILER!!!"
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("cpio: offset out of bounds")
|
|
}
|
|
|
|
validMagics := cpioMagic()
|
|
|
|
var headerCount int
|
|
var totalSize int
|
|
nextHeaderOffset := offset
|
|
var previousHeaderOffset *int
|
|
availableData := len(data)
|
|
|
|
for IsOffsetSafe(availableData, nextHeaderOffset, previousHeaderOffset) {
|
|
if nextHeaderOffset >= len(data) {
|
|
break
|
|
}
|
|
cpioEntryData := data[nextHeaderOffset:]
|
|
|
|
entryHeader, err := parseCPIOEntryHeader(cpioEntryData)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
// Validate magic
|
|
magicValid := false
|
|
for _, m := range validMagics {
|
|
if bytes.Equal(entryHeader.magic, m) {
|
|
magicValid = true
|
|
break
|
|
}
|
|
}
|
|
if !magicValid {
|
|
break
|
|
}
|
|
|
|
headerCount++
|
|
totalSize += entryHeader.headerSize + entryHeader.dataSize
|
|
|
|
if entryHeader.fileName == eofMarker {
|
|
if headerCount > 1 {
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: totalSize,
|
|
Description: fmt.Sprintf("%s, file count: %d", cpioDescription, headerCount-1),
|
|
Confidence: ConfidenceHigh,
|
|
}, nil
|
|
}
|
|
break
|
|
}
|
|
|
|
prev := nextHeaderOffset
|
|
previousHeaderOffset = &prev
|
|
nextHeaderOffset = offset + totalSize
|
|
}
|
|
|
|
return nil, fmt.Errorf("cpio: no valid archive found")
|
|
}
|
|
|
|
type cpioEntry struct {
|
|
magic []byte
|
|
fileName string
|
|
headerSize int
|
|
dataSize int
|
|
}
|
|
|
|
func parseCPIOEntryHeader(cpioData []byte) (*cpioEntry, error) {
|
|
const (
|
|
nullByteSize = 1
|
|
cpioMagicStart = 0
|
|
cpioMagicEnd = 6
|
|
fileSizeStart = 54
|
|
fileSizeEnd = 62
|
|
fileNameSizeStart = 94
|
|
fileNameSizeEnd = 102
|
|
)
|
|
|
|
if len(cpioData) <= cpioHeaderSize {
|
|
return nil, fmt.Errorf("cpio: not enough data")
|
|
}
|
|
|
|
headerMagic := make([]byte, cpioMagicEnd-cpioMagicStart)
|
|
copy(headerMagic, cpioData[cpioMagicStart:cpioMagicEnd])
|
|
|
|
// Get file data size (ASCII hex)
|
|
fileSizeStr := string(cpioData[fileSizeStart:fileSizeEnd])
|
|
fileDataSize, err := strconv.ParseInt(fileSizeStr, 16, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cpio: invalid file data size")
|
|
}
|
|
|
|
// Get file name size (ASCII hex)
|
|
fileNameSizeStr := string(cpioData[fileNameSizeStart:fileNameSizeEnd])
|
|
fileNameSize, err := strconv.ParseInt(fileNameSizeStr, 16, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cpio: invalid file name size")
|
|
}
|
|
|
|
fileNameStart := cpioHeaderSize
|
|
fileNameEnd := fileNameStart + int(fileNameSize) - nullByteSize
|
|
|
|
if fileNameEnd > len(cpioData) || fileNameEnd < fileNameStart {
|
|
return nil, fmt.Errorf("cpio: file name out of bounds")
|
|
}
|
|
|
|
fileName := string(cpioData[fileNameStart:fileNameEnd])
|
|
|
|
headerTotalSize := cpioHeaderSize + int(fileNameSize)
|
|
|
|
return &cpioEntry{
|
|
magic: headerMagic,
|
|
fileName: fileName,
|
|
headerSize: headerTotalSize + cpioBytePadding(headerTotalSize),
|
|
dataSize: int(fileDataSize) + cpioBytePadding(int(fileDataSize)),
|
|
}, nil
|
|
}
|
|
|
|
func cpioBytePadding(n int) int {
|
|
mod := n % 4
|
|
if mod == 0 {
|
|
return 0
|
|
}
|
|
return 4 - mod
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ARJ
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const arjDescription = "ARJ archive data"
|
|
|
|
func arjMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x60, 0xea},
|
|
}
|
|
}
|
|
|
|
func arjParser(data []byte, offset int) (*SignatureResult, error) {
|
|
arjHeaderFields := []StructField{
|
|
{"magic", "u16"},
|
|
{"basic_header_size", "u16"},
|
|
{"extra_header_size", "u8"},
|
|
{"archiver_version", "u8"},
|
|
{"min_version", "u8"},
|
|
{"host_os", "u8"},
|
|
{"internal_flags", "u8"},
|
|
{"compression_method", "u8"},
|
|
{"file_type", "u8"},
|
|
{"reserved1", "u8"},
|
|
{"datetime_file", "u32"},
|
|
{"compressed_filesize", "u32"},
|
|
{"original_filesize", "u32"},
|
|
}
|
|
|
|
if offset >= len(data) {
|
|
return nil, fmt.Errorf("arj: offset out of bounds")
|
|
}
|
|
|
|
hdr, err := Parse(data[offset:], arjHeaderFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("arj: failed to parse header")
|
|
}
|
|
|
|
// Version validation
|
|
if hdr["archiver_version"] < 1 || hdr["archiver_version"] > 16 ||
|
|
hdr["min_version"] < 1 || hdr["min_version"] > 16 ||
|
|
hdr["archiver_version"] < hdr["min_version"] {
|
|
return nil, fmt.Errorf("arj: invalid version info")
|
|
}
|
|
|
|
// Flags
|
|
flags := "no password"
|
|
if hdr["internal_flags"]&0x01 != 0 {
|
|
flags = "password"
|
|
}
|
|
if hdr["internal_flags"]&0x04 != 0 {
|
|
flags += "|multi-volume"
|
|
}
|
|
if hdr["internal_flags"]&0x10 != 0 {
|
|
flags += "|slash-switched"
|
|
}
|
|
if hdr["internal_flags"]&0x20 != 0 {
|
|
flags += "|backup"
|
|
}
|
|
|
|
// Host OS
|
|
hostOSMap := map[int]string{
|
|
0: "MS-DOS", 1: "PRIMOS", 2: "UNIX", 3: "AMIGA", 4: "MAX-OS",
|
|
5: "OS/2", 6: "APPLE GS", 7: "ATARI ST", 8: "NeXT", 9: "VAX VMS",
|
|
}
|
|
hostOS, ok := hostOSMap[hdr["host_os"]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("arj: unknown host OS %d", hdr["host_os"])
|
|
}
|
|
|
|
// Compression method
|
|
compressionMethodMap := map[int]string{
|
|
0: "stored", 1: "compressed most", 2: "compressed",
|
|
3: "compressed faster", 4: "compressed fastest",
|
|
}
|
|
compressionMethod, ok := compressionMethodMap[hdr["compression_method"]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("arj: unknown compression method %d", hdr["compression_method"])
|
|
}
|
|
|
|
// File type
|
|
fileTypeMap := map[int]string{
|
|
0: "binary", 1: "7-bit text", 2: "comment header",
|
|
3: "directory", 4: "volume label",
|
|
}
|
|
fileType, ok := fileTypeMap[hdr["file_type"]]
|
|
if !ok {
|
|
return nil, fmt.Errorf("arj: unknown file type %d", hdr["file_type"])
|
|
}
|
|
|
|
// Compressed/uncompressed sizes — check for negative as signed i32
|
|
compressedFileSize := int32(hdr["compressed_filesize"])
|
|
if compressedFileSize < 0 {
|
|
return nil, fmt.Errorf("arj: negative compressed file size")
|
|
}
|
|
uncompressedFileSize := int32(hdr["original_filesize"])
|
|
if uncompressedFileSize < 0 {
|
|
return nil, fmt.Errorf("arj: negative uncompressed file size")
|
|
}
|
|
|
|
// Original file name
|
|
headerSize := hdr["extra_header_size"]
|
|
originalName := ""
|
|
if headerSize+4 < len(data[offset:]) {
|
|
originalName = GetCString(data[offset+headerSize+4:])
|
|
}
|
|
|
|
originalFileDate := EpochToString(uint32(hdr["datetime_file"]))
|
|
|
|
availableData := len(data) - offset
|
|
if headerSize > availableData {
|
|
return nil, fmt.Errorf("arj: header size exceeds available data")
|
|
}
|
|
|
|
extractionDeclined := fileType != "comment header"
|
|
|
|
return &SignatureResult{
|
|
Offset: offset,
|
|
Size: headerSize,
|
|
Description: fmt.Sprintf(
|
|
"%s, header size: %d, version %d, minimum version to extract: %d, flags: %s, compression method: %s, file type: %s, original name: %s, original file date: %s, compressed file size: %d, uncompressed file size: %d, os: %s",
|
|
arjDescription,
|
|
headerSize,
|
|
hdr["archiver_version"],
|
|
hdr["min_version"],
|
|
flags,
|
|
compressionMethod,
|
|
fileType,
|
|
originalName,
|
|
originalFileDate,
|
|
compressedFileSize,
|
|
uncompressedFileSize,
|
|
hostOS,
|
|
),
|
|
Confidence: ConfidenceMedium,
|
|
ExtractionDeclined: extractionDeclined,
|
|
}, nil
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ISO 9660
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const iso9660Description = "ISO9660 primary volume"
|
|
|
|
func iso9660Magic() [][]byte {
|
|
return [][]byte{
|
|
{0x01, 0x43, 0x44, 0x30, 0x30, 0x31, 0x01, 0x00}, // \x01CD001\x01\x00
|
|
}
|
|
}
|
|
|
|
func iso9660Parser(data []byte, offset int) (*SignatureResult, error) {
|
|
const (
|
|
isoMagicOffset = 32768
|
|
isoStructStart = 32840
|
|
)
|
|
|
|
isoFields := []StructField{
|
|
{"unused1", "u64"},
|
|
{"volume_size_lsb", "u32"},
|
|
{"volume_size_msb", "u32"},
|
|
{"unused2", "u64"},
|
|
{"unused3", "u64"},
|
|
{"unused4", "u64"},
|
|
{"unused5", "u64"},
|
|
{"set_size_lsb", "u16"},
|
|
{"set_size_msb", "u16"},
|
|
{"sequence_number_lsb", "u16"},
|
|
{"sequence_number_msb", "u16"},
|
|
{"block_size_lsb", "u16"},
|
|
{"block_size_msb", "u16"},
|
|
{"path_table_size_lsb", "u32"},
|
|
{"path_table_size_msb", "u32"},
|
|
}
|
|
|
|
if offset < isoMagicOffset {
|
|
return nil, fmt.Errorf("iso9660: not enough data before magic offset")
|
|
}
|
|
|
|
isoStart := offset - isoMagicOffset
|
|
|
|
if isoStart+isoStructStart >= len(data) {
|
|
return nil, fmt.Errorf("iso9660: not enough data for header")
|
|
}
|
|
|
|
isoHeaderData := data[isoStart+isoStructStart:]
|
|
hdr, err := Parse(isoHeaderData, isoFields, "little")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iso9660: failed to parse header")
|
|
}
|
|
|
|
// Unused fields must be 0
|
|
if hdr["unused1"] != 0 || hdr["unused2"] != 0 || hdr["unused3"] != 0 ||
|
|
hdr["unused4"] != 0 || hdr["unused5"] != 0 {
|
|
return nil, fmt.Errorf("iso9660: unused fields not zero")
|
|
}
|
|
|
|
// Validate byte-swapped field pairs
|
|
if hdr["set_size_lsb"] != int(swapU16(uint16(hdr["set_size_msb"]))) {
|
|
return nil, fmt.Errorf("iso9660: set_size mismatch")
|
|
}
|
|
if hdr["block_size_lsb"] != int(swapU16(uint16(hdr["block_size_msb"]))) {
|
|
return nil, fmt.Errorf("iso9660: block_size mismatch")
|
|
}
|
|
if hdr["volume_size_lsb"] != int(swapU32(uint32(hdr["volume_size_msb"]))) {
|
|
return nil, fmt.Errorf("iso9660: volume_size mismatch")
|
|
}
|
|
if hdr["sequence_number_lsb"] != int(swapU16(uint16(hdr["sequence_number_msb"]))) {
|
|
return nil, fmt.Errorf("iso9660: sequence_number mismatch")
|
|
}
|
|
if hdr["path_table_size_lsb"] != int(swapU32(uint32(hdr["path_table_size_msb"]))) {
|
|
return nil, fmt.Errorf("iso9660: path_table_size mismatch")
|
|
}
|
|
|
|
imageSize := hdr["volume_size_lsb"] * hdr["block_size_lsb"]
|
|
|
|
return &SignatureResult{
|
|
Offset: isoStart,
|
|
Size: imageSize,
|
|
Description: fmt.Sprintf("%s, total size: %d bytes", iso9660Description, imageSize),
|
|
Confidence: ConfidenceHigh,
|
|
}, nil
|
|
}
|
|
|
|
// swapU16 swaps bytes of a uint16 (big-endian to native / vice versa)
|
|
func swapU16(v uint16) uint16 {
|
|
b := make([]byte, 2)
|
|
binary.BigEndian.PutUint16(b, v)
|
|
return binary.LittleEndian.Uint16(b)
|
|
}
|
|
|
|
// swapU32 swaps bytes of a uint32
|
|
func swapU32(v uint32) uint32 {
|
|
b := make([]byte, 4)
|
|
binary.BigEndian.PutUint32(b, v)
|
|
return binary.LittleEndian.Uint32(b)
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dahua ZIP
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const dahuaZipDescription = "Dahua ZIP archive"
|
|
|
|
func dahuaZipMagic() [][]byte {
|
|
return [][]byte{
|
|
{0x44, 0x48, 0x03, 0x04}, // DH\x03\x04
|
|
}
|
|
}
|
|
|
|
func dahuaZipParser(data []byte, offset int) (*SignatureResult, error) {
|
|
// Parse as a normal ZIP file, then replace the description prefix
|
|
result, err := parseZipLike(data, offset, zipDescription)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dahua_zip: %v", err)
|
|
}
|
|
result.Description = strings.Replace(result.Description, zipDescription, dahuaZipDescription, 1)
|
|
return result, nil
|
|
}
|