Comprendre le USN Journal NTFS ($UsnJrnl:$J)

Une introduction pratique au Update Sequence Number Journal de NTFS : ce qu'il est, comment il est structuré sur disque et pourquoi il est si précieux en forensique Windows.

Par 7 min de lecture

Le Update Sequence Number Journal de NTFS est l'artefact vers lequel je me tourne en premier sur tout hôte Windows quand je dois savoir ce qui a touché le disque et quand. Il est activé par défaut sur chaque volume NTFS livré avec Windows depuis Vista, presque personne ne le désactive et presque personne ne le lit avant que quelque chose ne tourne mal. C'est cette asymétrie qui le rend utile.

Chaque création, suppression, renommage, troncature, basculement d'attribut et fermeture de fichier sur le volume ajoute un enregistrement à $UsnJrnl:$J. Les enregistrements sont petits, quelques dizaines d'octets chacun, et le fichier est un buffer circulaire de taille fixe, donc une machine chargée conserve à peu près les derniers millions d'opérations. Sur un poste de travail avec l'allocation par défaut de 32 Mo, cela représente quelques jours. Sur un serveur de fichiers dimensionné à 1 Go, des semaines.

Où le journal vit réellement

Le journal n'est pas un fichier ordinaire. C'est un flux de données alternatif nommé $J, attaché au fichier de métadonnées \$Extend\$UsnJrnl à la racine du volume. Deux conséquences qui mordent ceux qui n'ont jamais fait ça :

  • Un glisser-déposer dans l'Explorateur, un xcopy ou un robocopy copiera silencieusement le flux par défaut vide et tu te retrouveras avec un fichier de zéro octet. Utilise un outil qui comprend les ADS : FTK Imager, X-Ways, icat de The Sleuth Kit, RawCopy.exe, PowerForensics. Le billet associé sur l'extraction de $J depuis une image disque passe en revue chacun.
  • Il y a un flux frère appelé $Max sur le même fichier de métadonnées. Il ne contient que des métadonnées de taille et de configuration. Si tu extrais $Max par erreur, tu n'obtiens rien d'utile. Choisis toujours $J.

Le fichier lui-même est sparse. NTFS réserve la taille maximale configurée sur disque mais laisse la région inutilisée en tête à zéro. Quand tu extrais $J depuis une image, les premières centaines de mégaoctets sont souvent entièrement à zéro avant d'atteindre le premier vrai enregistrement. Tout parser raisonnable (usnrs, le USN-Journal-Parser de PoorBillionaire, MFTECmd, le parser WebAssembly sur cette page) saute automatiquement les zéros de tête.

Structure de l'enregistrement (USN_RECORD_V2)

Chaque enregistrement est une structure USN_RECORD_V2. Microsoft documente le layout dans winioctl.h ; le résumé champ par champ :

