Appendix G — File System Reference

Purpose. A keyboard-side structural reference for the file systems you will meet on real evidence — NTFS, ext4, APFS, FAT32/exFAT, and HFS+ — giving you the on-disk signatures, offset tables, record anatomies, and, for each, the single most important question an examiner or recovery technician asks: what does "deleted" mean here, and what survives?

This appendix is a companion to Chapter 4 — File Systems, which teaches these structures in prose and worked examples. Here they are condensed for fast lookup. Everything is little-endian unless noted, all offsets are hexadecimal unless a decimal byte position is given for clarity, and "cluster" (NTFS/FAT) and "block" (ext4) are used as each file system uses them. Hex offsets, signatures, and field layouts are stated to be hand-checkable against a real image with xxd/dd; the file-signature magic numbers for file contents (as opposed to file-system structures) live in Appendix A — File Signatures Reference, and the command-line tools that read all of this are in Appendix H — Command-Line Reference.

How to use this appendix. When you sit down to a new image, work top-down: (1) identify the partition scheme and file system (§G.1), (2) read the volume geometry you need for offset math (§G.2), (3) jump to that file system's section for its record layout, and (4) check the deletion subsection so you pick the right recovery route before you touch user data. The master deletion table in §G.9 is the one to photocopy.


G.1 First moves: identify the file system

The first question on any media is "what is the layout, and what file systems are present?" Two layers answer it: the partition table at the front of the disk, and the file-system signature at the front of each partition.

