ReFS Forensics Reference

Timestomping Detection

Experimental / informational. Timestomp detection here is a corroborative aid, not a verdict. Its strongest intrinsic signal (CHANGE_LATE) is defeated by a native-API or raw-disk stomp that also sets the change time; ROUND_TIMESTAMPS is a weak, LOW-only signal that ordinary driver packages and archive extraction produce too; and every flag needs confirmation against the USN journal or a file’s hard-link siblings before you conclude tampering. Treat the output as leads to verify, not proof.

Timestomping — back-dating a file’s timestamps to hide when it was really created or written — is a standard anti-forensic move, and the technique an analyst reaches for first does not exist on ReFS. NTFS exposes it by comparing the two timestamp sets every file carries, the $STANDARD_INFORMATION set against the $FILE_NAME set; when a tool stomps $SI but leaves $FN untouched, the mismatch is the tell. ReFS carries one $SI timestamp set per name, not a second $FILE_NAME copy — so for a single-named file there is no $SI-vs-$FN twin to compare against and that particular cross-check is unavailable. A hard-linked file is the exception: each of its names holds an independent timestamp copy, so a name-scoped stomp leaves the sibling names sitting at the true birth, and comparing a file’s names’ MACB becomes a ReFS-specific tamper check (see Hard Links). Beyond that, timestomping is detectable through different anchors: the metadata-change time that common tools cannot reach, the USN change journal that records the tampering operation itself, and the volume’s own creation time as a hard floor. This page explains each anchor, why it works, and where it fails.

Three independent anchors

ReFS gives the analyst three things to lean on, in increasing order of strength:

  1. The metadata-change time is left behind. The high-level Windows APIs that ordinary timestomp tools call cannot set the change time, so the filesystem stamps it at the moment of the stomp while the forged times sit in the past. This is a heuristic against common tooling, not a guarantee.
  2. The USN journal records the edit. A deliberate timestamp change is logged as a BASIC_INFO_CHANGE record at its real time, and a file’s genuine creation is logged as a FILE_CREATE record. The journal is append-only, so these are the stronger, harder-to-forge anchors.
  3. The volume creation time is a floor. A file cannot have been created before the filesystem it lives on existed. Any creation time earlier than the volume’s own creation is structurally impossible.

The forefst.py <image> timestomp subcommand combines all three and ranks suspects by confidence.

Two more witnesses, when the journal is gone

The journal is the anchor an anti-forensic step removes first — and fsutil usn createjournal discards it outright while looking like a resize (see USN journal). Two other structures record a time independently, and both survive that:

  • The transaction log. A SetFileTime writes no log record of its own, so the act of stomping is not there. But the file’s creation is: an MLog CREATE transaction carries an embedded timestamp, and a creation time in $SI that disagrees with it is a contradiction between two structures. Measured on a volume whose journal had been destroyed, this caught every backdated birth present and flagged nothing else. It speaks to the birth time only — a forged last-write agrees with the CREATE record — and the log is a rolling buffer, so an old enough creation has aged out of it.
  • A stream snapshot. A snapshot descriptor timestamps the moment it was taken, not the file (see $SNAPSHOT). A creation time backdated below a snapshot of the same file is impossible.

Both are cross-checks an analyst runs today by reading mlog --parse and snapshots beside timestomp; folding them into the tool’s own corroboration column is planned work, not current behaviour.

Why the change time is the key intrinsic anchor

A ReFS $SI holds four FILETIMEs, all at fixed offsets in the type-0x10 own-row’s value (see $STANDARD_INFORMATION):

OffsetTimeLetter
0x00CreationB
0x08Modification / last writeM
0x10Metadata changeC
0x18Last accessA

The metadata-change time C is an ordinary timestamp — nothing about it is cryptographic or tamper-proof. What makes it useful is an API asymmetry: the high-level interfaces a timestomp tool normally uses expose only three of the four times.

  • Win32 SetFileTime(hFile, &Creation, &LastAccess, &LastWrite) has no ChangeTime parameter.
  • PowerShell Set-ItemProperty -Name CreationTime/LastWriteTime/LastAccessTime and .NET File.SetCreationTime / SetLastWriteTime / SetLastAccessTime expose the same three.

A caller using any of these cannot reach C, and the filesystem updates C to now as a side effect of the metadata write it performs. So after a back-dating stomp, B/M/A point into the past while C is left at the real moment of the operation. The signature is

