There are two common misconceptions about NTFS:
- A typical file has 8 timestamps.
- Windows Explorer displays $STANDARD_INFORMATION timestamps.
A file with a single name has 12 timestamps: 4 timestamps come from the $STANDARD_INFORMATION attribute in a file record, 4 timestamps come from the $FILE_NAME attribute in the same file record, and 4 timestamps come from the $FILE_NAME attribute in an index record ($I30) of a parent directory.
If there is a short file name together with a long one, the number of timestamps is 20 (8 more timestamps come from two additional $FILE_NAME attributes in a file record and in an index record of a parent directory respectively).
You can also add an UUIDv1 timestamp from the $OBJECT_ID attribute, timestamps recorded in the USN journal and in the $LogFile journal. But these aren’t always present.
Things are more complicated with timestamps displayed by Windows Explorer.
When you list a directory, the NTFS driver doesn’t scan all file records within the $MFT file looking for those representing child files or directories. Instead, it walks through a directory index (of a directory being listed) containing references to child files and directories.
To reduce the number of system calls required to list a directory in typical scenarios, such an index includes not only file names, but also some additional metadata like a file size, file flags, and timestamps. In fact, a directory index is built using the $FILE_NAME attributes for child files and directories.
To recover a directory index from possible corruption, a copy of each $FILE_NAME attribute for a given file is stored in its file record, so it’s possible to rebuild the entire directory structure of an NTFS volume using the $MFT file. However, there is no need to keep metadata stored in the backup $FILE_NAME attributes in sync at all times.
For example, a popular “Windows Time Rules” poster (PDF) states that $FILE_NAME timestamps are updated when:
- a file is created,
- a file is copied,
- a file is moved to another volume.
(Although not stated directly, these conclusions apply to the $FILE_NAME attribute in a file record, not in an index record of a parent directory.)
In contrast, the $FILE_NAME attribute in a directory index should be kept in sync. If a tool lists a directory and gets a wrong file size or outdated timestamps for a child file, bad things are expected to happen.
This is why the $FILE_NAME attribute in a directory index is (almost always) kept in sync with the $STANDARD_INFORMATION attribute in a corresponding file record. And this is why such an attribute is often ignored in favor of the $STANDARD_INFORMATION attribute.
Metadata from both attributes can be accessed programmatically:
- Metadata from the $STANDARD_INFORMATION attribute* can be read with the stat()-style calls like GetFileInformationByHandle().
- Metadata from the $FILE_NAME attribute (in a directory index) can be read with the readdir()-style calls like FindFirstFileW().
* – except the last access timestamp, which is overlaid in memory.
The documentation for the FindFirstFileW() routine also states that:
In rare cases or on a heavily loaded system, file attribute information on NTFS file systems may not be current at the time this function is called. To be assured of getting the current NTFS file system file attributes, call the GetFileInformationByHandle function.
Is this true? Can we have different metadata in the $STANDARD_INFORMATION (or, in general, in any attribute of a file record) and $FILE_NAME attributes? According to my previous tests, yes. (This is also mentioned here.)
It was demonstrated previously that an outdated value of the last access timestamp can be stored in the $FILE_NAME attribute (in a directory index).
And recently I found that more metadata, not only the last access timestamp, can be out-of-date in a directory index!
In one of my cases, a Windows 8.1 machine had a keylogger installed and its log file had two different sets of timestamps in the $STANDARD_INFORMATION and $FILE_NAME (in a directory index) attributes. Also, the file size was different between the $DATA attribute described in a file record and the $FILE_NAME attribute in a directory index…
The last modification timestamp and the $MFT entry modified timestamp in the $FILE_NAME attribute (in a directory index) had “older” values than corresponding timestamps recorded in the $STANDARD_INFORMATION attribute (and two other timestamps, file created and last accessed, match between these two sets). The difference was more than 21 hours (almost 22 hours) for both timestamps mentioned!
The file size in the $DATA attribute was slightly larger than in the $FILE_NAME attribute (in a directory index). However, the valid size was the smaller one, the size stored in the $DATA attribute pointed to the uninitialized area of a cluster (containing null bytes instead of expected ASCII text).
When trying to reproduce this behavior using a Windows 8.1 virtual machine, I got another interesting result.
During the test, one process opened a file for writing, then constantly (and slowly) appended a fixed byte pattern to that file. Another process opened the same file for reading, read some amount of data, then stopped reading (the handle remained open). Then, the system was hibernated and an E01 image has been acquired.
The image was mounted read-only in Windows 10 using the Arsenal Image Mounter tool.