G.1.1 Partition-table signatures (the disk's first sectors)

+--------------------------------------------------------------+
|  MBR  (LBA 0, 512 bytes)                                     |
|    0x000–0x1BD  bootstrap code (446 bytes)                   |
|    0x1BE–0x1FD  4 partition entries × 16 bytes               |
|    0x1FE–0x1FF  boot signature  55 AA   <-- must be present  |
+--------------------------------------------------------------+
|  GPT  (LBA 1)                                                |
|    "EFI PART"  45 46 49 20 50 41 52 54   <-- header magic    |
|    backup copy of header + entry array at END of disk        |
|    LBA 0 holds a "protective MBR": one 0xEE partition        |
+--------------------------------------------------------------+
Scheme Where Signature (hex) Notes
MBR offset 510 (0x1FE) 55 AA 4 primary entries; 32-bit LBA → 2.2 TB ceiling
GPT header LBA 1, offset 0 45 46 49 20 50 41 52 54 ("EFI PART") backup at last LBA; CRC32-protected; 128 entries typical

MBR partition entry (16 bytes): boot flag (1) · CHS start (3) · type (1) · CHS end (3) · LBA start (4) · sector count (4). Key type bytes: 0x07 NTFS/exFAT, 0x0B/0x0C FAT32, 0x83 Linux, 0x82 Linux swap, 0x05/0x0F extended, 0xEE GPT-protective. GPT entries lead with a type GUID: EBD0A0A2-… Microsoft Basic Data, 0FC63DAF-… Linux filesystem, C12A7328-… EFI System (FAT32), 7C3457EF-… Apple APFS, 48465300-… Apple HFS+. (Full partitioning treatment is in Chapter 4; MBR-vs-GPT recovery in Chapter 6.)

G.1.2 File-system signatures (the partition's first sectors)

This is the lookup that tells you, in one glance at a hex dump, what you are dealing with.

File system Look at byte… Signature (hex) ASCII Boot/VBR 55 AA?
NTFS offset 3 (0x03) 4E 54 46 53 20 20 20 20 NTFS␣␣␣␣ yes (at 0x1FE)
exFAT offset 3 (0x03) 45 58 46 41 54 20 20 20 EXFAT␣␣␣ yes
FAT32 offset 82 (0x52) 46 41 54 33 32 20 20 20 FAT32␣␣␣ yes
FAT12/16 offset 54 (0x36) 46 41 54 31 36 20… / FAT␣␣␣ FAT16␣␣ / FAT␣␣␣ yes
ext2/3/4 byte 1080 (SB+0x38) 53 EF (magic 0xEF53) n/a
APFS container block 0, offset 0x20 4E 58 53 42 NXSB n/a
APFS volume volume SB, offset 0x20 41 50 53 42 APSB n/a
HFS+ byte 1024 (0x400) 48 2B H+ n/a
HFSX byte 1024 (0x400) 48 58 HX n/a

Tool Tip. Do not trust the partition type byte alone — it is frequently wrong on removable media and is trivially altered. Confirm by reading the actual file-system signature at the offsets above (fsstat does this for you), and treat a type byte that disagrees with the on-disk signature as a finding worth noting. The single fastest manual check is xxd -l 16 -s <partition_start> image.dd for the NTFS/exFAT/FAT cases and xxd -l 2 -s $((part_start+1080)) image.dd for ext.

G.1.3 Identification with The Sleuth Kit

mmls image.dd                 # partition table: scheme, each part's start sector, type
fsstat -o <start> image.dd    # file-system type, cluster/block size, volume serial, MFT/SB info
img_stat image.dd             # image format & sector size (E01/raw/etc.)

mmls gives you each partition's start sector (feed it to every later command via -o); fsstat confirms the file system and — critically — hands you the cluster/block size you need for §G.2's math. Full options: Appendix H.


G.2 Offset & cluster arithmetic (the math you redo constantly)

Every manual recovery comes back to converting a logical unit (cluster/block) into an absolute byte offset on the image. Get the unit size wrong and you land in the wrong place — the most common hand-analysis error.

byte_offset = (partition_start_sector + unit_number × sectors_per_unit) × bytes_per_sector

  NTFS/FAT/exFAT:  unit = cluster
  ext4:            unit = block      (sectors_per_block = block_size / sector_size)
  APFS:            unit = block (default 4096)

Quick forms:
  cluster_size      = sectors_per_cluster × bytes_per_sector        (e.g. 8 × 512 = 4096)
  cluster→byte      = partition_byte_start + cluster × cluster_size
  sector↔byte       = sector × 512   (or × 4096 on Advanced-Format media)

Worked example (the values fsstat would give you): partition starts at sector 2048, 512-byte sectors, 8 sectors/cluster (4096-byte clusters). Cluster 22,068 sits at (2048 + 22068 × 8) × 512 = (2048 + 176544) × 512 = 178592 × 512 = 91,439,104 bytes — i.e. dd if=image.dd bs=512 skip=178592 count=8 reads that cluster. Read the actual sectors per cluster from the boot sector or fsstat first; a 4096-vs-512 mix-up puts you eight times too deep.

Unit NTFS FAT32 exFAT ext4 APFS HFS+
Allocation unit cluster cluster cluster block block allocation block
Typical size 4 KB 4–32 KB 32–128 KB 4 KB 4 KB 4 KB
Addressing term LCN / VCN cluster # cluster # block # paddr (block) allocation block #
First data unit base cluster 0 = volume start cluster 2 cluster 2 block 0 (or 1 if 1 KB) block 0 block 0

Limitation. "Advanced Format" drives expose 4096-byte physical sectors (sometimes 512-byte logical via 512e emulation). Confirm bytes_per_sector from the boot sector / fsstat rather than assuming 512, or every offset is off by 8×. See Chapter 2 for the sector/cluster model and Chapter 3 for the media.


G.3 NTFS

NTFS's design idea: everything is a file, including the metadata. Master that and the rest follows. NTFS is the file system you will see most in corporate forensics and consumer Windows recovery. Owner chapter: Chapter 4; Windows artifacts built on it (Recycle Bin, registry, Prefetch) are Chapter 16.

G.3.1 Boot sector / VBR (BIOS Parameter Block)

First sector of the partition. After a 3-byte jump, offset 3 is the OEM ID NTFS␣␣␣␣.

NTFS boot sector (Volume Boot Record) — first sector of the partition
Offset  Size  Field                          Typical / notes
 0x00    3    Jump instruction               EB 52 90
 0x03    8    OEM ID                          "NTFS    " (4E 54 46 53 20 20 20 20)
 0x0B    2    Bytes per sector                512 (or 4096)
 0x0D    1    Sectors per cluster             8  => 4096-byte clusters
 0x0E    2    Reserved sectors                0 (NTFS uses none)
 0x15    1    Media descriptor                0xF8 (fixed disk)
 0x18    2    Sectors per track               (geometry, legacy)
 0x1A    2    Number of heads                 (geometry, legacy)
 0x1C    4    Hidden sectors                  = partition start LBA
 0x28    8    Total sectors in volume
 0x30    8    Starting cluster of $MFT        e.g. 786432
 0x38    8    Starting cluster of $MFTMirr    often 2
 0x40    1    Clusters per MFT record         signed: 0xF6 = -10 => 2^10 = 1024-byte records
 0x44    1    Clusters per index buffer       signed: 0x01 => 1 cluster, or 2^|v| bytes
 0x48    8    Volume serial number            record it — ties FS to a specific volume
 0x50    4    Checksum
 0x1FE   2    Boot signature                  55 AA

The byte at 0x40 is signed: when negative, MFT-record size is 2^|value| bytes (0xF6 = −10 → 1024 bytes, the near-universal default); when positive, it is that many clusters. The same signed rule applies to the index-buffer byte at 0x44. The volume serial number at 0x48 is forensically useful — note it; it links an image to a specific volume.

G.3.2 NTFS metadata files (reserved MFT entries)

Entry  Name        Purpose
  0    $MFT        the Master File Table itself
  1    $MFTMirr    backup of the first MFT records (disaster recovery)
  2    $LogFile    transaction journal (metadata redo/undo)
  3    $Volume     volume name, version, dirty flag
  4    $AttrDef    attribute definitions
  5    .  (dot)    the root directory
  6    $Bitmap     cluster allocation bitmap (free vs. used; 1 bit per cluster)
  7    $Boot       the boot sector / bootstrap (a real file over the VBR)
  8    $BadClus    map of bad clusters
  9    $Secure     security descriptors / ACLs (de-duplicated)
 10    $UpCase     uppercase translation table (for case-insensitive compare)
 11    $Extend     directory of optional extensions, holding child files:
                     $UsnJrnl (change journal), $ObjId, $Quota, $Reparse, $RmMetadata
 12–15  (reserved)

$Bitmap` (entry 6) is allocation truth: one bit per cluster, set = in use. `$Extend\$UsnJrnl` is the change journal (see §G.3.9). `$Secure centralizes ACLs so file records reference a security ID rather than storing the descriptor inline.

G.3.3 MFT record (FILE record) anatomy

The MFT is an array of fixed-size records (1024 bytes by default). Every file and directory has at least one. The record number is the file's identity. To recover a deleted file on NTFS, find its MFT record and read what it still says.

+=============================================================+
|                 NTFS MFT RECORD (1024 bytes)                |
+=============================================================+
| 0x00  "FILE"  (46 49 4C 45)  signature  ("BAAD" = corrupt)  |
| 0x04  Offset to update-sequence (fixup) array               |
| 0x06  Size of fixup array (in 2-byte words)                 |
| 0x08  $LogFile sequence number (LSN)            (8 bytes)   |
| 0x10  Sequence number (incremented on entry reuse)          |
| 0x12  Hard-link count                                       |
| 0x14  Offset to first attribute  (e.g. 0x38)               |
| 0x16  FLAGS  <-- 0x01 = IN USE,  0x02 = DIRECTORY           |
| 0x18  Used size of this record   (bytes actually used)      |
| 0x1C  Allocated size of record   (1024)                     |
| 0x20  Base record reference (0 if this IS the base)         |
| 0x28  Next attribute ID                                     |
| 0x2C  (XP+) this MFT record number                          |
+-------------------------------------------------------------+
|  ATTRIBUTES (variable, packed back-to-back, type-ordered):  |
|    [0x10] $STANDARD_INFORMATION   timestamps, DOS flags     |
|    [0x30] $FILE_NAME              name + parent + timestamps |
|    [0x80] $DATA                   the file's content        |
|    ...                                                       |
|    FF FF FF FF  end-of-attributes marker                    |
+-------------------------------------------------------------+
|  ...slack to 1024 bytes (old attribute data may linger)...  |
+=============================================================+

The byte at 0x16 (flags) decides "deleted or not." Bit 0 (0x01) set = in use (live file); clear = free (deleted) — but the rest of the record is left intact until the entry is recycled. Flag values: 0x00 deleted file, 0x01 in-use file, 0x02 deleted directory, 0x03 in-use directory. (The fixup/update-sequence array at 0x04 is a consistency mechanism: the last 2 bytes of every 512-byte stride are swapped out to a saved array and must be restored before the record is read raw — TSK does this for you; hand-parsers must.)

G.3.4 NTFS attribute type codes

Type   Name                       Notes
0x10   $STANDARD_INFORMATION      always resident: 4 timestamps + DOS flags + IDs
0x20   $ATTRIBUTE_LIST            used when attributes overflow one record
0x30   $FILE_NAME                 name (UTF-16) + parent ref + 2nd timestamp set
0x40   $OBJECT_ID                 64-bit object id (link tracking)
0x50   $SECURITY_DESCRIPTOR       ACL (mostly moved to $Secure on modern NTFS)
0x60   $VOLUME_NAME               (on $Volume)
0x70   $VOLUME_INFORMATION        version + dirty flag (on $Volume)
0x80   $DATA                      file content; can be multiple (see ADS)
0x90   $INDEX_ROOT                directory B-tree root (resident)
0xA0   $INDEX_ALLOCATION          directory B-tree non-resident nodes
0xB0   $BITMAP                    allocation bitmap for an index / for $MFT
0xC0   $REPARSE_POINT             junctions, symlinks, mount points
0xD0   $EA_INFORMATION            extended-attribute info
0xE0   $EA                        extended attributes
0x100  $LOGGED_UTILITY_STREAM     e.g. EFS $EFS encryption metadata

G.3.5 $STANDARD_INFORMATION vs $FILE_NAME — the dual timestamps

NTFS stores two independent sets of four timestamps. This is the examiner's secret weapon for detecting timestomping (full treatment: Chapter 21 — Timeline Analysis and Chapter 30 — Anti-Forensics).

$STANDARD_INFORMATION (type 0x10, resident)      $FILE_NAME (type 0x30)
Offset Field                                      Offset Field
 0x00  Created  (FILETIME)                         0x00  Parent dir reference (6B entry + 2B seq)
 0x08  Modified (FILETIME)                         0x08  Created  (FILETIME)
 0x10  MFT changed (FILETIME)                      0x10  Modified (FILETIME)
 0x18  Accessed (FILETIME)                         0x18  MFT changed (FILETIME)
 0x20  DOS flags (RO/Hidden/System/Archive…)       0x20  Accessed (FILETIME)
 0x24  Max versions / version / class id           0x28  Allocated size
 0x30  Owner id / Security id / Quota / USN         0x30  Real size
                                                    0x38  Flags
                                                    0x40  Name length (chars, 1B)
                                                    0x41  Namespace (0 POSIX,1 Win32,2 DOS,3 both)
                                                    0x42  Name (UTF-16LE)
$STANDARD_INFORMATION` (`$SI) $FILE_NAME` (`$FN)
What Explorer shows yes no
Updated during normal use constantly only on create / move / rename
Settable by user-space tools yes (one API call, no privilege) no (kernel-written)
Forensic role the timestamps liars edit the timestamps that keep the truth