C >> max(B, M)

flagged as CHANGE_LATE. For a single-named file it is the closest ReFS analogue of the NTFS $SI-versus-$FN comparison — using the not-normally-reachable change time as the reference instead of a second timestamp set. (When the file is hard-linked, a truer analogue is available: the sibling names’ own timestamp copies, discussed below as HARDLINK_MACB_MISMATCH.)

This defeats common tooling, not a determined attacker. The change time is settable through the native path: FILE_BASIC_INFORMATION.ChangeTime passed to NtSetInformationFile carries an explicit change time, and the ReFS driver honours it. The driver tracks, per open handle, which of the four times the caller supplied explicitly versus which the filesystem should auto-update — a flag per time (change included) that only exists because every one of the four times can be set explicitly. An attacker who sets all four through the native API, or who edits the raw $SI bytes on disk directly, leaves no CHANGE_LATE signal. The intrinsic change-time anchor catches PowerShell / SetFileTime / .NET timestomp tools; it does not catch someone who knows about ChangeTime. When the journal is present, it is the anchor that does not depend on the attacker’s tool choice.

The signals

The tool emits a set of named signals; each is a single comparison, and each has a different strength and a different way to be wrong.

SignalMeaningSourceStrength
CHANGE_LATE$SI change time (0x10) post-dates Created/Modified by a wide margin — back-dated B/M against the real metadata-write timeintrinsic ($SI only)strong vs common tools; defeated by a native-API or raw-disk stomp that sets C too
HARDLINK_MACB_MISMATCHtwo names of one hard-linked file disagree on Created — a name-scoped stomp rewrites only the opened name’s $SI copy, so the sibling name still holds the true birth (the latest Created among the names is authentic; only the back-dated name is flagged, never the clean sibling)intrinsic (per-name $SI)strong; journal-independent, and unlike NTFS the copies can diverge; only fires when a file has more than one name
USN_BASIC_INFO_CHANGEthe journal holds a standalone BASIC_INFO_CHANGE (reason 0x8000, with no FILE_CREATE or DATA_* bits) — a deliberate basic-info / timestamp edit with no content change, recorded at its real timeUSN journalauthoritative
USN_CREATE_MISMATCH$SI Created differs from the file’s FILE_CREATE journal record (the journal is append-only)USN journalauthoritative
PRE_FORMATCreated precedes the volume creation time — impossible for a file genuinely created hereintrinsic + volumestrong*
FUTURECreated after the volume’s last metadata writeintrinsic + volumemedium*
CREATE_GT_MODIFYCreated after last writeintrinsicweak*
ROUND_TIMESTAMPSboth Created and Modified land on a whole second (.0000000 sub-second) — a set date is one cause, but VirtIO/QEMU driver packages and archive extraction also produce whole-second stampsintrinsic ($SI only)LOW only — never rises above LOW, never upgrades another tier

* PRE_FORMAT, FUTURE, CREATE_GT_MODIFY — and CHANGE_LATE — also fire on a creation-time-preserving copy (robocopy /COPY:T, a backup restore) or a legitimate late rename or ACL change on an aged file: these keep or set an old creation time while the metadata-write / change time is recent. Such a file is a genuine inconsistency, but whether it is malicious needs analyst context. The USN journal is what turns a suspicion into a confirmation, because it records the operation that caused the inconsistency at the time it actually happened.

HARDLINK_MACB_MISMATCH is the exception to that dependence: a creation-preserving copy gives every name the same times, so it does not trip on benign copies — it fires only when a file’s own names genuinely disagree on Created, which a name-scoped stomp is the natural cause of. That makes it a self-contained tamper check that needs no journal and no volume-create floor to stand on.

Confidence tiers

The tool collapses the signal set into one of four verdicts. The dividing line is corroboration from an independent source — the change journal, or a hard-link sibling — not the number of intrinsic signals that happen to fire together.

TierReached whenReading
HIGHthe journal confirms a deliberate edit (USN_BASIC_INFO_CHANGE / USN_CREATE_MISMATCH), or a hard-link sibling preserves a different birth time (HARDLINK_MACB_MISMATCH)an independent source disagrees with $SI
MEDIUMCHANGE_LATE without PRE_FORMAT — created on this volume and altered afterwards — or FUTUREworth investigating; a copy cannot produce it
INFOonly the copy-signature set fires: PRE_FORMAT, CHANGE_LATE, CREATE_GT_MODIFY, ROUND_TIMESTAMPSambiguous, see below
LOWanything else that fires but fits none of the aboveweak; corroborate before use