ChampOctetsSignification
RecordLength4Taille totale de l'enregistrement incluant le nom de fichier
Major / Minor version2 + 22.0 pour tout ce que tu verras en pratique
FileReferenceNumber8Entrée MFT plus séquence du fichier
ParentFileReferenceNumber8Entrée MFT plus séquence du répertoire parent
USN8Position de cet enregistrement dans le journal
Timestamp8Windows FILETIME (ticks de 100 ns depuis 1601-01-01)
Reason4Masque de bits décrivant ce qui a changé
SourceInfo4Indices (p. ex. DATA_MANAGEMENT pour les changements pilotés par l'OS)
SecurityId4Index dans $Secure:$SII
FileAttributes4Attributs NTFS standard
FilenameLength / Offset2 + 2Où le nom UTF-16 vit dans l'enregistrement
FilenamenUTF-16 LE, sans terminateur nul

Le masque Reason est le champ qui rapporte. Il est additif, pas exclusif : un seul enregistrement peut porter FileCreate | DataExtend | Close ensemble. Un cycle de vie typique de fichier ressemble à FileCreate | DataExtendDataExtend | CloseBasicInfoChange | CloseFileDelete | Close. Reconstruire cette séquence après coup, c'est comme ça que tu sais ce qui est réellement arrivé à un fichier. Le billet sur les Reason codes est la référence de champ pour chaque bit.

Deux champs que le nouveau lecteur sous-estime souvent :

  • FileReferenceNumber est ta clé de jointure. Groupe les enregistrements par ce champ et tu as toute l'histoire du fichier à travers les renommages, changements d'attribut et la suppression. Note que les 16 bits de poids fort sont un numéro de séquence : un fichier supprimé et un nouveau fichier alloué à la même entrée MFT partageront l'entrée mais différeront en séquence.
  • ParentFileReferenceNumber est la seule information de chemin dans l'enregistrement. Le nom de fichier n'est que la feuille. Il te faut une $MFT parsée pour remonter la chaîne des parents jusqu'à un chemin complet. Acquiers les deux, toujours.

Pourquoi il gagne sa place en DFIR

Deux raisons qui reviennent sur presque chaque engagement.

D'abord, le journal survit à la suppression. Une fois qu'un fichier a quitté l'espace de noms et que son entrée MFT a été réutilisée pour autre chose, l'enregistrement de sa création, sa croissance, son renommage et sa suppression est toujours dans $J jusqu'à ce que le buffer circulaire passe dessus. Pareil pour les fichiers vidés de la corbeille : les fichiers $I de la corbeille sont effacés, mais le journal se souvient. Lors d'une chasse au "ce fichier a-t-il jamais existé", le journal survivra à l'entrée MFT et aux métadonnées de la corbeille la plupart du temps.

Ensuite, il est peu coûteux et auto-descriptif. Un $J de 100 Mo contient typiquement de cinq à dix millions d'enregistrements couvrant des jours voire des semaines. Chaque enregistrement porte son propre horodatage, Reason, référence de fichier et parent. Tu peux l'envoyer dans un parser en secondes et pivoter sur n'importe quel champ. Il n'y a pas d'autre artefact Windows à ce prix.

Ce qu'il ne te dira pas, c'est qui. Pas d'utilisateur, pas de processus, pas de ligne de commande dans aucun enregistrement. Pour attacher un acteur, corrèle par horodatage avec l'événement 4663 (object access, ne marche que si les SACL ont été configurées, ce qui n'est presque jamais le cas) de Security.evtx, l'événement 11 de Sysmon (file create) ou les arbres de processus de l'événement 1 de Sysmon. Le journal répond au quoi et au quand. D'autres logs répondent au qui.

Où il s'inscrit dans la pile d'artefacts

Le modèle mental que j'utilise, ordonné de "état actuel" à "histoire passée" :

  • $MFT — l'index de l'état actuel. Chaque fichier et répertoire du volume, y compris quelques entrées récemment supprimées. Quatre timestamps par attribut, pas d'historique.
  • $UsnJrnl:$J — historique par opération, jours à semaines. Le sujet de ce que tu lis.
  • $LogFile — le journal de transactions de récupération sur crash de NTFS. Minutes à heures d'images avant/après brutes. Le billet USN journal vs $MFT vs $LogFile détaille quand recourir à chacun.

Mets le journal aux côtés de Prefetch, AmCache, Shimcache, des fichiers LNK et des jump lists, et tu peux généralement reconstituer à la minute ce qui a tourné, ce qui a été ouvert et ce qui a touché le disque.

Et après

Si tu n'as jamais ouvert un journal, le plus rapide pour bâtir une intuition est d'en récupérer un sur une machine de test et de le charger dans le parser en haut de cette page avec la $MFT correspondante. Filtre sur FileCreate et lis le fichier comme Windows l'a écrit. Les billets suivants de cette série creusent les flags Reason, la procédure d'extraction et les motifs que le journal expose pour le ransomware, l'exfiltration, le timestomping et la récupération de fichiers supprimés.

Lectures complémentaires

  • Microsoft Learn — Change Journals et la référence USN_RECORD_V2.
  • usnrs d'Airbus CERT — le parser open source de USN_RECORD_V2 le plus propre qui existe, et celui qui alimente le module WebAssembly de ce site.
  • Brian Carrier, File System Forensic Analysis — le traitement de la taille d'un livre sur les internes de NTFS qui n'a toujours pas été remplacé.