NTFS Forensics: $MFT, USN Journal and $LogFile Explained
Read NTFS like an examiner: MFT record structure, $SI vs $FN timestamps, timestomping checks, USN reason flags, $LogFile, $I30 slack and deleted files.
NTFS keeps far more history than the files a user can see: every file ever created leaves a record, every change is journaled, and deleted names linger in index slack. This guide walks through the metadata files that matter, how an MFT record is built, how to read $SI and $FN timestamps without over-claiming, how to catch timestomping, and how to combine the $MFT, USN journal, $LogFile and $I30 into one defensible file-system timeline.
The NTFS metadata files that matter
Everything on an NTFS volume, including the file system's own bookkeeping, is a file described in the Master File Table. The first records are reserved for system files that the Windows API hides and locks, so you need a raw-disk tool or an image to collect them.
| MFT record | File | Forensic value |
|---|---|---|
| 0 | $MFT | One FILE record per file and directory: names, parents, timestamps, sizes, resident data |
| 1 | $MFTMirr | Copy of the first four MFT records only, used for recovery; not a backup of the table |
| 2 | $LogFile | Transaction log of metadata changes with redo and undo data; short retention |
| 5 | . (root) | Root directory and its $I30 index |
| 6 | $Bitmap | One bit per cluster: allocated or free. Tells you whether a deleted file's clusters are still available |
| 7 | $Boot | Boot sector: bytes per sector, sectors per cluster, MFT location, record size |
| 9 | $Secure | $SDS stream holds shared security descriptors (owner SID, ACLs); $SDH and $SII index them |
| 11 | $Extend | Directory holding $UsnJrnl, $ObjId, $Quota, $Reparse and others |
| varies | $Extend\$UsnJrnl | $J stream: change journal records. $Max stream: journal size parameters |
Records 3 ($Volume), 4 ($AttrDef), 8 ($BadClus) and 10 ($UpCase) complete the set. Every NTFS volume has its own copy of all of this, including USB drives and VHDs, so collect per volume.
Anatomy of an MFT record
The FILE header
Each $MFT entry is normally 1,024 bytes (4,096 on some 4Kn disks; $Boot tells you which). In a hex editor a valid record usually starts with FILE0: the FILE signature followed by the update sequence array offset 0x30, which happens to be the ASCII character 0. A record that failed its integrity check carries BAAD instead.
| Offset | Size | Field |
|---|---|---|
| 0x00 | 4 | Signature FILE |
| 0x04 | 2 | Update sequence array offset (fixup) |
| 0x08 | 8 | $LogFile sequence number (LSN) of the last change |
| 0x10 | 2 | Sequence number |
| 0x12 | 2 | Hard link count |
| 0x14 | 2 | Offset to the first attribute |
| 0x16 | 2 | Flags: 0x01 in use, 0x02 directory |
| 0x18 | 4 | Used size of the record |
| 0x20 | 8 | Base record reference (non-zero for extension records) |
| 0x2C | 4 | Record number (XP and later) |
Remember the fixups: the last two bytes of each 512-byte sector in the record are replaced by the update sequence number and must be restored from the array before you read attribute data. Every serious parser does this; hand-carving often forgets.
Attributes
After the header comes a list of attributes, each with a type code, ending with 0xFFFFFFFF. The ones you meet daily are 0x10 $STANDARD_INFORMATION, 0x20 $ATTRIBUTE_LIST, 0x30 $FILE_NAME, 0x80 $DATA, 0x90 $INDEX_ROOT, 0xA0 $INDEX_ALLOCATION and 0xB0 $BITMAP. A file can carry several $FILE_NAME attributes: a long name, a DOS 8.3 short name and one per hard link.
Resident versus non-resident data
When the content is small enough to fit in the record, $DATA is resident: the bytes live inside the MFT entry itself. With 1,024-byte records the practical limit is roughly 700 bytes, depending on the other attributes present. Resident data is gold: small scripts, batch files, configuration fragments and Zone.Identifier streams can be read straight from the $MFT, even for deleted files whose record has not been reused. Larger content is non-resident: the attribute stores data runs (cluster offsets and lengths) pointing into the volume.
Record reuse and the file reference
When a file is deleted, NTFS clears the in-use flag and the record becomes available. The next time the record is allocated, the sequence number increments. Other structures refer to files by a 64-bit file reference: a 48-bit entry number plus a 16-bit sequence number. That pairing is what lets you tell apart "entry 81234, sequence 5" (the current file) from "entry 81234, sequence 4" (whatever lived there before). USN records, $I30 entries, LNK files and Jump Lists all store full references, so a sequence mismatch is a reliable sign that the file they point to is gone.
$STANDARD_INFORMATION versus $FILE_NAME timestamps
Both attributes hold four FILETIME values in UTC at 100-nanosecond resolution: created, modified, MFT record changed and accessed, the MACB set. They are maintained differently, which is the whole point of comparing them. The $SI versus $FN distinction is summarised below.
- $SI is what Explorer,
dirand most tools show. Windows updates it as the file is used, and any process with write access can set it throughSetFileTimeorNtSetInformationFile. - $FN is maintained by the kernel when the name entry is created or changed. There is no documented user-mode API to set it directly.
General behaviour on modern Windows (verify on the target build before relying on any single cell):
| Action | $SI | $FN |
|---|---|---|
| Create | All four set to now | All four set to now |
| Modify content | M, C (and often A) updated | Usually unchanged |
| Rename in the same folder | C updated | Behaviour varies by version; often refreshed or unchanged |
| Move within the volume | C updated | Varies; the new name entry may take current or $SI values |
| Copy | B, C and A new; M inherited from source | All set to the copy time |
| Move to another volume | B new; M inherited | All set to the move time |
Two practical consequences. First, a modified time earlier than the creation time is normal for a copied file and is not tampering. Second, the last access time depends on NtfsDisableLastAccessUpdate, which was disabled by default from Vista to Windows 10 1709 and has been "system managed" since 1803, so record the value before interpreting any A timestamp.
Detecting timestomping
Timestomping (MITRE T1070.006) is almost always done on $SI, because that is what the API exposes. Look for these signs, from strongest to weakest:
- $SI created earlier than $FN created. A file cannot be created before its own name entry. MFTECmd flags this as
SI<FN. - Zeroed sub-second values. Many tools write whole seconds, leaving
.0000000fractions. Real timestamps almost never end in seven zeros. MFTECmd flags this asuSecZeros. - A
BASIC_INFO_CHANGErecord in the USN journal for the file with no matching content write, especially shortly afterFILE_CREATE. - $LogFile undo data that still holds the previous $SI values, if the change is recent enough.
- Inconsistency with other artifacts: a binary "created in 2019" whose MFT entry number sits among files created last week, or which Prefetch, Amcache or the USN journal saw arrive yesterday.
Sophisticated actors can defeat the $FN check, for example by stomping $SI and then moving the file so the new name entry inherits the fake values, or by writing raw to the volume. That is why the USN journal and $LogFile corroboration matters more than any single flag.
# Full $MFT to CSV, then filter the anomaly columns in Timeline Explorer
MFTECmd.exe -f "E:\Triage\C\$MFT" --csv E:\Out --csvf mft.csv
# Dump one record in detail (entry number from the CSV)
MFTECmd.exe -f "E:\Triage\C\$MFT" --de 81234
The USN journal: every change, in order
The USN journal lives in the sparse $Extend\$UsnJrnl:$J stream and appends a record each time a file or directory changes. $Max holds its maximum size and allocation delta. When the journal exceeds its maximum, the oldest records are purged and their clusters read back as zeros, so a multi-gigabyte $J may contain only tens of megabytes of real data at the end.
Reason flags worth knowing
The Reason field is a bit mask that accumulates while a handle is open, then the final record carries CLOSE. Read the combination, not just one bit.
| Flag | Value | Typical meaning |
|---|---|---|
DATA_OVERWRITE | 0x00000001 | Content rewritten |
DATA_EXTEND | 0x00000002 | Content appended, normal for new files |
DATA_TRUNCATION | 0x00000004 | File shortened or emptied |
NAMED_DATA_EXTEND | 0x00000020 | Alternate data stream written, for example Zone.Identifier |
FILE_CREATE | 0x00000100 | File or directory created |
FILE_DELETE | 0x00000200 | File or directory deleted |
SECURITY_CHANGE | 0x00000800 | ACL or owner changed |
RENAME_OLD_NAME | 0x00001000 | Name before a rename or move |
RENAME_NEW_NAME | 0x00002000 | Name after a rename or move |
BASIC_INFO_CHANGE | 0x00008000 | Attributes or timestamps changed |
STREAM_CHANGE | 0x00200000 | A named stream added or removed |
CLOSE | 0x80000000 | Final record for this handle |
Rebuilding full paths
Each record stores only the file name plus the parent's file reference. To get a full path, resolve the parent reference against the $MFT, recursively up to the root. If the parent's entry was reused (sequence mismatch), the path is ambiguous and a good parser marks it as such rather than guessing. Resolving against the $MFT collected at the same time as $J is essential.
# Parse $J and resolve parent paths using the $MFT from the same volume
MFTECmd.exe -f "E:\Triage\C\$Extend\$J" -m "E:\Triage\C\$MFT" --csv E:\Out --csvf usn.csv
Typical patterns: a download shows FILE_CREATE, DATA_EXTEND, then NAMED_DATA_EXTEND for the Zone.Identifier stream; an archive extraction shows dozens of FILE_CREATE under one parent within a second; ransomware shows paired RENAME_OLD_NAME and RENAME_NEW_NAME with a new extension across thousands of files.
$LogFile: the last minutes, with before and after values
The $LogFile is the NTFS transaction log, typically 64 MB on modern volumes. Before changing metadata, NTFS writes a record with a redo operation (how to reapply the change) and an undo operation (how to roll it back), often containing the raw bytes before and after. That means $LogFile can show the original $SI timestamps of a stomped file, the names in an index entry that was just removed, or resident content that has since been overwritten.
The catch is retention. It is a circular buffer covering metadata changes, and on a busy system drive it may hold minutes to a few hours of activity. Collect it early, and treat it as a precision tool for events close to acquisition time, not a long-term history. Windows 8 and later use Log File Service version 2.0 while the volume is mounted, which some older parsers cannot read.
$I30 index slack: names that outlive their records
Every directory stores a sorted index of its children in $I30 attributes: $INDEX_ROOT inside the MFT record for small folders, plus $INDEX_ALLOCATION INDX records once it grows. Each entry contains a copy of the child's $FILE_NAME, with name, sizes and four timestamps. When entries are removed or the B-tree is rebalanced, old entries often survive in the unused tail of the INDX record. Parsing that $I30 slack can reveal files deleted from a staging folder even after their MFT records were reused. This is a form of file slack specific to directory indexes.
# Parse the $I30 of a directory exported with a raw-NTFS tool
MFTECmd.exe -f "E:\Triage\ProgramData_I30" --csv E:\Out --csvf i30.csv
Alternate data streams and Zone.Identifier
An alternate data stream is simply a named $DATA attribute. Windows uses one for the Mark of the Web: file.exe:Zone.Identifier stores ZoneId=3 for Internet downloads and, on Windows 10 and later with most browsers, ReferrerUrl and HostUrl. Because the stream is tiny, it is almost always resident, so MFTECmd prints its contents directly in the CSV. See the Zone.Identifier cheat sheet for field details.
Attackers also hide payloads in ADS (type evil.exe > notes.txt:hidden.exe). They show up as extra $DATA attributes in the $MFT and as NAMED_DATA_EXTEND or STREAM_CHANGE in the USN journal.
# Live triage: list streams on files in a folder
Get-ChildItem C:\Users\Public -Recurse -File | Get-Item -Stream * |
Where-Object Stream -ne ':$DATA' | Select-Object FileName, Stream, Length
Deleted file recovery versus carving
These are different techniques with different evidential weight.
- Metadata-based recovery uses an unallocated MFT record that still has its data runs. If
$Bitmapshows those clusters as free and nothing has reused them, you get the file with its original name, path and timestamps. Tools: The Sleuth Kit (icat), VelociraptorWindows.NTFS.Recover, most commercial suites. - File carving ignores the file system and scans unallocated space for headers and footers. It recovers content without names or timestamps and fragments badly on large files. Use it when metadata is gone, and say clearly in the report that carved content has no file-system provenance.
On SSDs with TRIM, freed clusters are often zeroed quickly, so expect less from both approaches. Volume shadow copies are frequently the better source for older versions of a file.
Tools
- MFTECmd (Eric Zimmerman) parses
$MFT,$J,$Boot,$SDSand$I30, with anomaly columns for timestomping and resident ADS output. Source and releases: github.com/EricZimmerman/MFTECmd. - $LogFile parsers: LogFileParser (Joakim Schicht), NTFS Log Tracker and dfir_ntfs decode redo and undo operations.
- Velociraptor:
Windows.NTFS.MFT,Windows.Forensics.UsnandWindows.NTFS.I30work live or offline. - Our browser-based parsers, which run client-side so the evidence never leaves your machine: MFT Parser, USN Parser and LogFile Parser.
- Plaso ingests
$MFTand$Jinto a super timeline; see building a super timeline with Plaso.
Common mistakes
- Treating any $SI/$FN difference as tampering. $FN routinely lags. Only $SI created earlier than $FN created, plus corroboration, supports a timestomping finding.
- Parsing
$Jagainst the wrong $MFT. Paths resolved against a later or earlier $MFT will be wrong wherever entries were reused. - Ignoring sequence numbers. Entry 81234 today is not necessarily the file a LNK or USN record referenced last month.
- Collecting too late. $LogFile and the end of
$Jare overwritten by normal activity, including your own triage tools running on the system. - Forgetting shadow copies. Older
$MFTand$Jversions often sit in VSS and can extend your timeline by weeks. - Reading access times without checking
NtfsDisableLastAccessUpdate.
Workflow
- Collect
$MFT,$J,$Max,$LogFile,$Secure:$SDSand$Bootfrom every NTFS volume, plus the same from shadow copies. See acquisition and triage with KAPE and Velociraptor. - Parse
$MFTto CSV. Record the NTFS version details, record size andNtfsDisableLastAccessUpdate. - Parse
$Jwith-mpointing to the same volume's $MFT. Note the first and last USN timestamps to know your coverage window. - Pivot on the incident window: filter USN for
FILE_CREATE,RENAME_*,FILE_DELETEandBASIC_INFO_CHANGEunder suspicious parents (\ProgramData\,\Users\Public\,\Windows\Temp\, userDownloads). - For every suspicious file, check the $MFT row:
SI<FN,uSecZeros, residentZone.Identifier, owner viaSecurityIdand $Secure:$SDS. - Parse
$LogFileif the activity is recent, and$I30of staging directories to find deleted names. - Recover deleted files by metadata where possible; carve only as a fallback, and label carved results.
- Merge everything into a single timeline and correlate with execution artifacts such as Prefetch and event logs. When the file system shows deliberate cleanup, continue with detecting Windows anti-forensics.
Area 05
NTFS & File System
The artifact cheat sheets behind this guide: location by Windows version, what each one proves and the tools to parse it.