Security Engineering · Incident Response

Filesystem Timestamps and Why Timestomping Leaves a Seam

Which timestamps an application can set, which only the kernel writes, and how the divergence between them exposes an attempt to backdate a file.

A file’s timestamps look like a single property and are in fact several properties stored in different places, written by different code, and settable by different callers. The gap between what an application is permitted to write and what only the kernel writes is the entire basis for detecting a backdated file, and the detection works because the gap cannot be closed from user space on either of the two dominant file systems.

The naive approach to hiding a file’s arrival is to set its modification time to something old. That succeeds at the level the naive check inspects and fails at the level below it, and the artifact of the failure is a mismatch that no amount of care removes without rewriting structures the API does not expose.

What a caller may set, and what it may not

On POSIX systems the interface is utimensat, which sets access and modification times and accepts either explicit timespec values or the sentinels UTIME_NOW and UTIME_OMIT. Two timestamps are deliberately absent from that interface.

ctime, the inode change time, is written by the kernel on every metadata change: a rename, a permission change, a link count change, an ownership change. It cannot be set from user space at all. Any utimensat call that changes mtime updates ctime as a side effect, because the inode’s metadata changed. The consequence is structural: a file whose mtime was set to a date in the past has a ctime at the moment of the call. mtime < ctime with a large gap is the canonical signature of backdating, and it is not forgeable through the standard interface.

Birth time, where the file system records it, is also kernel-written. ext4 stores a creation timestamp in the inode’s extra space, and statx exposes it through STATX_BTIME. Not all file systems record it and not all tools read it, which makes its absence uninformative and its presence decisive.

A demonstration of the asymmetry:

#include <fcntl.h>
#include <stdio.h>
#include <time.h>

int main(void) {
    struct timespec times[2] = {
        { .tv_sec = 1600000000, .tv_nsec = 0 },  /* atime: backdated */
        { .tv_sec = 1600000000, .tv_nsec = 0 },  /* mtime: backdated */
    };
    /* utimensat cannot touch ctime; the kernel stamps it to now. */
    if (utimensat(AT_FDCWD, "payload.bin", times, 0) != 0)
        perror("utimensat");
    return 0;
}

Reading the result shows mtime at the requested instant and ctime at the present, with the difference equal to the backdate interval. A file whose mtime predates the file system’s own creation is a stronger case still, since no sequence of legitimate operations produces it.

NTFS implements the same distinction with different names. Every file record carries two attribute sets containing four timestamps each: $STANDARD_INFORMATION and $FILE_NAME. The SetFileTime API writes the $STANDARD_INFORMATION copy. The $FILE_NAME copy is maintained by the file system and updated on operations that create or rename the file. A tool that backdates a file through the documented API therefore changes one copy and leaves the other at the true creation time, and the divergence between the two is the seam.

Why the seam survives ordinary use

The divergence is only informative because legitimate operations keep the two copies consistent. Creating a file sets both; renaming it updates the $FILE_NAME copy’s timestamps to the rename moment while leaving the $STANDARD_INFORMATION copy’s modification time alone. That asymmetry is itself documented behavior, so a mismatch is not automatically suspicious; the analyst has to reason about which operation produces which pattern.

The pattern that is hard to explain legitimately is a $FILE_NAME creation time later than a $STANDARD_INFORMATION modification time. Nothing in normal operation modifies content before the name exists. Equally hard is a $STANDARD_INFORMATION creation time earlier than the file record’s own allocation in the volume’s metadata, since the record was created when the file was, so a claim that the file was created before its own record is incoherent.

The journal as an independent witness

Even a rewrite of both timestamp copies does not remove the record of the change, because NTFS maintains a change journal that logs metadata operations as they occur. The $UsnJrnl stream records, per volume, a sequence of records each with a USN, a file reference, a reason mask, and a timestamp. Reasons include file creation, data overwrite, basic information change, and rename.

A record with reason BASIC_INFO_CHANGE on a file whose $STANDARD_INFORMATION timestamps claim a date before the journal record’s own timestamp is a direct contradiction: the timestamps were changed at a moment later than the values they now hold. The journal is a separate structure from the file record, so altering one does not alter the other, and the USN ordering is monotonic and independent of the wall clock, which is why it survives the clock problems that affect timestamp comparison across sources.

Journal coverage is bounded. The $UsnJrnl is a sparse stream that grows as changes occur and is truncated by maximum size, so on a busy volume the record of a change may be aged out within days. Whether the witness is available depends on volume activity since the event, not on the event’s age in isolation.

ext4 provides a weaker analogue. The journal records metadata transactions for crash recovery and is normally discarded once the transaction is checkpointed, so it is not a change history. The dtime field and the inode’s generation number provide some resistance: an inode reused for a new file has a different generation, and a directory entry pointing at a stale generation is detectable. There is no per-file operation log.

atime, and why it is unreliable in both directions

Access time is written by the kernel on read, which sounds like a gift to the investigator and is not. Most Linux distributions mount with relatime, which updates atime only if the previous atime is older than mtime or ctime, or older than roughly a day. The result is that atime frequently records nothing at all, and its absence says nothing about whether the file was read.

A file with atime equal to mtime on a relatime mount has usually never been read since it was written. A file with an atime a day newer than its mtime has been read at least once, but the recorded time is the first read after the threshold, not the most recent. noatime mounts disable the update entirely, and a read through the page cache without an inode touch may not update it either. Treating atime as a read log is the most common timestamp error, and it produces conclusions in both directions: false negatives on read, and false positives where a backup agent’s traversal touched everything.

What the timestamp can and cannot establish

Timestamps establish a lower bound and rarely an upper bound. A ctime of a given moment proves the inode was modified no earlier than that moment, provided the clock was correct and the structure is authentic. It does not prove the file was untouched afterwards, because a subsequent modification writes a later ctime that overwrites the earlier one.

That overwriting property is the reason timestamps are best treated as a chain of latest-writes rather than a history. The history lives in whatever separate structure records operations: the change journal on NTFS, the file system journal for recent transactions, an auditing layer that logged the syscall, or an external backup that captured the earlier state. Where none of those exists, the timestamps answer the question “what was the last metadata operation” and nothing else.

The acquisition discipline described in disk imaging and chain of custody applies directly here: a timestamp read from a mounted file system is a timestamp the mount may have changed. Reading timestamps from an image, or from a source blocked against writes, is what makes the values admissible as evidence of the file’s state rather than evidence of the examiner’s handling.

References

  1. statx(2): Linux man page — Linux man-pages project
  2. utimensat(2): Linux man page — Linux man-pages project
  3. Microsoft Windows Dev Center: SetFileTime function — Microsoft
  4. Microsoft Learn: Change Journals — Microsoft