As you can see on the screenshot above, the reported file size is “0 KB”, but when the file is opened in a text editor, there is some data.
When viewing the properties of that file, the file size is reported as “0 bytes”, but the size on a disk is “192 KB”.

The timestamps reported by The Sleuth Kit are:
$ TZ=UTC istat -o 718848 mnt/ewf1 197441-128-4
MFT Entry Header Values:
Entry: 197441 Sequence: 5
$LogFile Sequence Number: 1747779458
Allocated File
Links: 1
$STANDARD_INFORMATION Attribute Values:
Flags: Archive, Not Content Indexed
Owner ID: 0
Security ID: 1524 (S-1-5-21-2306428429-1570712259-4110267456-1001)
Last User Journal Update Sequence Number: 788046176
Created: 2020-12-31 04:43:07.757976400 (UTC)
File Modified: 2020-12-31 05:31:54.788495000 (UTC)
MFT Modified: 2020-12-31 05:31:54.788495000 (UTC)
Accessed: 2020-12-31 04:43:07.757976400 (UTC)
$FILE_NAME Attribute Values:
Flags: Archive, Not Content Indexed
Name: test_ts.txt
Parent MFT Entry: 195076 Sequence: 6
Allocated Size: 0 Actual Size: 0
Created: 2020-12-31 04:43:07.757976400 (UTC)
File Modified: 2020-12-31 04:43:07.757976400 (UTC)
MFT Modified: 2020-12-31 04:43:07.757976400 (UTC)
Accessed: 2020-12-31 04:43:07.757976400 (UTC)
Attributes:
Type: $STANDARD_INFORMATION (16-0) Name: N/A Resident size: 72
Type: $FILE_NAME (48-2) Name: N/A Resident size: 88
Type: $DATA (128-4) Name: N/A Non-Resident size: 115932 init_size: 115932
4389408 2952598 2952599 3238813 3238814 3238815 3238816 3238817
3239656 3239657 3239658 3239659 3297756 3297757 3297758 3297759
3297760 3297761 3297762 3297763 3297764 3297765 3297766 3297767
1516384 1516385 1516386 1516387 1516388 0 0 0
0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0
As you can see, the last modification timestamp in the $STANDARD_INFORMATION attribute (2020-12-31 05:31:54 UTC) doesn’t match the last modification timestamp as shown by Windows Explorer (2020-12-31 05:25:26 UTC).
But this perfectly matches the output produced by the dfir_ntfs project:

