Ouvrez n'importe quel enregistrement de journal USN, n'importe quelle entrée MFT, n'importe quelle ruche du registre, et les timestamps que vous trouvez ont tous la même forme : un entier 64 bits qui ne signifie rien sans conversion. Ce format est FILETIME, et une fois qu'on a le réflexe, tous les autres formats d'horodatage Windows font sens.
Ce qu'est FILETIME
Un FILETIME est un entier 64 bits non signé. Sa valeur est le nombre d'intervalles de 100 nanosecondes depuis le 1er janvier 1601 à 00:00:00 UTC. Voilà.
Le choix de 1601 n'est pas arbitraire — c'est le début du cycle grégorien de 400 ans qui contient aujourd'hui, ce qui permet à l'arithmétique calendaire de rester cohérente sans cas particuliers liés aux années bissextiles. Microsoft documente le type dans la référence FILETIME.
Petit exemple : le FILETIME 133593600000000000 correspond à 2024-08-09 12:00:00 UTC.
Conversion vers le temps Unix
La conversion se fait en deux étapes arithmétiques :
- Diviser par 10 000 000 pour passer de tics de 100 ns à des secondes.
- Soustraire 11 644 473 600 — le nombre de secondes entre 1601-01-01 et 1970-01-01.
En pseudo-code :
secondes_unix = filetime / 10_000_000 - 11_644_473_600
En Rust (c'est exactement ce que fait usnrs::Entry::unix_timestamp) :
pub fn unix_timestamp(&self) -> i64 {
(self.timestamp as i64) / 10_000_000 - 11_644_473_600
}
En Python :
unix_seconds = filetime / 10_000_000 - 11_644_473_600
# ou avec précision sous la seconde :
from datetime import datetime, timezone, timedelta
EPOCH = datetime(1601, 1, 1, tzinfo=timezone.utc)
dt = EPOCH + timedelta(microseconds=filetime / 10)
En JavaScript :
// filetime est un BigInt pour conserver la précision 64 bits
const unixMs = Number((filetime - 116444736000000000n) / 10000n);
const date = new Date(unixMs);
En SQL (Postgres) :
SELECT TIMESTAMP '1601-01-01 00:00:00' + (filetime / 10000000) * INTERVAL '1 second';
Précision sous la seconde
La résolution complète de 100 ns importe rarement en forensique — les timestamps sur disque sont quantisés à ce que l'OS a bien voulu enregistrer, et $STANDARD_INFORMATION est mis à jour par appel système, pas par horloge. Mais dans les cas où ça compte (corrélation avec des captures réseau, par exemple), garder la valeur en tics de 100 ns jusqu'à la dernière étape :
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 résolution microseconde du datetime Python couvre tout sauf le dernier chiffre de ticks_100ns. La plupart des analystes tronquent à la milliseconde sans perte de précision significative.
Variantes que l'on rencontre
Plusieurs artefacts Windows utilisent des encodages légèrement différents de la même idée :
| Format | Où il apparaît | Encodage |
|---|---|---|
FILETIME | $MFT, $UsnJrnl, registre, EVTX | 64 bits LE, tics de 100 ns depuis 1601-01-01 UTC |
SYSTEMTIME | EVTX, certaines API COM | 8 × 16 bits (année, mois, jour…) |
TIME_T (32 bits) | Anciennes clés de registre | Secondes Unix depuis 1970, 32 bits |
DOSTIME | FAT (parfois infiltré dans les métadonnées NTFS) | 16 bits date + 16 bits heure compactés, en local |
Pour parser une structure binaire, l'ordre des octets est little-endian — le LSB du FILETIME vient en premier.
Pièges classiques
- Endianness en dump brut.
xxdaffiche les octets de gauche à droite ; les octets FILETIME doivent être inversés avant interprétation. Les outils et parseurs s'en chargent automatiquement ; l'analyse manuelle implique habituellement d'inverser l'ordre d'abord. - Valeur zéro.
FILETIME = 0est une valeur réelle (1601-01-01) mais aussi une sentinelle pour « jamais positionné ». À traiter comme null à l'affichage. - Bizarreries du complément à deux. Certaines implémentations (y compris internes à Windows) traitent
FILETIMEcomme unint64signé, ce qui fait que les valeurs au-delà de ~30828 enroulent en négatif. En cas de doute, utiliser non signé. - Local vs UTC. FILETIME est toujours UTC. Si un outil affiche du local, c'est qu'il a converti à la sortie. Pour la DFIR, on veut presque toujours de l'UTC pour corréler entre postes.
- Timestamps
$STANDARD_INFORMATIONvs$FILE_NAME. Les deux vivent dans$MFT, les deux sont du FILETIME, les deux enregistrent des temps M/A/C/B. Les copies dans$FILE_NAMEse mettent à jour moins souvent, c'est pourquoi la détection de timestomping compare les deux. Les notes de Brian Carrier et le papier Mandiant sur le timestomp (désormais devancé par les notes X-Ways) sont les références standard.
Une petite table de conversion pour vérifier
Si vous écrivez votre propre convertisseur, ces valeurs sont pratiques pour valider :
| FILETIME | Date UTC |
|---|---|
0 | 1601-01-01 00:00:00 |
116444736000000000 | 1970-01-01 00:00:00 |
132923520000000000 | 2022-02-23 16:00:00 |
133593600000000000 | 2024-08-09 12:00:00 |
Si votre convertisseur produit cela, il produit le bon résultat.