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

1290 lines
38 KiB
Go

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
}