← Back to blog

Inside an MFT record, byte by byte

· 6 min read

Every MFT record is 1,024 bytes. Every one of them is laid out the same way. If you can read one in a hex editor, you can read all of them, and you can write a parser that beats the brittle commercial tools at recovering damaged records. The layout is small enough that a couple of hours and 010 Editor will get you fluent.

This is the byte-level tour I give to analysts who want to stop treating MFT parsers as black boxes.

The FILE signature

46 49 4C 45  ("FILE")

First four bytes. ASCII FILE. This is the magic that lets you carve MFT records out of raw clusters when $MFT itself is unreadable. Walk a disk image at 512-byte boundaries (older NTFS) or 4096-byte boundaries (Win10 with 4K records, rare but exists), pattern-match for 46 49 4C 45, and you can usually recover most of an MFT even when the table's own header is destroyed.

The other signature you will see in the wild is BAAD (42 41 41 44). NTFS writes this when chkdsk decided a record was unrepairable. The slot is kept, the sequence number is preserved, but the rest of the bytes should not be trusted. Treat BAAD records as evidence that chkdsk ran and as targets for surrounding-context analysis. Do not try to parse their attributes naively.

The record header (offsets 0x00 to 0x37, give or take)

After the four signature bytes, NTFS lays out a header that varies slightly by NTFS version. The fields, with offsets from the start of the record, that you actually use:

0x00  4 bytes  Signature ("FILE" or "BAAD")
0x04  2 bytes  Offset to update sequence array (USA)
0x06  2 bytes  USA size in 16-bit words (records=count of (USN+fixup_entries))
0x08  8 bytes  $LogFile sequence number (LSN)
0x10  2 bytes  Sequence number
0x12  2 bytes  Hard link count
0x14  2 bytes  Offset to first attribute
0x16  2 bytes  Flags (bit 0 = IN_USE, bit 1 = DIRECTORY,
                     bit 2 = QUOTA_CHARGED, bit 3 = HAS_VIEW_INDEX)
0x18  4 bytes  Used size of record
0x1C  4 bytes  Allocated size (always 1024 on standard volumes)
0x20  8 bytes  Base file record reference (non-zero on extension records)
0x28  2 bytes  Next attribute ID
0x2A  2 bytes  (padding/align on NTFS 3.0)
0x2C  4 bytes  Record number (NTFS 3.1+; self-reference)

A few of these earn special attention.

Flags at 0x16. Bit 0 cleared means deleted. Bit 1 set means directory (the record holds index attributes rather than $DATA). The combination of both flags being meaningful is what makes one byte tell you a lot about an entry.

Sequence number at 0x10. Incremented every time the slot is reused. The 64-bit file reference (record number in the low 48 bits, sequence number in the high 16) is the actual unique identifier for a particular file's existence. References inside other attributes ($FILE_NAME parent reference, $ATTRIBUTE_LIST entries) use this 64-bit form. A reference whose sequence does not match the current record points at a previous tenant; usually a deleted file. This is how Sleuth Kit walks deleted directory chains.

