← Torna al blog

Windows FILETIME spiegato — convertire i timestamp NTFS in qualcosa di leggibile

FILETIME è il formato di timestamp Windows che incontrerai in $MFT, $UsnJrnl, registro, $Recycle.Bin e quasi ogni artefatto Windows. Riferimento breve e completo: cos'è, come convertirlo, le insidie.

4 min di lettura

Apri qualsiasi record di journal USN, qualsiasi entry MFT, qualsiasi hive di registro, e i timestamp che trovi hanno tutti la stessa forma: un intero a 64 bit che non significa nulla senza conversione. Quel formato è FILETIME, e una volta che ci hai preso confidenza, ogni altro formato di tempo Windows acquista senso.

Cos'è FILETIME

Un FILETIME è un intero a 64 bit senza segno. Il suo valore è il numero di intervalli di 100 nanosecondi dal 1° gennaio 1601 alle 00:00:00 UTC. Tutto qui.

La scelta del 1601 non è arbitraria — è l'inizio del ciclo gregoriano di 400 anni che contiene oggi, il che permette all'aritmetica del calendario di restare coerente senza casi limite di anni bisestili. Microsoft documenta il tipo nella reference della struttura FILETIME.

Un piccolo esempio: il FILETIME 133593600000000000 corrisponde a 2024-08-09 12:00:00 UTC.

Conversione in tempo Unix

La conversione si fa in due passi aritmetici:

  1. Dividere per 10 000 000 per passare da tick di 100 ns a secondi.
  2. Sottrarre 11 644 473 600 — il numero di secondi fra 1601-01-01 e 1970-01-01.

In pseudo-codice:

unix_seconds = filetime / 10_000_000 - 11_644_473_600

In Rust (esattamente quello che fa usnrs::Entry::unix_timestamp):

pub fn unix_timestamp(&self) -> i64 {
    (self.timestamp as i64) / 10_000_000 - 11_644_473_600
}

In Python:

unix_seconds = filetime / 10_000_000 - 11_644_473_600
# o con precisione sotto il secondo:
from datetime import datetime, timezone, timedelta
EPOCH = datetime(1601, 1, 1, tzinfo=timezone.utc)
dt = EPOCH + timedelta(microseconds=filetime / 10)

In JavaScript:

// filetime è un BigInt per preservare la precisione a 64 bit
const unixMs = Number((filetime - 116444736000000000n) / 10000n);
const date = new Date(unixMs);

In SQL (Postgres):

SELECT TIMESTAMP '1601-01-01 00:00:00' + (filetime / 10000000) * INTERVAL '1 second';

Precisione sotto il secondo

La risoluzione completa a 100 ns conta raramente in forense — i timestamp su disco sono quantizzati a quello che il SO ha deciso di registrare, e $STANDARD_INFORMATION si aggiorna per syscall, non per orologio. Ma quando conta (correlazione con catture di pacchetti, per esempio), mantenere il valore in tick di 100 ns fino all'ultimo passo:

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")

La risoluzione a microsecondi del datetime di Python copre tutto tranne l'ultima cifra di ticks_100ns. La maggior parte degli analisti tronca ai millisecondi senza perdere precisione significativa.

Varianti che incontrerai

Diversi artefatti Windows usano codifiche leggermente diverse della stessa idea:

FormatoDove appareCodifica
FILETIME$MFT, $UsnJrnl, registro, EVTX64 bit LE, tick di 100 ns dal 1601-01-01 UTC
SYSTEMTIMEEVTX, alcune API COM8× campi a 16 bit (anno, mese, giorno…)
TIME_T (32 bit)Vecchie chiavi di registroSecondi Unix dal 1970, 32 bit
DOSTIMEFAT (talvolta filtra nei metadati NTFS)Data 16 bit + ora 16 bit compattati, ora locale

Parsando una struttura binaria, l'ordine dei byte è little-endian — il LSB del FILETIME viene per primo.

Insidie comuni

  • Endianness nei dump grezzi. xxd mostra i byte da sinistra a destra; i byte FILETIME vanno invertiti prima dell'interpretazione. Strumenti e parser lo fanno automaticamente; l'analisi manuale di solito implica invertire l'ordine prima.
  • Valore zero. FILETIME = 0 è un valore reale (1601-01-01) ma anche un sentinella per "mai impostato". Trattarlo come null in visualizzazione.
  • Stranezze del complemento a due. Alcune implementazioni (anche interne a Windows) trattano FILETIME come int64 con segno, cosa che fa avvolgere in negativo i valori oltre il ~30828 d.C. Nel dubbio, usare senza segno.
  • Locale vs UTC. FILETIME è sempre UTC. Se uno strumento ti mostra ora locale, ha convertito in uscita. Per DFIR vuoi quasi sempre UTC per correlare fra host.
  • Timestamp $STANDARD_INFORMATION vs $FILE_NAME. Entrambi vivono in $MFT, entrambi sono FILETIME, entrambi registrano tempi M/A/C/B. Le copie in $FILE_NAME si aggiornano meno spesso, ed è per questo che il rilevamento di timestomping confronta i due. Le note di Brian Carrier e il paper Mandiant sul timestomp (oggi dietro le note di X-Ways) sono i riferimenti standard.

Una piccola tabella di conversione per verificare

Se scrivi il tuo convertitore, questi valori sono comodi per validarlo:

FILETIMEData UTC
01601-01-01 00:00:00
1164447360000000001970-01-01 00:00:00
1329235200000000002022-02-23 16:00:00
1335936000000000002024-08-09 12:00:00

Se il tuo convertitore produce questi, produce il risultato giusto.