Red flag for timestomping: a $SI` time **earlier than** the corresponding `$FN time (logically impossible in normal operation), or $SI` times that are suspiciously round (`…12:00:00.0000000`) while `$FN is sub-second precise. NTFS records the truth twice; tamperers usually edit only one copy. Timestamps are FILETIME: 100-nanosecond ticks since 1601-01-01 UTC (see §G.8).

G.3.6 Attribute header: resident vs. non-resident

Every attribute begins with a common header; the byte at +0x08 (non-resident flag) decides where the content lives.

COMMON HEADER (first 16 bytes of every attribute)
 0x00  Attribute type code (4)          0x0A  Name offset (2)
 0x04  Total length incl. header (4)     0x0C  Flags (2): 0x0001 compressed,
 0x08  Non-resident flag (1)                          0x4000 encrypted, 0x8000 sparse
 0x09  Name length (1)                   0x0E  Attribute id (2)

IF RESIDENT (+0x08 == 0x00): content is INSIDE the MFT record
 0x10  Content length (4)                0x16  Indexed flag (1)
 0x14  Content offset (2)                0x17  padding
 [content follows]                        <-- small files live entirely here

IF NON-RESIDENT (+0x08 == 0x01): content is out in clusters, mapped by data runs
 0x10  Starting VCN (8)                  0x28  Allocated size (8)
 0x18  Last VCN (8)                      0x30  Real size (8)
 0x20  Data-run (mapping pairs) offset (2) 0x38  Initialized size (8)
 0x22  Compression unit size (2)          0x40  [data runs ...]

A resident $DATA` (files smaller than roughly 700–900 bytes, whatever fits in the 1024-byte record) means the *entire file is the MFT entry* — recover a small deleted text file by reading its still-free record, no cluster-walking needed. When too big, `$DATA goes non-resident and the record holds a data-run map instead.

G.3.7 Decoding data runs (mapping pairs)

A non-resident attribute maps its clusters as a sequence of compact runs. Each run: a header byte whose low nibble = byte-count of the length field and high nibble = byte-count of the offset field, followed by those bytes. Offsets after the first are signed deltas from the previous run's start (this is how NTFS compactly describes fragmentation).