The next step is to check if this discrepancy is a result of an uncommitted NTFS transaction.
In the $LogFile journal, the most recent operation on the index of the “/ProgramData/test/” directory is the following (the last modification timestamp is shown as bold):
LSN: 1754097652
Transaction ID: 64
Log record, redo operation: UpdateFileNameRoot, undo operation: UpdateFileNameRoot
Target (file number): 195076
Target path (from $MFT, likely wrong if the file was deleted later): /ProgramData/test
Offset in tagret: 320
LCN(s): 835201
Redo data:
00000000 F4 E7 49 6F 2F DF D6 01-96 51 EF 3F 36 DF D6 01 ..Io/....Q.?6...
00000010 96 51 EF 3F 36 DF D6 01-F4 E7 49 6F 2F DF D6 01 .Q.?6.....Io/...
00000020 00 00 03 00 00 00 00 00-DC C4 01 00 00 00 00 00 ................
00000030 20 20 00 00 00 00 00 00 ......
Undo data:
00000000 F4 E7 49 6F 2F DF D6 01-EF FA 70 58 35 DF D6 01 ..Io/.....pX5...
00000010 EF FA 70 58 35 DF D6 01-F4 E7 49 6F 2F DF D6 01 ..pX5.....Io/...
00000020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
00000030 20 20 00 00 00 00 00 00 ......
During this operation, the last written timestamp is changed from 0x01d6df355870faef (2020-12-31 05:25:26.406834 UTC) to 0x01d6df363fef5196 (2020-12-31 05:31:54.788496 UTC).
These values match the $FILE_NAME timestamp (in a directory index) and the $STANDARD_INFORMATION timestamp respectively.
Clearly, we deal with an uncommited transaction. Since the file system is in an unclean state, we shouldn’t be surprised. But let’s continue the experiment.
Resume the virtual machine and let it run for more than one hour. Then, “pull the plug”.
The following metadata for the file in question can be observed:
- Last written timestamp, $FILE_NAME (directory index): 2021-01-08 01:59:38.826954 UTC;
- Last written timestamp, $STANDARD_INFORMATION: 2021-01-08 03:13:36.758612 UTC;
- File size, $FILE_NAME (directory index): 140502 bytes;
- File size, $DATA: 173262 bytes.
As you can see, the time difference is more than one hour. The file size in the directory index is non-zero, but different from the value recorded in the $DATA attribute. (According to data in clusters occupied by the file, 173262 bytes is the correct size. In the case mentioned before, the situation was the opposite.)
Now, let’s check the $LogFile journal. The most recent operation on the index of the “/ProgramData/test/” directory is the following (the last modification timestamp is shown as bold):
LSN: 1762105976
Transaction ID: 24
Log record, redo operation: UpdateFileNameRoot, undo operation: UpdateFileNameRoot
Target (file number): 195076
Target path (from $MFT, likely wrong if the file was deleted later): /ProgramData/test
Offset in tagret: 320
LCN(s): 835201
Redo data:
00000000 F4 E7 49 6F 2F DF D6 01-DD CF 03 EC 61 E5 D6 01 ..Io/.......a...
00000010 DD CF 03 EC 61 E5 D6 01-F4 E7 49 6F 2F DF D6 01 ....a.....Io/...
00000020 00 00 03 00 00 00 00 00-D6 24 02 00 00 00 00 00 .........$......
00000030 20 20 00 00 00 00 00 00 ......
Undo data:
00000000 F4 E7 49 6F 2F DF D6 01-EE F3 DA 0D 60 E5 D6 01 ..Io/.......`...
00000010 EE F3 DA 0D 60 E5 D6 01-F4 E7 49 6F 2F DF D6 01 ....`.....Io/...
00000020 00 00 03 00 00 00 00 00-D8 04 02 00 00 00 00 00 ................
00000030 20 20 00 00 00 00 00 00 ......
Here, the last written timestamp is changed from 0x01d6e5600ddaf3ee (2021-01-08 01:46:16.608254 UTC) to 0x01d6e561ec03cfdd (2021-01-08 01:59:38.826954 UTC). The latter is the same timestamp as found in the $FILE_NAME attribute (in a directory index).
So, there is no pending update to the $FILE_NAME attribute in the directory index having the same timestamp value as in the $STANDARD_INFORMATION attribute (2021-01-08 03:13:36.758612 UTC)!
I can easily locate the latest update to the $STANDARD_INFORMATION attribute, but there is no corresponding update to the $FILE_NAME attribute…
LSN: 1762438841
Transaction ID: 64
Log record, redo operation: UpdateResidentValue, undo operation: UpdateResidentValue
Target (file number): 197441
Target path (from $MFT, likely wrong if the file was deleted later): /ProgramData/test/test_ts.txt
Offset in tagret: 88
LCN(s): 835792
Redo data:
00000000 50 71 3A 41 6C E5 D6 01-50 71 3A 41 6C E5 D6 01 Pq:Al...Pq:Al...
00000010 F4 E7 49 6F 2F DF D6 01-20 20 00 00 00 00 00 00 ..Io/... ......
00000020 00 00 00 00 00 00 00 00-00 00 00 00 F4 05 00 00 ................
00000030 00 00 00 00 00 00 00 00-68 0B 1A 30 00 00 00 00 ........h..0....
Undo data:
00000000 AD A4 1E AC 69 E5 D6 01-AD A4 1E AC 69 E5 D6 01 ....i.......i...
00000010 F4 E7 49 6F 2F DF D6 01-20 20 00 00 00 00 00 00 ..Io/... ......
00000020 00 00 00 00 00 00 00 00-00 00 00 00 F4 05 00 00 ................
00000030 00 00 00 00 00 00 00 00-68 0B 1A 30 00 00 00 00 ........h..0....
Possible update to $STANDARD_INFORMATION (redo data):
* M timestamp: 2021-01-08 03:13:36.758612
* A timestamp: 2020-12-31 04:43:07.757976
* C timestamp: N/A
* E timestamp: 2021-01-08 03:13:36.758612
Possible update to $STANDARD_INFORMATION (undo data):
* M timestamp: 2021-01-08 02:55:07.602450
* A timestamp: 2020-12-31 04:43:07.757976
* C timestamp: N/A
* E timestamp: 2021-01-08 02:55:07.602450
(Notice that the update is relative to the second timestamp in the attribute. Since the first 8 bytes, which belong to the file created timestamp, aren’t updated, they are excluded from undo/redo data.)
Thus, this particular discrepancy isn’t a result of an uncommitted NTFS transaction.
This is why you need to account NTFS index entries in your timelines!
Re “This is why you need to account NTFS index entries in your timelines!”
Exactly, issue is that most forensic tools don’t include index entries.
Excellent analysis, very thought provoking.
Sometimes have needed to parse index entries when the files MFT entries have been over written and often thought it would be very useful if forensic tools automatically parsed all the index entries.
LikeLike