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 (
fsstatdoes 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 isxxd -l 16 -s <partition_start> image.ddfor the NTFS/exFAT/FAT cases andxxd -l 2 -s $((part_start+1080)) image.ddfor 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_sectorfrom the boot sector /fsstatrather 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 `$FNtime (logically impossible in normal operation), or$SI` times that are suspiciously round (`…12:00:00.0000000`) while `$FNis 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$MFTand 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_dtimestamped, 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,noloadto 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
0xE5and the FAT chain is zeroed, but start cluster and size survive in the directory entry. Contiguous files recover trivially (readsizebytes 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
0x80in-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 withicat, 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
- Concepts & worked examples for everything above: Chapter 4 — File Systems (owner). Sectors/clusters and "deleted = pointer removed": Chapter 2. Media beneath the file system: Chapter 3.
- Recovery techniques: logical / deleted-record recovery Chapter 6; file carving Chapter 7; SSD/TRIM limits Chapter 9.
- Forensic use of these structures: Windows ($Recycle.Bin, registry, Prefetch) Chapter 16; macOS/Linux Chapter 17; timelines & timestomping detection Chapter 21; anti-forensics detection Chapter 30; encrypted volumes Chapter 29.
- Companion appendices: file-content magic numbers for carving — Appendix A — File Signatures Reference; the tools that read every structure here — Appendix H — Command-Line Reference; artifact paths on each OS — Appendix D — Forensic Artifact Locations; definitions — Glossary; practice images to try this on — Appendix J — Practice Images and Lab Setup.
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.