Why PRE_FORMAT + CHANGE_LATE is INFO, and what INFO does not mean

These two are not independent. A timestamp-preserving copy — robocopy /COPY:T, a restore, an archive extraction — writes the original create/modify (which predate this volume, so PRE_FORMAT) and sets the change time to the moment of the copy (so CHANGE_LATE). They are two halves of one event, so treating their co-occurrence as two agreeing signals is wrong: it marked 95.4 % of the files on a real Windows installation as tampered, which trains an examiner to ignore the column.

But the pair does not identify that event as a copy. Deliberately back-dating a creation time produces exactly the same two signals — a lab volume whose generator log records 74 SetCreationTimeUtc actions carries this signature. So INFO means a copy and a back-dated creation are indistinguishable here, and the only thing that separates them is an independent source: the USN journal, or a hard-link sibling. INFO is a lowered tier, not an exoneration.

How often each signal fires

Base rates over the full corpus — 96 volumes, 521,060 files. Read them as a floor, not as a typical volume: the corpus is mostly small purpose-built lab images.

SignalBase rateTier contribution
PRE_FORMAT22.67 %INFO
CHANGE_LATE22.32 %MEDIUM only without PRE_FORMAT
CREATE_GT_MODIFY1.04 %INFO
ROUND_TIMESTAMPS0.04 %never lifts a tier
HARDLINK_MACB_MISMATCH0.04 %HIGH
FUTURE4 filesMEDIUM

The split by volume kind is the reason the tiers are shaped this way. On the corpus’s one real Windows installation (120,617 files) the copy signature fires on 95.7 % of files — that is what installing an operating system looks like — against 1.86 % across 95 lab-built volumes. Under a tiering that treated the pair as corroboration, almost every file on that volume was MEDIUM; the actionable set is now 385 files rather than roughly 115,000.

ROUND_TIMESTAMPS is capped by design. Whole-second Created and Modified stamps are suggestive of a deliberately set date but are not proof: VirtIO/QEMU driver packages and archive extraction also write whole-second times, so the signal never lifts another tier’s verdict.

The four tiers are assigned in one place, by the timestomp command, which share one function — see the tool reference for the command-line surface and --min TIER filtering. The column reaches a tier no higher than the command, because the listing does not read the journal.

What does not work as a substitute

Two tempting shortcuts do not hold up and should not be treated as primary signals:

  • The NextFileId ordinal ($SI+0x58 on v3.7–v3.10; ExternalFileId_1 on v3.4) is a per-directory child-creation ordinal — it records each child’s position in the directory’s creation sequence, written by RefsMoveFile incrementing the parent directory’s counter. A higher ordinal means the child was created later in that directory’s lifecycle, so a gross created-time-versus-ordinal inversion within one directory is corroborating. But it is coarse: it is directory-scoped, and it is 0 on native v3.14 own-rows (the ordinal moved into the object-record payload at v3.11), so it cannot be a general anchor.
  • A zero sub-second fraction (whole-second times, the low 100 ns part = 0) is sometimes left by timestomp tools that write second-granularity values, but it is entirely tool-dependent — a tool that copies full-resolution FILETIMEs leaves no such fingerprint, and VirtIO/QEMU driver packages and archive extraction produce whole-second stamps on ordinary files. The tool surfaces it as the ROUND_TIMESTAMPS signal but caps it at LOW for exactly this reason: it is a lead to corroborate (with the USN journal or a hard-link MACB comparison), never a primary signal on its own.

How the tool is wired

The detector has two parts, but one surface. timestomp_intrinsic_flags() computes the $SI-intrinsic signals (CHANGE_LATE, PRE_FORMAT, CREATE_GT_MODIFY, FUTURE) without touching the journal, and timestomp_verdict() turns signals into a tier. Both are reached only through forefst.py <image> timestomp, which loads the USN journal and adds the BASIC_INFO_CHANGE and create-mismatch signals before deciding:

Earlier releases also attached the intrinsic half to every files row as a TimestompFlags column. That column was removed, because a listing cannot read the journal: it could only ever carry the half of the answer that a timestamp-preserving copy also produces, while sitting in a column that looked like a verdict. Timestamp-anomaly information now comes from this subcommand alone.

