Crack open any USN record, any MFT entry, any registry hive, any LNK file, any Prefetch trace, any jump list, and the timestamps you find are all the same shape: a 64-bit integer that reads as nonsense without conversion. That format is FILETIME. Learn it once, read every Windows artefact for the rest of your career.
The definition
A FILETIME is a 64-bit unsigned integer counting 100-nanosecond intervals since 1601-01-01 00:00:00 UTC. That is the whole specification.
The choice of 1601 is not arbitrary — it is the start of the 400-year Gregorian cycle that contains the present, which lets calendar arithmetic skip the leap-year edge cases that Unix time has to handle. Microsoft documents the type in the FILETIME structure reference.
A worked example: FILETIME 133593600000000000 is 2024-08-09 12:00:00 UTC. Verify your converter against that value before you trust it on case data.
Converting to Unix time
Two arithmetic steps:
- Divide by 10,000,000 to go from 100-ns ticks to seconds.
- Subtract 11,644,473,600 — the number of seconds between 1601-01-01 and 1970-01-01.
In pseudocode:
unix_seconds = filetime / 10_000_000 - 11_644_473_600
In Rust, which is exactly what usnrs::Entry::unix_timestamp does:
pub fn unix_timestamp(&self) -> i64 {
(self.timestamp as i64) / 10_000_000 - 11_644_473_600
}
In Python, where the standard library handles the calendar arithmetic for you:
unix_seconds = filetime / 10_000_000 - 11_644_473_600
# with subsecond precision:
from datetime import datetime, timezone, timedelta
EPOCH = datetime(1601, 1, 1, tzinfo=timezone.utc)
dt = EPOCH + timedelta(microseconds=filetime / 10)
In JavaScript, where you need BigInt to preserve 64-bit precision:
const unixMs = Number((filetime - 116444736000000000n) / 10000n);
const date = new Date(unixMs);
In SQL (Postgres), useful when you have a column of raw FILETIME values dumped from a Plaso export:
SELECT TIMESTAMP '1601-01-01 00:00:00' + (filetime / 10000000) * INTERVAL '1 second';
Sub-second precision
The full 100-ns resolution rarely matters for forensics. Disk timestamps are quantised to whatever NTFS bothered to record, and $STANDARD_INFORMATION updates are driven by syscalls rather than by the system clock. But the cases where it does matter — correlating a USN record with a packet capture, lining up a Sysmon event 11 against a DataExtend — you want to keep the value in 100-ns ticks until the very last step:
import datetime as dt
EPOCH = dt.datetime(1601, 1, 1, tzinfo=dt.timezone.utc)
ticks_100ns = 133593612345678901
microseconds = ticks_100ns // 10
nanoseconds_remainder = (ticks_100ns % 10) * 100
timestamp = EPOCH + dt.timedelta(microseconds=microseconds)
print(timestamp, f"+{nanoseconds_remainder}ns")
Python's datetime covers everything except the last digit. Most analysts truncate to milliseconds without losing meaningful precision.
Variants you will run into
Windows ships several timestamp encodings and they leak across artefacts in ways that catch new analysts out:
| Format | Where it appears | Encoding |
|---|---|---|
FILETIME | $MFT, $UsnJrnl, registry, EVTX | 64-bit LE, 100-ns ticks since 1601-01-01 UTC |
SYSTEMTIME | EVTX rendered output, some COM APIs | 8× 16-bit fields (year, month, day, ...) |
TIME_T (32-bit) | Older registry keys, pagefile headers | Unix seconds since 1970, 32-bit |
DOSTIME | FAT (occasionally leaks into NTFS metadata copied from FAT) | Packed 16-bit date + 16-bit time, local time |
On disk, FILETIME is little-endian — the LSB comes first. xxd shows bytes left-to-right; flip the byte order before interpretation if you are reading by hand. Tools handle this automatically.
Gotchas that have cost real time
Endianness in raw dumps. Hex viewers display in memory order; the conversion arithmetic assumes you have the integer value. Bytes need to be reversed first.
The zero sentinel. FILETIME = 0 is technically 1601-01-01 00:00:00 UTC. In practice Windows uses it to mean "never set". Treat zero as null at the presentation layer or you will publish reports that claim a file was last accessed in 1601. The same trap applies to certain $FILE_NAME timestamps NTFS leaves unset.
Signed vs unsigned. Some Windows internals (and some parsers built on top of them) treat FILETIME as signed int64. Values past roughly 30828 AD wrap into negatives, which obviously does not matter in practice — but a deliberately tampered value can land in that range and break a buggy converter. Stay unsigned.
Local vs UTC. FILETIME is always UTC. If a tool displays local time, it converted on output. For DFIR you almost always want UTC at the storage layer — convert to local zones only when rendering for a stakeholder. Cross-host correlation across time zones falls apart otherwise.
$STANDARD_INFORMATION vs $FILE_NAME. Both live in every $MFT entry, both store FILETIME M/A/C/B times. The SI values are user-space writable; the FN copies update less often and are much harder to change. Comparing the two is the classic timestomping detection, and the timestomping post walks through exactly how the USN journal complements that comparison.
Verification table
If you write your own converter — and at some point everyone does, because the language they are stuck with does not handle the conversion natively — these values are the ones I check against:
| FILETIME | UTC date |
|---|---|
0 | 1601-01-01 00:00:00 (or null, by convention) |
116444736000000000 | 1970-01-01 00:00:00 |
132923520000000000 | 2022-02-23 16:00:00 |
133593600000000000 | 2024-08-09 12:00:00 |
If your converter produces those four, it produces the right thing for everything else.
Further reading
- Microsoft Learn — FILETIME structure and the related FileTimeToSystemTime function.
- Joachim Metz's libfwnt — the reference C implementation for FILETIME conversion that almost every libyal parser uses.
- SANS DFIR — Windows time rules cheat sheet covers how
$STANDARD_INFORMATIONand$FILE_NAMEtimestamps update under different operations, which is the practical complement to the FILETIME format itself.