package binsnoop import ( "bytes" "encoding/binary" "fmt" "math" ) // --------------------------------------------------------------------------- // SquashFS // --------------------------------------------------------------------------- const SquashFSDescription = "SquashFS file system" func squashfs_magic() [][]byte { return [][]byte{ []byte("sqsh"), []byte("hsqs"), []byte("sqlz"), []byte("qshs"), []byte("tqsh"), []byte("hsqt"), []byte("shsq"), } } func squashfs_parser(data []byte, offset int) (*SignatureResult, error) { const squashfsV4 = 4 const maxSquashfsVersion uint16 = 4 const minSquashfsHeaderSize = 120 const versionStart = 28 const versionEnd = 30 compressionTypes := map[int]string{ 0: "unknown", 1: "gzip", 2: "lzma", 3: "lzo", 4: "xz", 5: "lz4", 6: "zstd", } available := len(data) - offset sqshData := data[offset:] if len(sqshData) <= minSquashfsHeaderSize { return nil, fmt.Errorf("not enough data for squashfs header") } // Determine endianness from version field at fixed offset endianness := "little" squashfsVersion := int(binary.LittleEndian.Uint16(sqshData[versionStart:versionEnd])) if squashfsVersion == 0 || squashfsVersion > int(maxSquashfsVersion) { endianness = "big" squashfsVersion = int(binary.BigEndian.Uint16(sqshData[versionStart:versionEnd])) } if squashfsVersion <= 0 || squashfsVersion > int(maxSquashfsVersion) { return nil, fmt.Errorf("invalid squashfs version") } // Pick the right structure var fields []StructField if squashfsVersion == squashfsV4 { fields = []StructField{ {"magic", "u32"}, {"inode_count", "u32"}, {"modification_time", "u32"}, {"block_size", "u32"}, {"fragment_count", "u32"}, {"compression_id", "u16"}, {"block_log", "u16"}, {"flags", "u16"}, {"id_count", "u16"}, {"major_version", "u16"}, {"minor_version", "u16"}, {"root_inode_ref", "u64"}, {"image_size", "u64"}, {"uid_start", "u64"}, } } else { fields = []StructField{ {"magic", "u32"}, {"inode_count", "u32"}, {"bytes_used_2", "u32"}, {"uid_start_2", "u32"}, {"guid_start_2", "u32"}, {"inode_table_start_2", "u32"}, {"directory_table_start_2", "u32"}, {"major_version", "u16"}, {"minor_version", "u16"}, {"block_size_1", "u16"}, {"block_log", "u16"}, {"flags", "u8"}, {"uid_count", "u8"}, {"guid_count", "u8"}, {"modification_time", "u32"}, {"root_inode_ref", "u64"}, {"block_size", "u32"}, {"fragment_entry_count", "u32"}, {"fragment_table_start_2", "u32"}, {"image_size", "u64"}, {"uid_start", "u64"}, {"guid_start", "u64"}, {"inode_table_start", "u64"}, {"directory_table_start", "u64"}, {"fragment_table_start", "u64"}, {"lookup_table_start", "u64"}, } } hdr, err := Parse(sqshData, fields, endianness) if err != nil { return nil, fmt.Errorf("squashfs header parse failed") } // For v1/v2 images, adjust fields if squashfsVersion < 3 { hdr["uid_start"] = hdr["uid_start_2"] hdr["image_size"] = hdr["bytes_used_2"] } headerSize := StructSize(fields) imageSize := hdr["image_size"] if imageSize <= minSquashfsHeaderSize { return nil, fmt.Errorf("squashfs image size too small") } if imageSize > available { return nil, fmt.Errorf("squashfs image size exceeds available data") } // Validate block_size vs block_log blockSize := hdr["block_size"] blockLog := hdr["block_log"] if blockSize <= 0 || blockLog != int(math.Log2(float64(blockSize))) { return nil, fmt.Errorf("squashfs block_size/block_log mismatch") } // Validate UID table uidTableStart := offset + hdr["uid_start"] if uidTableStart <= headerSize { return nil, fmt.Errorf("squashfs uid table offset invalid") } if uidTableStart >= len(data) { return nil, fmt.Errorf("squashfs uid table out of bounds") } // Parse one UID entry uidData := data[uidTableStart:] var uidFields []StructField if squashfsVersion == squashfsV4 { uidFields = []StructField{{"uid_block_ptr", "u64"}} } else { uidFields = []StructField{{"uid_block_ptr", "u32"}} } uidEntry, err := Parse(uidData, uidFields, endianness) if err != nil { return nil, fmt.Errorf("squashfs uid entry parse failed") } uidPtr := uidEntry["uid_block_ptr"] if uidPtr != 0 && (uidPtr <= headerSize || uidPtr > imageSize) { return nil, fmt.Errorf("squashfs uid table pointer invalid") } compressionID := hdr["compression_id"] if squashfsVersion < 4 { // v3 headers don't have compression_id; use 0 (unknown) compressionID = 0 } compStr, ok := compressionTypes[compressionID] if !ok { return nil, fmt.Errorf("squashfs unknown compression type") } createDate := EpochToString(uint32(hdr["modification_time"])) return &SignatureResult{ Offset: offset, Size: imageSize, Confidence: ConfidenceHigh, Description: fmt.Sprintf( "%s, %s endian, version: %d.%d, compression: %s, inode count: %d, block size: %d, image size: %d bytes, created: %s", SquashFSDescription, endianness, hdr["major_version"], hdr["minor_version"], compStr, hdr["inode_count"], blockSize, imageSize, createDate, ), }, nil } // --------------------------------------------------------------------------- // CramFS // --------------------------------------------------------------------------- const CramFSDescription = "CramFS filesystem" func cramfs_magic() [][]byte { return [][]byte{ []byte("Compressed ROMFS"), } } func cramfs_parser(data []byte, offset int) (*SignatureResult, error) { const signatureOffset = 16 const crcStartOffset = 32 const crcEndOffset = 36 const bigEndianMagic = 0x453DCD28 const littleEndianMagic = 0x28CD3D45 realOffset := offset - signatureOffset if realOffset < 0 || realOffset >= len(data) { return nil, fmt.Errorf("cramfs offset out of bounds") } cramfsData := data[realOffset:] fields := []StructField{ {"magic", "u32"}, {"size", "u32"}, {"flags", "u32"}, {"future", "u32"}, {"signature_p1", "u64"}, {"signature_p2", "u64"}, {"checksum", "u32"}, {"edition", "u32"}, {"block_count", "u32"}, {"file_count", "u32"}, } endianness := "little" hdr, err := Parse(cramfsData, fields, endianness) if err != nil { return nil, fmt.Errorf("cramfs parse failed") } magic := hdr["magic"] if magic != bigEndianMagic && magic != littleEndianMagic { return nil, fmt.Errorf("cramfs invalid magic") } if magic == bigEndianMagic { endianness = "big" hdr, err = Parse(cramfsData, fields, endianness) if err != nil { return nil, fmt.Errorf("cramfs big endian parse failed") } } structSize := StructSize(fields) if hdr["size"] <= structSize { return nil, fmt.Errorf("cramfs reported size too small") } imageSize := hdr["size"] checksum := uint32(hdr["checksum"]) fileCount := hdr["file_count"] if realOffset+imageSize > len(data) { return nil, fmt.Errorf("cramfs image exceeds available data") } // Copy image data and null out checksum field for CRC validation cramfsImage := make([]byte, imageSize) copy(cramfsImage, cramfsData[:imageSize]) for i := crcStartOffset; i < crcEndOffset && i < imageSize; i++ { cramfsImage[i] = 0 } errorMsg := "" confidence := ConfidenceHigh if CRC32(cramfsImage) != checksum { errorMsg = " (checksum error)" confidence = ConfidenceMedium } return &SignatureResult{ Offset: realOffset, Size: imageSize, Confidence: confidence, Description: fmt.Sprintf("%s, %s endian, %d files, total size: %d bytes%s", CramFSDescription, endianness, fileCount, imageSize, errorMsg), }, nil } // --------------------------------------------------------------------------- // JFFS2 // --------------------------------------------------------------------------- const JFFS2Description = "JFFS2 filesystem" func jffs2_magic() [][]byte { return [][]byte{ {0x19, 0x85, 0xE0, 0x01}, {0x19, 0x85, 0xE0, 0x02}, {0x19, 0x85, 0x20, 0x03}, {0x85, 0x19, 0x01, 0xE0}, {0x85, 0x19, 0x02, 0xE0}, {0x85, 0x19, 0x03, 0x20}, } } func jffs2_parser(data []byte, offset int) (*SignatureResult, error) { const maxPageSize = 0x20000 const minValidNodeCount = 2 const jffs2CorrectMagic = 0x1985 const headerCRCSize = 8 jffs2NodeFields := []StructField{ {"magic", "u16"}, {"type", "u16"}, {"size", "u32"}, {"crc", "u32"}, } jffs2NodeStructSize := StructSize(jffs2NodeFields) // 12 // Parse first node firstNode, endianness, err := parseJFFS2NodeHeader(data[offset:], jffs2NodeFields, headerCRCSize, jffs2CorrectMagic) if err != nil { return nil, err } jffs2EOF := offset + jffs2Roundup(firstNode["size"]) if jffs2EOF >= len(data) { return nil, fmt.Errorf("jffs2 first node exceeds data") } var nodeMagic []byte if endianness == "big" { nodeMagic = []byte{0x19, 0x85} } else { nodeMagic = []byte{0x85, 0x19} } nodeCount := 1 // Scan for subsequent nodes searchData := data[jffs2EOF:] for i := 0; i < len(searchData); { idx := bytes.Index(searchData[i:], nodeMagic) if idx < 0 { break } absPos := jffs2EOF + i + idx if absPos < jffs2EOF { i += idx + 1 continue } if absPos-jffs2EOF > maxPageSize { break } headerEnd := absPos + jffs2NodeStructSize if headerEnd > len(data) { break } nodeHdr, _, err := parseJFFS2NodeHeader(data[absPos:headerEnd], jffs2NodeFields, headerCRCSize, jffs2CorrectMagic) if err == nil { nodeCount++ jffs2EOF = absPos + jffs2Roundup(nodeHdr["size"]) } i += idx + 1 } if nodeCount <= minValidNodeCount { return nil, fmt.Errorf("jffs2 too few valid nodes: %d", nodeCount) } totalSize := jffs2EOF - offset return &SignatureResult{ Offset: offset, Size: totalSize, Confidence: ConfidenceHigh, Description: fmt.Sprintf("%s, %s endian, nodes: %d, total size: %d bytes", JFFS2Description, endianness, nodeCount, totalSize), }, nil } func parseJFFS2NodeHeader(nodeData []byte, fields []StructField, headerCRCSize, correctMagic int) (map[string]int, string, error) { endianness := "little" hdr, err := Parse(nodeData, fields, endianness) if err != nil { return nil, "", fmt.Errorf("jffs2 node parse failed") } if hdr["magic"] != correctMagic { endianness = "big" hdr, err = Parse(nodeData, fields, endianness) if err != nil { return nil, "", fmt.Errorf("jffs2 node big endian parse failed") } } if hdr["magic"] != correctMagic { return nil, "", fmt.Errorf("jffs2 node invalid magic") } // Validate header CRC if headerCRCSize > len(nodeData) { return nil, "", fmt.Errorf("jffs2 not enough data for crc") } calcCRC := int(jffs2NodeCRC(nodeData[:headerCRCSize])) if calcCRC != hdr["crc"] { return nil, "", fmt.Errorf("jffs2 node header crc mismatch") } return hdr, endianness, nil } func jffs2NodeCRC(data []byte) uint32 { return CRC32(data) // CRC32 uses IEEE polynomial; JFFS2: crc32(0xFFFFFFFF, data) ^ 0xFFFFFFFF which is standard CRC32 } func jffs2Roundup(num int) int { base := 4 return ((num + base - 1) / base) * base } // --------------------------------------------------------------------------- // YAFFS // --------------------------------------------------------------------------- const YAFFSDescription = "YAFFSv2 filesystem" func yaffs_magic() [][]byte { return [][]byte{ {0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF}, {0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF}, {0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF}, {0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF}, } } func yaffs_parser(data []byte, offset int) (*SignatureResult, error) { const maxObjSize = 16896 const minNumberOfObjs = 2 const bigEndianFirstByte byte = 0 const fileType = 1 available := len(data) requiredMinOffset := offset + (maxObjSize * minNumberOfObjs) if !IsOffsetSafe(available, requiredMinOffset, nil) { return nil, fmt.Errorf("yaffs not enough data") } endianness := "little" if data[offset] == bigEndianFirstByte { endianness = "big" } yaffsData := data[offset:] // Determine page size pageSize, err := yaffsGetPageSize(yaffsData) if err != nil { return nil, err } // Determine spare size spareSize, err := yaffsGetSpareSize(yaffsData, pageSize, endianness) if err != nil { return nil, err } // Get total image size imageSize, err := yaffsGetImageSize(yaffsData, pageSize, spareSize, endianness) if err != nil { return nil, err } return &SignatureResult{ Offset: offset, Size: imageSize, Confidence: ConfidenceMedium, Description: fmt.Sprintf("%s, %s endian, page size: %d, spare size: %d, image size: %d bytes", YAFFSDescription, endianness, pageSize, spareSize, imageSize), }, nil } func yaffsGetPageSize(fileData []byte) (int, error) { spareMagics := [][]byte{ {0x00, 0x00, 0x10, 0x00}, {0x00, 0x10, 0x00, 0x00}, {0xFF, 0xFF, 0x00, 0x00, 0x10, 0x00}, {0xFF, 0xFF, 0x00, 0x10, 0x00, 0x00}, } pageSizes := []int{512, 1024, 2048, 4096, 8192, 16384} for _, ps := range pageSizes { for _, sm := range spareMagics { start := ps end := start + len(sm) if end <= len(fileData) { if bytes.Equal(fileData[start:end], sm) { return ps, nil } } } } return 0, fmt.Errorf("yaffs page size not detected") } func yaffsGetSpareSize(fileData []byte, pageSize int, endianness string) (int, error) { const minNumberOfObjs = 2 spareSizes := []int{16, 32, 64, 128, 256, 512} yaffsObjFields := []StructField{ {"type", "u32"}, {"parent_id", "u32"}, {"name_checksum", "u16"}, } for _, ss := range spareSizes { nextObjOffset := (pageSize + ss) * minNumberOfObjs if nextObjOffset < len(fileData) { hdr, err := Parse(fileData[nextObjOffset:], yaffsObjFields, endianness) if err == nil { allowedTypes := []int{0, 1, 2, 3, 4, 5} if containsInt(allowedTypes, hdr["type"]) && hdr["parent_id"] > 0 && hdr["name_checksum"] == 0xFFFF { return ss, nil } } } } return 0, fmt.Errorf("yaffs spare size not detected") } func yaffsGetImageSize(fileData []byte, pageSize, spareSize int, endianness string) (int, error) { const fileType = 1 const minNumberOfObjs = 2 const infoStructStart = 268 yaffsObjFields := []StructField{ {"type", "u32"}, {"parent_id", "u32"}, {"name_checksum", "u16"}, } yaffsFileInfo := []StructField{ {"mode", "u32"}, {"uid", "u32"}, {"gid", "u32"}, {"atime", "u32"}, {"mtime", "u32"}, {"ctime", "u32"}, {"file_size", "u32"}, } imageSize := 0 nextObjOffset := 0 var previousObjOffset *int availableData := len(fileData) blockSize := pageSize + spareSize for IsOffsetSafe(availableData, nextObjOffset, previousObjOffset) { objData := fileData[nextObjOffset:] hdr, err := Parse(objData, yaffsObjFields, endianness) if err != nil { break } allowedTypes := []int{0, 1, 2, 3, 4, 5} if !containsInt(allowedTypes, hdr["type"]) || hdr["parent_id"] <= 0 || hdr["name_checksum"] != 0xFFFF { break } dataBlocks := 1 if hdr["type"] == fileType { if infoStructStart < len(objData) { fileInfo, err := Parse(objData[infoStructStart:], yaffsFileInfo, endianness) if err != nil { return 0, fmt.Errorf("yaffs file header parse failed") } fileBlockCount := int(math.Ceil(float64(fileInfo["file_size"]) / float64(pageSize))) dataBlocks += fileBlockCount } else { return 0, fmt.Errorf("yaffs not enough data for file header") } } prev := nextObjOffset previousObjOffset = &prev imageSize += dataBlocks * blockSize nextObjOffset = imageSize } if imageSize > blockSize*minNumberOfObjs && imageSize <= availableData { return imageSize, nil } return 0, fmt.Errorf("yaffs image size invalid") } // --------------------------------------------------------------------------- // RomFS // --------------------------------------------------------------------------- const RomFSDescription = "RomFS filesystem" func romfs_magic() [][]byte { return [][]byte{ []byte("-rom1fs-"), } } func romfs_parser(data []byte, offset int) (*SignatureResult, error) { const maxHeaderCRCDataLen = 512 headerFields := []StructField{ {"magic", "u64"}, {"image_size", "u32"}, {"checksum", "u32"}, } romfsData := data[offset:] headerSize := StructSize(headerFields) hdr, err := Parse(romfsData, headerFields, "big") if err != nil { return nil, fmt.Errorf("romfs header parse failed") } if hdr["image_size"] <= headerSize { return nil, fmt.Errorf("romfs image size too small") } // Get volume name volumeName := "" if headerSize < len(romfsData) { volumeName = GetCString(romfsData[headerSize:]) } // Validate CRC crcDataLen := maxHeaderCRCDataLen if hdr["image_size"] < crcDataLen { crcDataLen = hdr["image_size"] } if crcDataLen > len(romfsData) { return nil, fmt.Errorf("romfs not enough data for crc") } if !romfsCRCValid(romfsData[:crcDataLen]) { return nil, fmt.Errorf("romfs crc invalid") } return &SignatureResult{ Offset: offset, Size: hdr["image_size"], Confidence: ConfidenceHigh, Description: fmt.Sprintf(`%s, volume name: "%s", total size: %d bytes`, RomFSDescription, volumeName, hdr["image_size"]), }, nil } func romfsCRCValid(crcData []byte) bool { wordSize := 4 if len(crcData)%wordSize != 0 { return false } var sum uint32 for i := 0; i < len(crcData); i += wordSize { sum += binary.BigEndian.Uint32(crcData[i : i+wordSize]) } return sum == 0 } // --------------------------------------------------------------------------- // EXT // --------------------------------------------------------------------------- const EXTDescription = "EXT filesystem" func ext_magic() [][]byte { return [][]byte{ {0x53, 0xEF, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00}, {0x53, 0xEF, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00}, {0x53, 0xEF, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00}, {0x53, 0xEF, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00}, {0x53, 0xEF, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00}, {0x53, 0xEF, 0x02, 0x00, 0x03, 0x00, 0x00, 0x00}, } } const EXTMagicOffset = 1080 func ext_parser(data []byte, offset int) (*SignatureResult, error) { const magicOffset = 1080 const superblockOffset = 1024 const superblockSize = 1024 const maxBlockLog = 2 realOffset := offset - magicOffset if realOffset < 0 { return nil, fmt.Errorf("ext offset out of bounds") } extData := data[realOffset:] if len(extData) < superblockOffset+superblockSize { return nil, fmt.Errorf("ext not enough data for superblock") } extSBFields := []StructField{ {"inodes_count", "u32"}, {"blocks_count", "u32"}, {"reserved_blocks_count", "u32"}, {"free_blocks_count", "u32"}, {"free_inodes_count", "u32"}, {"first_data_block", "u32"}, {"log_block_size", "u32"}, {"log_frag_size", "u32"}, {"blocks_per_group", "u32"}, {"frags_per_group", "u32"}, {"inodes_per_group", "u32"}, {"modification_time", "u32"}, {"write_time", "u32"}, {"mount_count", "u16"}, {"max_mount_count", "u16"}, {"magic", "u16"}, {"state", "u16"}, {"errors", "u16"}, {"s_minor_rev_level", "u16"}, {"last_check", "u32"}, {"check_interval", "u32"}, {"creator_os", "u32"}, {"s_rev_level", "u32"}, {"resuid", "u16"}, {"resgid", "u16"}, } hdr, err := Parse(extData[superblockOffset:], extSBFields, "little") if err != nil { return nil, fmt.Errorf("ext superblock parse failed") } supportedOS := map[int]string{0: "Linux", 1: "GNU HURD", 2: "MASIX", 3: "FreeBSD", 4: "Lites"} osName, osOk := supportedOS[hdr["creator_os"]] if !osOk { return nil, fmt.Errorf("ext unknown OS") } allowedRevLevels := []int{0, 1} if !containsInt(allowedRevLevels, hdr["s_rev_level"]) { return nil, fmt.Errorf("ext invalid rev level") } allowedFirstDataBlocks := []int{0, 1} if !containsInt(allowedFirstDataBlocks, hdr["first_data_block"]) { return nil, fmt.Errorf("ext invalid first_data_block") } if hdr["log_block_size"] > maxBlockLog { return nil, fmt.Errorf("ext invalid log_block_size") } blockSize := 1024 << hdr["log_block_size"] imageSize := blockSize * hdr["blocks_count"] return &SignatureResult{ Offset: realOffset, Size: imageSize, Confidence: ConfidenceMedium, Description: fmt.Sprintf( "%s for %s, inodes: %d, block size: %d, block count: %d, free blocks: %d, reserved blocks: %d, total size: %d bytes", EXTDescription, osName, hdr["inodes_count"], blockSize, hdr["free_blocks_count"], hdr["reserved_blocks_count"], hdr["blocks_count"], imageSize), }, nil } // --------------------------------------------------------------------------- // FAT // --------------------------------------------------------------------------- const FATDescription = "FAT file system" const FATMagicOffset = 0x01FE func fat_magic() [][]byte { return [][]byte{ {0x55, 0xAA}, } } func fat_parser(data []byte, offset int) (*SignatureResult, error) { const expectedFATCount = 2 validOpcode1 := []int{0xEB, 0xE9} validSectorSizes := []int{512, 1024, 2048, 4096} validSectorsPerCluster := []int{1, 2, 4, 8, 16, 32, 64, 128} validMediaTypes := []int{0xF0, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE} if offset < FATMagicOffset { return nil, fmt.Errorf("fat offset too small") } realOffset := offset - FATMagicOffset fatData := data[realOffset:] fields := []StructField{ {"opcode1", "u8"}, {"opcode2", "u8"}, {"opcode3", "u8"}, {"oem_name", "u64"}, {"bytes_per_sector", "u16"}, {"sectors_per_cluster", "u8"}, {"reserved_sectors", "u16"}, {"fat_count", "u8"}, {"root_entries_count_16", "u16"}, {"total_sectors_16", "u16"}, {"media_type", "u8"}, {"fat_size_16", "u16"}, {"sectors_per_track", "u16"}, {"number_of_heads", "u16"}, {"hidden_sectors", "u32"}, {"total_sectors_32", "u32"}, } hdr, err := Parse(fatData, fields, "little") if err != nil { return nil, fmt.Errorf("fat header parse failed") } if !containsInt(validOpcode1, hdr["opcode1"]) { return nil, fmt.Errorf("fat invalid opcode1") } if !containsInt(validSectorSizes, hdr["bytes_per_sector"]) { return nil, fmt.Errorf("fat invalid sector size") } if !containsInt(validSectorsPerCluster, hdr["sectors_per_cluster"]) { return nil, fmt.Errorf("fat invalid sectors per cluster") } if hdr["reserved_sectors"] <= 0 { return nil, fmt.Errorf("fat invalid reserved sectors") } if hdr["fat_count"] != expectedFATCount { return nil, fmt.Errorf("fat invalid fat count") } if !containsInt(validMediaTypes, hdr["media_type"]) { return nil, fmt.Errorf("fat invalid media type") } isFAT32 := hdr["fat_size_16"] == 0 totalSize := 0 if hdr["total_sectors_16"] != 0 { totalSize = hdr["total_sectors_16"] * hdr["bytes_per_sector"] } else { totalSize = hdr["total_sectors_32"] * hdr["bytes_per_sector"] } if totalSize <= 0 { return nil, fmt.Errorf("fat total size is zero") } fatTypeDesc := "FAT12/16" if isFAT32 { fatTypeDesc = "FAT32" } return &SignatureResult{ Offset: realOffset, Size: totalSize, Confidence: ConfidenceMedium, Description: fmt.Sprintf("%s, type: %s, total size: %d bytes", FATDescription, fatTypeDesc, totalSize), }, nil } // --------------------------------------------------------------------------- // NTFS // --------------------------------------------------------------------------- const NTFSDescription = "NTFS partition" func ntfs_magic() [][]byte { return [][]byte{ {0xEB, 0x52, 0x90, 0x4E, 0x54, 0x46, 0x53, 0x20, 0x20, 0x20, 0x20}, } } func ntfs_parser(data []byte, offset int) (*SignatureResult, error) { ntfsFields := []StructField{ {"opcodes", "u24"}, {"magic", "u64"}, {"bytes_per_sector", "u16"}, {"sectors_per_cluster", "u8"}, {"unused1", "u16"}, {"unused2", "u24"}, {"unused3", "u16"}, {"media_type", "u8"}, {"unused4", "u16"}, {"sectors_per_track", "u16"}, {"head_count", "u16"}, {"hidden_sector_count", "u32"}, {"unused5", "u32"}, {"unknown", "u32"}, {"sector_count", "u64"}, } ntfsData := data[offset:] hdr, err := Parse(ntfsData, ntfsFields, "little") if err != nil { return nil, fmt.Errorf("ntfs header parse failed") } if hdr["unused1"] != 0 || hdr["unused2"] != 0 || hdr["unused3"] != 0 || hdr["unused4"] != 0 || hdr["unused5"] != 0 { return nil, fmt.Errorf("ntfs unused fields are not zero") } sectorSize := hdr["bytes_per_sector"] sectorCount := hdr["sector_count"] // The reported sector count does not include the NTFS boot sector itself totalSize := sectorSize * (sectorCount + 1) if totalSize <= sectorSize { return nil, fmt.Errorf("ntfs total size too small") } return &SignatureResult{ Offset: offset, Size: totalSize, Confidence: ConfidenceMedium, Description: fmt.Sprintf("%s, number of sectors: %d, bytes per sector: %d, total size: %d bytes", NTFSDescription, sectorCount, sectorSize, totalSize), }, nil } // --------------------------------------------------------------------------- // APFS // --------------------------------------------------------------------------- const APFSDescription = "APple File System" const APFSMagicOffset = 0x20 func apfs_magic() [][]byte { return [][]byte{ []byte("NXSB"), } } func apfs_parser(data []byte, offset int) (*SignatureResult, error) { const mbrBlockSize = 512 const maxFSCount = 100 const fsCountBlockSize = 512 if offset < APFSMagicOffset { return nil, fmt.Errorf("apfs offset too small") } realOffset := offset - APFSMagicOffset available := len(data) - realOffset apfsData := data[realOffset:] apfsStructStart := APFSMagicOffset fields := []StructField{ {"magic", "u32"}, {"block_size", "u32"}, {"block_count", "u64"}, {"nx_features", "u64"}, {"nx_ro_compat_features", "u64"}, {"nx_incompat_features", "u64"}, {"nx_uuid_p1", "u64"}, {"nx_uuid_p2", "u64"}, {"nx_next_oid", "u64"}, {"nx_next_xid", "u64"}, {"nx_xp_desc_blocks", "u32"}, {"nx_xp_data_blocks", "u32"}, {"nx_xp_desc_base", "u64"}, {"nx_xp_data_base", "u64"}, {"nx_xp_desc_next", "u32"}, {"nx_xp_data_next", "u32"}, {"nx_xp_desc_index", "u32"}, {"nx_xp_desc_len", "u32"}, {"nx_xp_data_index", "u32"}, {"nx_xp_data_len", "u32"}, {"nx_spaceman_oid", "u64"}, {"nx_omap_oid", "u64"}, {"nx_reaper_oid", "u64"}, {"nx_xp_test_type", "u32"}, {"nx_xp_max_file_systems", "u32"}, } structEnd := apfsStructStart + StructSize(fields) if structEnd > len(apfsData) { return nil, fmt.Errorf("apfs not enough data for header") } hdr, err := Parse(apfsData[apfsStructStart:structEnd], fields, "little") if err != nil { return nil, fmt.Errorf("apfs header parse failed") } if hdr["block_size"] == 0 || hdr["block_count"] == 0 { return nil, fmt.Errorf("apfs zero block size or count") } allowedFeatureFlags := []int{0, 1, 2, 3} allowedIncompatFlags := []int{0, 1, 2, 3, 0x100, 0x101, 0x102, 0x103} allowedROCompatFlags := []int{0} if !containsInt(allowedFeatureFlags, hdr["nx_features"]) || !containsInt(allowedROCompatFlags, hdr["nx_ro_compat_features"]) || !containsInt(allowedIncompatFlags, hdr["nx_incompat_features"]) { return nil, fmt.Errorf("apfs invalid feature flags") } if hdr["nx_xp_test_type"] != 0 { return nil, fmt.Errorf("apfs test_type must be null") } fsCount := int(math.Ceil(float64(hdr["nx_xp_max_file_systems"]) / float64(fsCountBlockSize))) if fsCount <= 0 || fsCount > maxFSCount { return nil, fmt.Errorf("apfs invalid filesystem count") } totalSize := hdr["block_count"] * hdr["block_size"] truncatedMsg := "" if totalSize > available { truncatedSize := totalSize - available if truncatedSize == mbrBlockSize { totalSize -= truncatedSize truncatedMsg = fmt.Sprintf(" (truncated by %d bytes)", truncatedSize) } } return &SignatureResult{ Offset: realOffset, Size: totalSize, Confidence: ConfidenceMedium, Description: fmt.Sprintf("%s, block size: %d bytes, block count: %d, total size: %d bytes%s", APFSDescription, hdr["block_size"], hdr["block_count"], totalSize, truncatedMsg), }, nil } // --------------------------------------------------------------------------- // BTRFS // --------------------------------------------------------------------------- const BTRFSDescription = "BTRFS file system" const BTRFSMagicOffset = 0x10040 func btrfs_magic() [][]byte { return [][]byte{ []byte("_BHRfS_M"), } } func btrfs_parser(data []byte, offset int) (*SignatureResult, error) { const superblockOffset = 0x10000 const superblockEnd = superblockOffset + 0x1000 const crcStart = 0x20 if offset < BTRFSMagicOffset { return nil, fmt.Errorf("btrfs offset too small") } realOffset := offset - BTRFSMagicOffset btrfsData := data[realOffset:] if len(btrfsData) < superblockEnd { return nil, fmt.Errorf("btrfs not enough data") } fields := []StructField{ {"header_checksum", "u32"}, {"unused1", "u32"}, {"unused2", "u64"}, {"unused3", "u64"}, {"unused4", "u64"}, {"uuid_p1", "u64"}, {"uuid_p2", "u64"}, {"block_phys_addr", "u64"}, {"flags", "u64"}, {"magic", "u64"}, {"generation", "u64"}, {"root_tree_address", "u64"}, {"chunk_tree_address", "u64"}, {"log_tree_address", "u64"}, {"log_root_transid", "u64"}, {"total_bytes", "u64"}, {"bytes_used", "u64"}, {"root_dir_objid", "u64"}, {"num_devices", "u64"}, {"sector_size", "u32"}, {"node_size", "u32"}, {"leaf_size", "u32"}, {"stripe_size", "u32"}, } headerData := btrfsData[superblockOffset:superblockEnd] hdr, err := Parse(headerData, fields, "little") if err != nil { return nil, fmt.Errorf("btrfs header parse failed") } // Validate CRC32C calcCRC := crc32c(headerData[crcStart:]) if hdr["header_checksum"] != int(calcCRC) { return nil, fmt.Errorf("btrfs crc mismatch") } return &SignatureResult{ Offset: realOffset, Size: hdr["total_bytes"], Confidence: ConfidenceMedium, Description: fmt.Sprintf( "%s, node size: %d, sector size: %d, leaf size: %d, stripe size: %d, bytes used: %d, total size: %d bytes", BTRFSDescription, hdr["node_size"], hdr["sector_size"], hdr["leaf_size"], hdr["stripe_size"], hdr["bytes_used"], hdr["total_bytes"]), }, nil } // crc32c computes a CRC-32C (Castagnoli) checksum. func crc32c(data []byte) uint32 { // CRC-32C polynomial: 0x1EDC6F41 const poly = 0x82F63B78 // reversed representation crc := uint32(0xFFFFFFFF) for _, b := range data { crc ^= uint32(b) for i := 0; i < 8; i++ { if crc&1 != 0 { crc = (crc >> 1) ^ poly } else { crc >>= 1 } } } return crc ^ 0xFFFFFFFF } // --------------------------------------------------------------------------- // LogFS // --------------------------------------------------------------------------- const LogFSDescription = "LogFS file system" const LogFSMagicOffset = 0x18 func logfs_magic() [][]byte { return [][]byte{ {0x7A, 0x3A, 0x8E, 0x5C, 0xB9, 0xD5, 0xBF, 0x67}, } } func logfs_parser(data []byte, offset int) (*SignatureResult, error) { if offset < LogFSMagicOffset { return nil, fmt.Errorf("logfs offset too small") } realOffset := offset - LogFSMagicOffset logfsData := data[realOffset:] fields := []StructField{ {"magic", "u64"}, {"crc32", "u32"}, {"ifile_levels", "u8"}, {"iblock_levels", "u8"}, {"data_levels", "u8"}, {"segment_shift", "u8"}, {"block_shift", "u8"}, {"write_shift", "u8"}, {"pad0", "u32"}, {"pad1", "u16"}, {"filesystem_size", "u64"}, {"segment_size", "u32"}, {"bad_seg_reserved", "u32"}, {"feature_incompat", "u64"}, {"feature_ro_compat", "u64"}, {"feature_compat", "u64"}, {"feature_flags", "u64"}, {"root_reserve", "u64"}, {"speed_reserve", "u64"}, } if LogFSMagicOffset >= len(logfsData) { return nil, fmt.Errorf("logfs not enough data") } hdr, err := Parse(logfsData[LogFSMagicOffset:], fields, "big") if err != nil { return nil, fmt.Errorf("logfs superblock parse failed") } if hdr["pad0"] != 0 || hdr["pad1"] != 0 { return nil, fmt.Errorf("logfs padding fields not zero") } totalSize := hdr["filesystem_size"] return &SignatureResult{ Offset: realOffset, Size: totalSize, Confidence: ConfidenceMedium, Description: fmt.Sprintf("%s, total size: %d bytes", LogFSDescription, totalSize), }, nil } // --------------------------------------------------------------------------- // UBI // --------------------------------------------------------------------------- const UBIImageDescription = "UBI image" func ubi_magic() [][]byte { return [][]byte{ {0x55, 0x42, 0x49, 0x23, 0x01}, // "UBI#" + version 1 } } func ubi_parser(data []byte, offset int) (*SignatureResult, error) { ubiECFields := []StructField{ {"magic", "u32"}, {"version", "u8"}, {"padding1", "u24"}, {"ec", "u64"}, {"volume_id_header_offset", "u32"}, {"data_offset", "u32"}, {"image_sequence_number", "u32"}, {"padding2", "u64"}, {"padding3", "u64"}, {"padding4", "u64"}, {"padding5", "u64"}, {"header_crc", "u32"}, } ecHeaderSize := StructSize(ubiECFields) crcDataSize := ecHeaderSize - 4 // minus the header_crc field ubiData := data[offset:] ecHdr, err := Parse(ubiData, ubiECFields, "big") if err != nil { return nil, fmt.Errorf("ubi ec header parse failed") } if ecHdr["data_offset"] < ecHeaderSize || ecHdr["volume_id_header_offset"] < ecHeaderSize { return nil, fmt.Errorf("ubi invalid offsets") } // Validate header CRC if crcDataSize > len(ubiData) { return nil, fmt.Errorf("ubi not enough data for crc") } if ubiCRC(ubiData[:crcDataSize]) != ecHdr["header_crc"] { return nil, fmt.Errorf("ubi ec header crc mismatch") } dataOffset := offset + ecHdr["data_offset"] volumeOffset := offset + ecHdr["volume_id_header_offset"] if dataOffset >= len(data) || volumeOffset >= len(data) { return nil, fmt.Errorf("ubi offsets out of bounds") } // Get image size by scanning volume headers imageSize, err := getUBIImageSize(data[offset:]) if err != nil { return nil, err } return &SignatureResult{ Offset: offset, Size: imageSize, Confidence: ConfidenceHigh, Description: fmt.Sprintf("%s, version: %d, image size: %d bytes", UBIImageDescription, ecHdr["version"], imageSize), }, nil } func getUBIImageSize(ubiData []byte) (int, error) { volMagic := []byte{0x55, 0x42, 0x49, 0x21, 0x01} // "UBI!" + version 1 ubiVolFields := []StructField{ {"magic", "u32"}, {"version", "u8"}, {"volume_type", "u8"}, {"copy_flag", "u8"}, {"compat_type", "u8"}, {"volume_id", "u32"}, {"logical_erase_block_number", "u32"}, {"padding1", "u32"}, {"data_size", "u32"}, {"used_erase_block_count", "u32"}, {"data_padding_size", "u32"}, {"data_crc", "u32"}, {"padding2", "u32"}, {"sequence_number", "u64"}, {"padding3", "u64"}, {"padding4", "u32"}, {"header_crc", "u32"}, } volHeaderSize := StructSize(ubiVolFields) volCRCDataSize := volHeaderSize - 4 blockCount := 0 previousVolumeOffset := 0 possibleLEBSizes := make(map[int]int) // Scan for volume headers for i := 0; i <= len(ubiData)-len(volMagic); { idx := bytes.Index(ubiData[i:], volMagic) if idx < 0 { break } thisVolumeOffset := i + idx if thisVolumeOffset+volHeaderSize > len(ubiData) { break } volHdr, err := Parse(ubiData[thisVolumeOffset:], ubiVolFields, "big") if err == nil && volHdr["padding1"] == 0 && volHdr["padding2"] == 0 && volHdr["padding3"] == 0 && volHdr["padding4"] == 0 { // Validate CRC if thisVolumeOffset+volCRCDataSize <= len(ubiData) { if ubiCRC(ubiData[thisVolumeOffset:thisVolumeOffset+volCRCDataSize]) == volHdr["header_crc"] { blockCount++ if previousVolumeOffset != 0 { lebSize := thisVolumeOffset - previousVolumeOffset possibleLEBSizes[lebSize]++ } previousVolumeOffset = thisVolumeOffset } } } i = thisVolumeOffset + 1 } // Pick the most common LEB size lebSize := 0 bestCount := 0 for size, count := range possibleLEBSizes { if count > bestCount { lebSize = size bestCount = count } } if lebSize != 0 && blockCount != 0 { return blockCount * lebSize, nil } return 0, fmt.Errorf("ubi could not determine image size") } // --------------------------------------------------------------------------- // UBIFS // --------------------------------------------------------------------------- const UBIFSDescription = "UBIFS image" func ubifs_magic() [][]byte { return [][]byte{ {0x31, 0x18, 0x10, 0x06}, } } func ubifs_parser(data []byte, offset int) (*SignatureResult, error) { const maxGroupType = 2 const crcStartOffset = 8 const superblockNodeType = 6 const superblockStructExtraSize = 3968 ubiSBFields := []StructField{ {"magic", "u32"}, {"header_crc", "u32"}, {"sequence_number", "u64"}, {"node_len", "u32"}, {"node_type", "u8"}, {"group_type", "u8"}, {"padding1", "u32"}, {"key_hash", "u8"}, {"key_format", "u8"}, {"flags", "u32"}, {"min_io_size", "u32"}, {"leb_size", "u32"}, {"leb_count", "u32"}, {"max_leb_count", "u32"}, {"max_bud_bytes", "u64"}, {"log_lebs", "u32"}, {"lpt_lebs", "u32"}, {"orph_lebs", "u32"}, {"jhead_count", "u32"}, {"fanout", "u32"}, {"lsave_count", "u32"}, {"fmt_version", "u32"}, {"default_compression", "u16"}, {"padding2", "u16"}, {"rp_uid", "u32"}, {"rp_gid", "u32"}, {"rp_size", "u64"}, {"time_gran", "u32"}, {"uuid_p1", "u64"}, {"uuid_p2", "u64"}, {"ro_compat_version", "u32"}, } sbStructSize := StructSize(ubiSBFields) + superblockStructExtraSize ubisData := data[offset:] hdr, err := Parse(ubisData, ubiSBFields, "little") if err != nil { return nil, fmt.Errorf("ubifs superblock parse failed") } if hdr["padding1"] != 0 || hdr["padding2"] != 0 { return nil, fmt.Errorf("ubifs padding fields not zero") } if hdr["node_type"] != superblockNodeType { return nil, fmt.Errorf("ubifs not a superblock node") } if hdr["group_type"] > maxGroupType { return nil, fmt.Errorf("ubifs invalid group type") } // Validate header CRC if crcStartOffset+sbStructSize-crcStartOffset > len(ubisData) { return nil, fmt.Errorf("ubifs not enough data for crc") } crcData := ubisData[crcStartOffset:sbStructSize] if ubiCRC(crcData) != hdr["header_crc"] { return nil, fmt.Errorf("ubifs header crc mismatch") } imageSize := hdr["leb_count"] * hdr["leb_size"] return &SignatureResult{ Offset: offset, Size: imageSize, Confidence: ConfidenceHigh, Description: fmt.Sprintf("%s, total size: %d bytes", UBIFSDescription, imageSize), }, nil } // ubiCRC computes a UBI checksum: NOT(CRC32(data)) & 0xFFFFFFFF func ubiCRC(data []byte) int { const ubiCRCInit = uint32(0xFFFFFFFF) return int((^CRC32(data)) & ubiCRCInit) } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- func containsInt(slice []int, val int) bool { for _, s := range slice { if s == val { return true } } return false }