Base file record reference at 0x20. Zero on a base record; non-zero on an extension record (when one file's attributes overflow a single slot). The non-zero value is the 64-bit reference of the base record this extension belongs to. Parsers must follow $ATTRIBUTE_LIST chains to assemble the complete file.

$LogFile sequence number at 0x08. Points into $LogFile. Useful for transaction-level recovery; less useful for routine analysis. Worth knowing exists.

The fixup (update sequence) array

NTFS protects against torn writes with a small trick. Each 1,024-byte record is divided into two 512-byte sectors. Before writing, NTFS:

  1. Picks a 16-bit update sequence number (USN, no relation to the $UsnJrnl USN despite the shared acronym).
  2. Stashes the original last two bytes of each sector into an array that lives right after the header.
  3. Replaces the last two bytes of each sector with the USN itself.

On read, NTFS checks that the last two bytes of every 512-byte sector equal the chosen USN. If they do, the write was atomic; pull the original bytes out of the fixup array and slot them back in. If any sector's tail does not match, the write was torn and the record is suspect.

The array is laid out as one USN word followed by N fixup words, where N is the number of sectors. For a 1,024-byte record on a 512-byte-sector volume, N = 2. The array therefore occupies 6 bytes total (USN, fixup_for_sector_0, fixup_for_sector_1). Its offset is the 2-byte value at 0x04 of the record (typically 0x2A or 0x30 depending on NTFS version).

The practical consequence: if you read raw 1,024-byte chunks of $MFT without applying fixups, every record will have garbage at offsets 510 and 1022. Resident $DATA that crosses those offsets will be corrupted. Parsers like MFTECmd, omerbenamram/mft, analyzeMFT and Sleuth Kit's fls/istat apply fixups as the first step. If you are writing your own parser (which is a fine exercise; see parse-mft in Python), do this before anything else.

The attribute stream

After the header and the fixup array, every record contains a series of typed attributes packed back to back, 8-byte aligned, terminated by the sentinel value 0xFFFFFFFF where the next attribute's type code would be.

Each attribute starts with a small standardized header:

0x00  4 bytes  Attribute type code (0x10 = $STANDARD_INFORMATION, etc.)
0x04  4 bytes  Length of this attribute (header + data)
0x08  1 byte   Non-resident flag (0 = resident, 1 = non-resident)
0x09  1 byte   Name length (in characters; 0 if unnamed)
0x0A  2 bytes  Offset to name (in characters from start of attribute)
0x0C  2 bytes  Flags (compressed/encrypted/sparse)
0x0E  2 bytes  Attribute ID

For resident attributes the next fields are content length and content offset; for non-resident they are starting/ending VCN, runlist offset, compressed/allocated/real sizes. After all of that, the actual data. Names (if any) are Unicode and unaligned; expect the implementation to be fiddly.

A minimal record carries three attributes:

  • $STANDARD_INFORMATION (0x10): timestamps, DOS flags, security ID.
  • $FILE_NAME (0x30): name, parent reference, second set of timestamps, allocated/real sizes from when the name was set. Records can carry many of these (one per hard link plus the 8.3 short name on volumes where 8.3 generation is enabled).
  • $DATA (0x80): the file's content, resident if it fits, runlist otherwise. Records can carry multiple $DATA attributes; the unnamed one is the primary stream, named ones are alternate data streams.

For the full attribute catalog and where each lives, see the Master File Table reference.

Why this matters when records are damaged

The combination of a stable 1,024-byte layout, the FILE signature, the fixup mechanism, and self-describing attribute headers is what makes carving deleted NTFS records possible. Even when $MFT itself is gone (corruption, ransomware scribbles, partial wipe), a signature scan of the raw volume for 46 49 4C 45 boundaries recovers the records as long as the underlying clusters were not overwritten. Fixup verification gives you a per-sector integrity check; records that fail it should be flagged but their attribute data is sometimes still partially readable.

This is the foundation under tools like Sleuth Kit's mmls+fls, R-Studio's "deep scan", and the various commercial recovery suites. They are all walking the same byte layout. Knowing it yourself is what lets you sanity-check their output.

Reading one by hand

Open an extracted $MFT in 010 Editor with the NTFS MFT Record template applied. Pick record 5 (the root directory; offset 5 * 1024 = 5120 from the start of the file). Confirm:

  • Bytes 0x00 to 0x03: 46 49 4C 45.
  • Flags at 0x16: 0x03 (IN_USE and DIRECTORY both set).
  • First attribute type at the offset given by 0x14: 0x10 ($STANDARD_INFORMATION).

If those line up, the record parsed cleanly. If they do not, either the file is corrupted or the fixup array has not been applied (010 Editor's template does apply it for you).

Once you have done this once, the rest of the MFT becomes legible. The fields stop being arcane and start being a record you can read without help.

Further reading

External resources