Decoding:  21 18 34 56 00
  21    -> header: low nibble 1 = length is 1 byte; high nibble 2 = offset is 2 bytes
  18    -> run length = 0x18 = 24 clusters
  34 56 -> starting LCN = 0x5634 = 22,068   (little-endian)
  00    -> end-of-runs marker
  Meaning: 24 clusters starting at cluster 22,068 (24 × 4096 = 98,304 bytes here).

Fragmented file example: 31 08 00 40 00  21 10 E0 FF
  31 08 000040  -> 8 clusters at LCN 0x4000 = 16,384
  21 10 FFE0    -> 16 clusters at LCN 16,384 + (signed 0xFFE0 = -32) = 16,352
  (each subsequent offset is a SIGNED delta from the previous run's start)

As long as the deleted record is readable, the data runs tell you exactly which clusters to read, in order, to rebuild even a badly fragmented file — which is why NTFS recovery is the most forgiving of the major file systems.

G.3.8 Alternate Data Streams (ADS)

Because a file is a bag of attributes, it can have more than one $DATA. The unnamed stream is the visible content; named streams (file.txt:streamname) hang off the same record invisibly — not shown by dir, Explorer, or the reported size.

Legit:   downloaded.exe:Zone.Identifier   <- Mark-of-the-Web; records zone (3 = internet) + source URL
Hiding:  report.docx:payload.exe           <- an entire executable concealed in a named stream
Get-Item .\installer.exe -Stream *                       # list all streams on a file
Get-Content .\installer.exe -Stream Zone.Identifier      # read Mark-of-the-Web (source URL!)
Get-ChildItem -Recurse -File | ForEach-Object { Get-Item $_.FullName -Stream * } |
    Where-Object { $_.Stream -ne ':$DATA' } |
    Select-Object FileName, Stream, Length               # hunt non-default streams in a tree

dir /r reveals streams plain dir hides; TSK's fls lists each stream as a separate attribute id. The presence of an unexpected named stream — and the Mark-of-the-Web's recorded source URL — are themselves findings.

G.3.9 $LogFile and $UsnJrnl — NTFS keeps a diary

$LogFile  (entry 2)  transaction journal: redo/undo of metadata ops. A short, fixed window
                     of RECENT changes (creates/deletes/renames). Often reconstructs the last
                     minutes–hours even after the MFT moved on.
$UsnJrnl  ($Extend\$UsnJrnl, read from the :$J stream)  logs EVERY change to EVERY file:
                     USN · timestamp · file MFT reference · reason flags
   Reason flags: FILE_CREATE, FILE_DELETE, DATA_OVERWRITE, DATA_EXTEND,
                 RENAME_OLD_NAME, RENAME_NEW_NAME, BASIC_INFO_CHANGE, CLOSE …

The $UsnJrnl can prove a file named clientlist_final.xlsx was created, renamed, then deleted at a specific time — even if its MFT entry has since been reused and the file is otherwise unrecoverable. It remembers the name and the act of deletion when nothing else does.

fsutil usn readjournal C: csv | Select-Object -First 20   # dump recent USN records
fsutil usn queryjournal C:                                # confirm journal + parameters

G.3.10 NTFS deletion — what remains

A real delete (Shift+Delete / empty Recycle Bin; ordinary delete just moves the file to $Recycle.Bin, see Chapter 16) does only three cheap things:

1. Clear the in-use flag at MFT offset 0x16     (record now "free")
2. Remove the file's entry from the parent directory B-tree   (name no longer resolves)
3. Mark the file's clusters free in $Bitmap

NOT done: erase the MFT record, wipe the $DATA clusters, or touch the data runs.

What "deleted" means on NTFS: the pointers are removed (directory index entry + allocation bits) and one flag is cleared (in-use). The full record — name, both timestamp sets, and the exact cluster map via data runs — persists, and the content clusters persist, until (a) the MFT entry is reused or (b) the clusters are overwritten. Recover by reading the deleted record and following its data runs (icat). Quick format writes a fresh, nearly empty $MFT and root but does not zero the data area or (usually) the old MFT — old records and content linger in unallocated space (recover via metadata where the old MFT survives, carve where the new MFT overwrote it). Full format zeroes the volume and genuinely destroys it.


G.4 ext4

ext4 is the Linux default — servers, embedded devices, and Android's /data. Its defining contrast with NTFS: deletion strips the file's block map, so recovery runs through the journal or carving, not the inode. Owner chapter: Chapter 4; Linux artifacts: Chapter 17.

G.4.1 On-disk layout

+--------+-------------------------------------------------------------+
| boot   |  Block Group 0  | Block Group 1  | ... | Block Group N      |
| (1 KB) |                 |                |     |                    |
+--------+-------------------------------------------------------------+
            |
            v  inside a block group (with flexible block groups, some are pooled):
   +-----------+-----------+--------------+--------------+-------------+----------+
   | super-    | group     | block        | inode        | inode       | data     |
   | block(*)  | descriptors| bitmap      | bitmap       | table       | blocks   |
   +-----------+-----------+--------------+--------------+-------------+----------+
   (*) superblock + descriptors are present in group 0 and BACKED UP in some later
       groups (sparse_super). Backups are your friend if group 0 is damaged:
       e.g. dumpe2fs / mke2fs -n show backup superblock locations.

Default block size is 4096 bytes (block_size = 1024 << s_log_block_size). The block bitmap and inode bitmap (one bit per object, set = allocated) are ext4's equivalent of NTFS $Bitmap.

G.4.2 Superblock

Begins at byte 1024 from the start of the volume (after the boot area). Magic 0xEF53 at superblock-offset 0x38 = disk byte 1080.

ext4 superblock (starts at byte 1024)
SB off  Disk byte  Field                       Notes
 0x00    1024      s_inodes_count              total inodes
 0x04    1028      s_blocks_count_lo           total blocks (low 32)
 0x0C    1036      s_free_blocks_count_lo
 0x10    1040      s_free_inodes_count
 0x14    1044      s_first_data_block          0 if 4 KB blocks, 1 if 1 KB
 0x18    1048      s_log_block_size            block size = 1024 << this
 0x20    1056      s_blocks_per_group
 0x28    1064      s_inodes_per_group
 0x2C    1068      s_mtime                     last mount time (Unix epoch)
 0x30    1072      s_wtime                     last write time
 0x38    1080      s_magic = 0xEF53            "53 EF" little-endian  <-- the FS fingerprint
 0x3A    1082      s_state                     1 = clean, 2 = errors
 0x4C    1100      s_rev_level
 0x54    1108      s_first_ino                 first non-reserved inode (usually 11)
 0x58    1112      s_inode_size                inode size in bytes (128 or 256)
 0x5C    1116      s_feature_compat
 0x60    1120      s_feature_incompat          bit 0x40 = EXTENTS in use; 0x200 = 64BIT
 0x64    1124      s_feature_ro_compat
 0x68    1128      s_uuid (16)                 volume UUID — record it
 0x78    1144      s_volume_name (16)

s_inode_size (0x58) is 256 on modern ext4, which is what makes room for crtime and nanosecond fields. The EXTENTS feature flag lives in s_feature_incompat (0x60), bit 0x40.

G.4.3 Inode

ext4's per-file metadata, default 256 bytes. Analog of the MFT record with one decisive difference: the inode holds no filename — the name lives only in the directory entry that points to this inode by number.

+=====================================================================+
|                       ext4 INODE (256 bytes)                        |
+=====================================================================+
| 0x00  i_mode          file type (high 4 bits) + permissions (low 12)|
| 0x02  i_uid           owner UID (low 16)                            |
| 0x04  i_size_lo       file size, low 32 bits                        |
| 0x08  i_atime         ACCESS time   (Unix epoch seconds)            |
| 0x0C  i_ctime         inode CHANGE time (metadata; NOT creation)    |
| 0x10  i_mtime         content MODIFY time                           |
| 0x14  i_dtime         DELETION time  <-- 0 while live; set on delete |
| 0x18  i_gid           group GID (low 16)                            |
| 0x1A  i_links_count   hard-link count  <-- reaches 0 => inode freed  |
| 0x1C  i_blocks_lo     count of 512-byte sectors used                |
| 0x20  i_flags         0x80000 = EXTENTS used for this file          |
| 0x28  i_block[15]     60 bytes: EXTENT TREE (ext4)                  |
|                        OR (ext2/3) 12 direct + 1 indirect +         |
|                        1 double-indirect + 1 triple-indirect ptrs   |
| 0x6C  i_size_high     file size, high 32 bits (for large files)     |
| 0x80  i_extra_isize   size of the extra fields (when inode > 128 B) |
| 0x90  i_crtime        CREATION time (extra field; ext4 only)        |
+=====================================================================+
   NO filename here. The name is in the directory entry (§G.4.5).

Timestamp notes: ext4 tracks atime / mtime / ctime and, uniquely vs. ext2/3, crtime (true creation, at 0x90 in the extra fields). i_ctime is inode change, not creation — a classic trip-up. A nonzero i_dtime on an inode whose bitmap bit is clear flags "this inode held a file deleted at this time." All ext times are seconds since the Unix epoch (UTC), with extra nanosecond bits in the post-128-byte region (see §G.8).

G.4.4 Extents

ext4 replaced ext2/3's indirect block pointers with extents(logical_block, length, physical_block) triples that map contiguous ranges efficiently. The i_block area holds an extent tree: a 12-byte header (magic 0xF30A) plus up to four inline entries; larger/fragmented files push the tree into index blocks.

ext4_extent_header (12 bytes)         ext4_extent (12 bytes, leaf: eh_depth == 0)
 0x00  eh_magic = 0xF30A ("0A F3")     0x00  ee_block     first logical block covered
 0x02  eh_entries  valid entries       0x04  ee_len       block count (contiguous)
 0x04  eh_max      capacity            0x06  ee_start_hi  physical start, high 16 bits
 0x06  eh_depth    0 = leaf (extents)  0x08  ee_start_lo  physical start, low 32 bits
 0x08  eh_generation                  ext4_extent_idx (12 bytes, interior: eh_depth > 0)
                                        0x00 ei_block  0x04 ei_leaf_lo  0x08 ei_leaf_hi

ee_len > 32768 marks an uninitialized (preallocated) extent; subtract 32768 for the real length. The extent tree is what recovery tools follow once they retrieve a pre-deletion copy of the inode from the journal.

G.4.5 Directory entries and the journal

A directory is a file whose content is a chain of variable-length entries:

ext4 linked directory entry (ext4_dir_entry_2)
 0x00  inode        (4)   inode number (0 = unused slot)
 0x04  rec_len      (2)   length of THIS entry (lets entries be skipped/absorbed)
 0x06  name_len     (1)
 0x07  file_type    (1)   1 reg, 2 dir, 3 chr, 4 blk, 5 fifo, 6 sock, 7 symlink
 0x08  name         (name_len bytes, no NUL)

The journal (jbd2, reserved inode 8) is ext4's crash-safety mechanism and your recovery lifeline. Modes: journal (data+metadata journaled — safest, slowest), ordered (default — metadata journaled, data forced to disk first), writeback (metadata only). Because it records metadata before commit, it frequently holds a recent pre-deletion copy of an inode, including the block map you need.

G.4.6 ext4 deletion — what remains

unlink():
  1. Remove the directory entry — usually by EXTENDING the previous entry's rec_len to
     swallow it, so the deleted NAME often still sits readable in the directory block slack.
  2. Decrement i_links_count.
  3. When links reach 0: clear the inode bitmap bit, stamp i_dtime,
     free the data blocks in the block bitmap.

The divergence from NTFS:
  ext2  left the inode's block pointers intact   -> classic debugfs undelete worked
  ext3  DELIBERATELY ZEROED the block pointers    -> easy undelete broke
  ext4  extents are likewise unreliable post-delete (live deleted inode no longer maps)

What "deleted" means on ext4: the directory entry is unlinked (the name often lingers in directory slack), and once links hit zero the inode is freed, i_dtime stamped, and the block map is no longer trustworthy on the live inode. The data blocks themselves remain until overwritten. Recovery route: (1) mine the journal for a pre-deletion inode copy whose extents still mapped the data (extundelete, ext4magic), then follow that map; (2) salvage deleted names from directory slack; (3) carve when the map is gone everywhere (Chapter 7). Time-critical: the journal is a fixed-size ring — once it wraps, the inode copy you needed is gone, even if you never touched the disk. Stop writing, pull power, image. Mount images read-only with -o ro,loop,noload to avoid journal replay.


G.5 APFS

APFS replaced HFS+ across Apple's lineup in 2017 — every modern Mac, iPhone, iPad. Built for flash, organized as checksummed B-trees of objects, copy-on-write throughout, encryption baked in. Owner chapter: Chapter 4; encryption (FileVault) in Chapter 29; the SSD/TRIM reality in Chapter 9.

G.5.1 Containers, volumes, and objects

+===========================================================================+
|  APFS CONTAINER  (occupies a partition; superblock magic NXSB at off 0x20) |
|  +---------------------+   space is SHARED by all volumes (no fixed sizes)  |
|  | Container Superblock|   - checkpoint ring (descriptor + data areas)      |
|  |  (nx_superblock_t)  |   - object map (omap) B-tree                       |
|  +---------------------+   - space manager (allocation), reaper             |
|     |        |        |                                                     |
|     v        v        v                                                     |
|  +-------+ +-------+ +-------+   Each VOLUME: superblock magic APSB at 0x20  |
|  |Volume | |Volume | |Volume |   - its own file-system B-tree (fs-tree)      |
|  |"HD"   | |"Data" | |"VM"   |   - its own object map                       |
|  +-------+ +-------+ +-------+   - may be individually ENCRYPTED (FileVault) |
+===========================================================================+

Every object block (default 4096 B) begins with obj_phys_t:
  0x00 o_cksum  (8)  Fletcher-64 checksum   <-- detects corruption / tampering
  0x08 o_oid    (8)  object id
  0x10 o_xid    (8)  transaction id (monotonic; higher = newer)
  0x18 o_type   (2)+ flags    0x1C o_subtype (4)
  0x20 magic ("NXSB" container / "APSB" volume) ...

NXSB = 4E 58 53 42, APSB = 41 50 53 42, each at offset 0x20 of its superblock block (after the 32-byte object header). Space sharing is why a Mac's Macintosh HD (system, often a sealed snapshot) and Macintosh HD - Data volumes draw from one free pool. Every block is Fletcher-64 checksummed.

G.5.2 Copy-on-write, clones, snapshots, checkpoints

APFS never overwrites live data in place — modified blocks are written to new locations and metadata is repointed; old blocks are freed afterward. Consequences that dominate recovery:

Feature What it is Forensic/recovery value
Clone instant, space-free copy (two files share blocks until one changes) two "files" may share physical blocks — note when carving/attributing
Snapshot point-in-time, read-only image of a whole volume best recovery source: preserves files (incl. later-deleted) intact and mountable
Checkpoint periodic superblock+metadata write in a ring superseded checkpoints can reconstruct a prior B-tree state
COW new-location writes prior metadata may briefly linger before reuse

macOS takes local snapshots constantly (Time Machine hourly local snapshots; installers snapshot before updates) — frequently your way back to a deleted file. List them with tmutil listlocalsnapshots / or diskutil apfs listSnapshots.

G.5.3 File records and deletion

A file is a set of records in the volume fs-tree, keyed by object id + record type: an inode record (APFS_TYPE_INODE), directory-entry records (DIR_REC), extent records (FILE_EXTENT, mapping logical → physical), and xattr records. Inode timestamps are nanoseconds since the Unix epoch (create/mod/change/access). Creation adds these records and updates the space manager; deletion removes the directory-entry record and, at link-count zero, the inode and extent records, returning blocks to the space manager in a new checkpoint.

What "deleted" means on APFS: B-tree records are removed and blocks returned to the space manager; COW means a prior metadata state may linger briefly in superseded checkpoints, and snapshots may preserve the file outright. But APFS lives on SSDs that run TRIM — freed blocks are typically discarded/zeroed by the controller within seconds at the FTL level (Chapter 9). So the recovery window for freed content is near-zero; your real sources are snapshots → Time Machine → iCloud → live acquisition of an unlocked device, plus superseded checkpoints for very recent changes. And APFS volumes are usually encrypted (FileVault on Macs; effectively always-on hardware encryption on iOS) — without the key, the bytes are ciphertext and no structure knowledge helps (Chapter 29).


G.6 FAT32 / exFAT

The file system on the SD card, thumb drive, dashcam, and a thousand embedded gadgets — the simplest to parse by hand and often the easiest to recover from, with one famous trap. Owner chapter: Chapter 4.

G.6.1 FAT volume layout and boot sector

+-----------------+--------------+--------------+----------------+-------------------+
| Reserved region | FAT #1       | FAT #2       | Root directory | Data region       |
| (boot + FSInfo) | (copy 1)     | (copy 2)     | (FAT12/16 only;| (clusters 2..N)   |
|                 |              |              | FAT32 root is  |                   |
|                 |              |              | a normal chain)|                   |
+-----------------+--------------+--------------+----------------+-------------------+
FAT32 boot sector (BPB)
Offset  Size  Field                         Typical
 0x00    3    Jump                           EB 58 90
 0x0B    2    Bytes per sector               512
 0x0D    1    Sectors per cluster            8
 0x0E    2    Reserved sectors               32 (FAT32)
 0x10    1    Number of FATs                 2
 0x16    2    Sectors per FAT (FAT12/16)     0 on FAT32
 0x1C    4    Hidden sectors                 = partition start LBA
 0x20    4    Total sectors (32-bit)
 0x24    4    Sectors per FAT (FAT32)
 0x2C    4    Root directory first cluster   usually 2
 0x30    2    FSInfo sector                  usually 1
 0x32    2    Backup boot sector             usually 6
 0x43    4    Volume serial number (ID)
 0x47   11    Volume label
 0x52    8    File-system type string        "FAT32   "  <-- the fingerprint
 0x1FE   2    Boot signature                 55 AA

G.6.2 The FAT and the cluster chain

The File Allocation Table is an array indexed by cluster number; each entry holds the next cluster in the file's chain — a singly linked list embedded in a table. The directory entry gives the start; the FAT gives the rest.

File starts at cluster 5 (from its directory entry), size 13,000 bytes:

  FAT array:  index ...  5    6    7    8    9   10 ...
              value ...  6    7    9    0   EOC   0 ...
                          \    \    \________\
  Chain: 5 -> 6 -> 7 -> 9 -> EOC      (cluster 8 skipped => fragmentation)
  0   = free cluster
  EOC = end of chain

FAT entry widths & markers:
  FAT16:  16-bit  free=0x0000  bad=0xFFF7  EOC=0xFFF8..0xFFFF
  FAT32:  28-bit  free=0x0000000  bad=0x0FFFFFF7  EOC=0x0FFFFFF8..0x0FFFFFFF
          (top 4 bits of each 32-bit FAT32 entry are reserved — mask them off)

G.6.3 Directory entries (8.3 + long names)

FAT 8.3 directory entry (32 bytes)
 0x00  11   Name (8.3). First byte: 0xE5 = DELETED; 0x00 = end/never used; 0x05 = real 0xE5
 0x0B   1   Attributes: 0x01 RO 0x02 Hidden 0x04 System 0x08 VolLabel 0x10 Dir 0x20 Archive
                        0x0F = long-name (LFN) entry
 0x0D   1   Creation time, tenths of a second
 0x0E   2   Creation time          0x10  2  Creation date
 0x12   2   Last access date       0x14  2  First cluster, HIGH 16 bits
 0x16   2   Last write time        0x18  2  Last write date
 0x1A   2   First cluster, LOW 16 bits
 0x1C   4   File size in bytes

Long File Name entry (attribute 0x0F), chained BEFORE the 8.3 entry, 13 UTF-16 chars each:
 0x00  seq (bit 0x40 = last LFN) · 0x01 chars1-5 · 0x0B 0x0F · 0x0D 8.3-checksum
 0x0E chars6-11 · 0x1A first-cluster=0 · 0x1C chars12-13

FAT date/time packing (LOCAL time, see §G.8):
  time (2B): bits 15-11 hours · 10-5 minutes · 4-0 (seconds / 2)
  date (2B): bits 15-9 (year - 1980) · 8-5 month · 4-0 day

G.6.4 FAT deletion — the 0xE5 trap

Delete does two things:
  1. First byte of the directory entry -> 0xE5   (entry marked available)
  2. The file's FAT chain entries      -> 0      (clusters freed)

What SURVIVES in the directory entry: first cluster + file size + (most of) the name.
What is LOST: the chain (which clusters after the first, and in what order).

Deleted entry on disk:
  0000: E5 4D 47 5F 30 34 32 33 4A 50 47 20 ...   .MG_0423JPG   (was IMG_0423.JPG)
         ^^ first char clobbered -> recovery shows "_MG_0423.JPG"
  off 0x14: 00 00         first cluster high = 0
  off 0x1A: 0C 00         first cluster low  = 0x000C = 12     <-- SURVIVES
  off 0x1C: B8 1E 00 00   size = 0x1EB8 = 7,864 bytes          <-- SURVIVES

What "deleted" means on FAT32: the entry's first byte becomes 0xE5 and the FAT chain is zeroed, but start cluster and size survive in the directory entry. Contiguous files recover trivially (read size bytes of consecutive clusters from the start). Fragmented files lose the path between fragments — recovery degrades to heuristics or carving (Chapter 7).

G.6.5 exFAT — built for big flash

exFAT (SDXC by spec) lifts FAT32's 4 GB/file and 2 TB/volume ceilings. Two design changes matter for recovery: it uses an allocation bitmap (not a FAT scan) for free space, and contiguous files set a NoFatChain flag so the FAT is not consulted at all — the file is fully described by start cluster + length in its directory record. Since freshly written cards lay files down contiguously, many exFAT files carry their whole map in the directory entry and survive deletion better than FAT32.

exFAT boot sector (key fields)
 0x03  8  "EXFAT   "          0x50 4 FAT offset (sectors)   0x54 4 FAT length
 0x40  8  Partition offset    0x58 4 Cluster-heap offset    0x5C 4 Cluster count
 0x48  8  Volume length       0x60 4 Root-directory 1st cluster   0x64 4 Volume serial
 0x6C  1  Bytes/sector shift (2^n)   0x6D 1 Sectors/cluster shift   0x6E 1 Number of FATs

exFAT directory entries come in typed SETS. Type byte: bit 0x80 = IN USE.
 0x85  File Directory Entry     (timestamps, attributes, secondary count)
 0xC0  Stream Extension          (flags incl. NoFatChain bit 0x02; name length;
                                  +0x14 first cluster; +0x18 data length)
 0xC1  File Name                 (15 UTF-16 chars each; chained)
 0x81  Allocation Bitmap   0x82  Up-case Table   0x83  Volume Label

On delete the IN-USE bit (0x80) is cleared:  0x85 -> 0x05,  0xC0 -> 0x40,  0xC1 -> 0x41.
Recovery scans for these cleared-bit signatures to rebuild deleted entry sets.

What "deleted" means on exFAT: the entry set's 0x80 in-use bits are cleared, but the Stream Extension still holds start cluster + length; NoFatChain (contiguous) files map directly and recover cleanly. Only genuinely fragmented files (rare on cards) still need the chain.


G.7 HFS+ (brief)

Apple's file system from 1998 to 2017. You will still meet it on older Macs, on Time Machine backup drives (which stayed HFS+ long after Macs went APFS internally), and on legacy externals — which makes it more relevant than its retirement date suggests.

Volume Header at byte offset 1024:
  0x00  2  Signature: "H+" (0x482B) HFS+  |  "HX" (0x4858) HFSX (case-sensitive)
  0x02  2  Version: 4 (HFS+) / 5 (HFSX)
  ...      fork data describing the special files below

B-tree based; special files keyed by Catalog Node ID (CNID):
  CNID 2  root folder        Catalog File      B-tree: every file/folder -> metadata (master index)
  CNID 3  Extents Overflow   block maps for fragmented files overflowing the catalog record
  CNID 4  Catalog File       (the catalog itself)
  CNID 6  Allocation File    free-space bitmap
  CNID 8  Attributes File    extended attributes
Optional journaling since 2002. Timestamps: seconds since 1904-01-01, GMT (see §G.8).

What "deleted" means on HFS+: the file's records are removed from the Catalog (and Extents) B-tree and its Allocation bits cleared; the journal may briefly retain prior state. Recover by harvesting orphaned B-tree leaf nodes (which often persist in unallocated space) from the Catalog/Extents files, else carve. B-tree node reuse and carving's loss of names/paths are the limits.


G.8 Timestamps across file systems

Timeline work lives or dies on getting epochs and time zones right (full treatment: Chapter 21 — Timeline Analysis). The two traps: different epochs, and FAT/exFAT storing local time, not UTC.

File system Epoch (zero point) Resolution Stored as Time zone
NTFS 1601-01-01 00:00 100 ns 64-bit FILETIME UTC
ext4 1970-01-01 00:00 1 s (+ns in extra fields) 32-bit (+extra) UTC
APFS 1970-01-01 00:00 1 ns 64-bit UTC
HFS+ 1904-01-01 00:00 1 s 32-bit GMT/UTC
FAT32 1980-01-01 00:00 2 s packed DOS date/time LOCAL (no zone stored)
exFAT 1980-01-01 00:00 10 ms packed DOS + UTC-offset byte local + offset field
# Convert the common on-disk time formats to ISO-8601 UTC (illustrative).
from datetime import datetime, timedelta, timezone

def filetime_to_utc(ft):              # NTFS: 100-ns ticks since 1601-01-01
    return datetime(1601, 1, 1, tzinfo=timezone.utc) + timedelta(microseconds=ft // 10)

def unix_to_utc(secs):                # ext4 / APFS(÷1e9 first): seconds since 1970
    return datetime(1970, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=secs)

def hfs_to_utc(secs):                 # HFS+: seconds since 1904-01-01 (GMT)
    return datetime(1904, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=secs)

# Offsets, if you prefer raw subtraction:
#   unix_seconds = filetime / 1e7 - 11644473600      # FILETIME -> Unix
#   unix_seconds = hfs_seconds - 2082844800          # HFS+    -> Unix

MACB behavior — what updates when (verify per-OS; defer to Chapter 21 for the authoritative matrix):

NTFS $SI ext4 APFS FAT/exFAT
M odified content write mtime mod_time last-write date/time
A ccessed read (often disabled by default on Win) atime (often relatime) access_time last-access date only
C hanged MFT/metadata change ctime (metadata) change_time
B orn (created) create crtime (ext4 only) create_time creation date/time

Note NTFS $FN carries a second MACB set that user tools cannot easily forge — the timestomping check in §G.3.5.


G.9 The master deletion comparison

The operational payload of this appendix. Read down "what survives" to see the spectrum from most-recoverable (NTFS) to harshest (APFS-on-SSD).

File system On delete, the system… What survives (recoverable) Primary recovery route The gotcha
NTFS clears the MFT in-use flag (0x16), removes the directory index entry, frees clusters in $Bitmap the full MFT record incl. data runs, both timestamp sets, name; the content clusters read the deleted MFT record; follow data runs (icat) record reuse / cluster overwrite; quick-format MFT overlap
FAT32 sets dir-entry byte 0 to 0xE5, zeroes the FAT chain dir entry with start cluster + size; lost 1st name char contiguous: start+size; fragmented: carve the chain is gone → fragmented files are guesswork
exFAT clears the 0x80 in-use bit on the entry set entry set with start cluster + length; NoFatChain files fully mapped rebuild entry set; contiguous files map directly fragmented non-contiguous files still need the chain
ext4 unlinks dir entry, decrements links; at 0 frees inode/blocks, stamps i_dtime, strips the inode's block map data blocks; journal copy of old inode; deleted name in dir slack mine the journal (extundelete/ext4magic), else carve live deleted inode no longer maps; journal ring wraps fast
APFS removes fs-tree records, returns blocks to the space manager snapshots (best); maybe superseded checkpoints snapshots / Time Machine / iCloud / live, then carve TRIM + encryption usually destroy/obscure freed data within seconds
HFS+ removes Catalog/Extents B-tree records, clears Allocation bits orphaned B-tree leaf nodes; journal remnants salvage Catalog/Extents leaf nodes, else carve B-tree node reuse; carving loses names/paths

Recovery vs. Forensics. The same surviving structures serve both masters differently. The recovery technician asks "can I get the bytes back?" and reaches for the fastest route (rebuild the partition with testdisk, follow data runs with icat, mount a snapshot). The examiner asks "what can I prove, and can I show it was unaltered?" — and so reads the same record as testimony (the two NTFS timestamp sets, the parent reference, the $UsnJrnl, the ext4 journal's transaction history), always from a hashed image, never the original (Chapter 5 — The Forensic Process; chain-of-custody and report templates in Appendix F). Deleted ≠ destroyed is universally true; the size of the gap between "deleted" and "destroyed" is set by the file system and, increasingly, by the storage technology underneath it.


G.10 Cross-references


A file system is just an agreement about where bytes go. Learn the agreement, and a "wiped" drive becomes a document you can still read — in part, with discipline, and with the honesty to say where the reading stops.