forefst.py <image> timestomp                # flagged files, ranked by confidence
forefst.py <image> timestomp --min HIGH     # only high-confidence suspects
forefst.py <image> timestomp --json         # machine-readable
forefst.py <image> timestomp --csv out.csv  # per-file CSV (path, confidence, signals, MACB)
forefst.py <image> timestomp --all          # every file with its flags (incl. NONE)
forefst.py <image> timestomp --margin-days N # anomaly margin (default 1 day)

The --margin-days value is the slack the intrinsic comparisons allow before calling a gap suspicious, which keeps ordinary clock skew and sub-day operation latency from generating noise.

The two USN-based signals correlate a file to its journal records by File ID — the FileRef (HomeOid, ordinal), not the path. Because the File ID is unchanged by a rename or a cross-directory move, a file that was renamed or relocated after creation is still matched to its true FILE_CREATE record, so USN_CREATE_MISMATCH and USN_BASIC_INFO_CHANGE are not defeated by an intervening move — a stomp cannot be hidden simply by moving the file afterward. See File IDs.

Cross-references

  • $STANDARD_INFORMATION — the per-name timestamp set ReFS carries (B/M/C/A at 0x00/0x08/0x10/0x18) that every intrinsic signal reads
  • File IDs — the FileRef that ties a file to its journal records across renames and moves, so a stomp can’t be hidden by relocating the file
  • Hard Links — why each name of a file carries its own $SI copy, so their MACB can diverge under a name-scoped stomp (the HARDLINK_MACB_MISMATCH basis)
  • USN Journal — the append-only record whose BASIC_INFO_CHANGE (reason 0x8000) and FILE_CREATE entries are the authoritative anchors
  • Artifact Timeline — how the MACB times feed a unified event timeline
  • Forensic Analysis Workflow — the tamper check is step seven ($SI vs USN vs volume-create) of the standard workflow
  • Copy-on-Write — why a content-preserving copy can keep an old creation time and trip the intrinsic signals
  • What Survives — the format-time immutables that fix the volume creation floor

Two things that do not help

SetMace and tools like it do not work on ReFS. They edit $MFT records, and ReFS has no $MFT. A stomp on ReFS goes through the ordinary Windows API, which is why the change time is left behind.

A write to a named stream moves the parent file’s times. An ADS write updates the host file’s LastWrite and LastAccess, so a file whose content never changed can still carry a fresh modification time. Measured on format 3.14. Before reading a modification time as evidence of a content change, check whether the file has streams.

Evidence

The four-FILETIME $SI layout, the NextFileId ordinal, and the per-handle “caller supplied this time explicitly” tracking are confirmed in the driver: RefsComputeStandardInformationInternalFromFcb builds the $SI fields from the FCB, RefsMoveFile writes the directory child-creation ordinal, and the driver honours an explicit FILE_BASIC_INFORMATION.ChangeTime. The two journal-independent witnesses are (the MLog CREATE embedded timestamp measured against $SI on a controlled set whose journal had been destroyed) and (the snapshot descriptor records the snapshot moment, not a copy of $SI). The detection itself is validated on the raw-disk corpus against a controlled ground-truth image built by a replay log of SET_TIMESTAMPS operations that back-date creation by years on a freshly formatted volume: every HIGH-tier file has a provable basis (created before the volume existed and/or a journal-confirmed deliberate edit), the clean baseline and a clean operations image (real renames, ADS, snapshots, symlinks, hardlinks) flag nothing, and the same synthetic back-dated files are flagged identically wherever the test corpus was written. The journal’s standalone BASIC_INFO_CHANGE records sit on the timestomp image at the tampering’s real time while $SI shows the forged time. The per-name MACB behind HARDLINK_MACB_MISMATCH is a documented correction: $SI is stored per name entry (per hard link), not per inode, so a name-scoped stomp on one of two hard links leaves the sibling name at the true birth — measured directly on a two-name file where the opened name reads a back-dated Created and its sibling reads the real creation moment. Also registered for statements on this page:. The controlled-set calibration — eight files with one known cause each, six flagged and all six at INFO — is confirmed; that an ADS write updates the parent’s LastWrite and LastAccess is confirmed; that $MFT-editing tools cannot work here is confirmed.