Les opérations redo/undo du $LogFile NTFS expliquées
L'en-tête d'enregistrement du journal NTFS octet par octet, les 38 opcodes redo/undo, ceux qui comptent en forensique, et à quoi ressemble chaque action.
TL;DR. Un enregistrement du journal NTFS, c'est un en-tête LFS (LSN, LSN précédent, ID de transaction…) suivi d'un en-tête NTFS : opcode redo, opcode undo, emplacement des octets redo et undo, attribut ouvert visé, et position à l'intérieur de celui-ci. Il existe trente-huit opcodes (0x00–0x25), mais une poignée porte l'essentiel de la valeur forensique : InitializeFileRecordSegment, DeallocateFileRecordSegment, CreateAttribute / DeleteAttribute, UpdateResidentValue et les quatre opérations Add/DeleteIndexEntry*. Le reste est de l'intendance, dont vous avez tout de même besoin pour résoudre les cibles et regrouper les enregistrements.
Si le $LogFile paraît opaque, c'est qu'une seule action utilisateur — « renommer ce fichier » — produit plusieurs enregistrements, dont chacun n'a de sens qu'avec le bon contexte : quel attribut est ouvert sous quel index, quelle taille de cluster utilise le volume, à quoi ressemblait l'enregistrement FILE avant. Cet article sert de grille de décodage. La vue d'ensemble se trouve dans le guide complet du $LogFile.
Deux en-têtes par enregistrement
Chaque enregistrement commence par l'en-tête d'enregistrement LFS, suivi des données client. Pour le client NTFS, ces données commencent par leur propre en-tête. Offsets tirés du fslog.c de ntfs3 sous Linux :
En-tête d'enregistrement LFS (0x30 octets)
| Offset | Taille | Champ |
|---|---|---|
0x00 | 8 | LSN de l'enregistrement |
0x08 | 8 | LSN précédent du client (même transaction) |
0x10 | 8 | LSN undo suivant du client |
0x18 | 4 | Longueur des données client |
0x1C | 4 | ID du client |
0x20 | 4 | Type d'enregistrement : 1 = enregistrement client, 2 = redémarrage client |
0x24 | 4 | ID de transaction |
0x28 | 2 | Drapeaux (0x1 = l'enregistrement continue sur la page suivante) |
En-tête d'enregistrement NTFS (début des données client)
| Offset | Taille | Champ | Usage |
|---|---|---|---|
0x00 | 2 | Opération redo | Ce qu'il faut faire |
0x02 | 2 | Opération undo | Comment l'annuler |
0x04 / 0x06 | 2 + 2 | Offset / longueur redo | Où se trouvent les octets redo |
0x08 / 0x0A | 2 + 2 | Offset / longueur undo | Où se trouvent les octets undo |
0x0C | 2 | Attribut cible | Index dans la table des attributs ouverts |
0x0E | 2 | Nombre de LCN | Nombre de numéros de cluster à 0x20 |
0x10 | 2 | Offset d'enregistrement | Offset de l'attribut dans l'enregistrement FILE (ou de l'entrée dans un tampon d'index) |
0x12 | 2 | Offset d'attribut | Offset du changement dans cet attribut |
0x14 | 2 | Offset de bloc de cluster | En unités de 512 octets, dans le cluster cible |
0x18 | 8 | VCN cible | Cluster virtuel dans l'attribut cible |
0x20 | 8 × n | LCN cibles | Clusters physiques |
Les enregistrements de type 2 sont des points de contrôle (zones de redémarrage client), pas des changements. L'article Zone de redémarrage et LSN les couvre.
Retrouver l'entrée MFT visée par un enregistrement
Les enregistrements ne disent pas « entrée MFT 322 ». Ils disent « attribut n° N de la table des attributs ouverts, VCN v, offset de bloc de cluster c ». Quand l'attribut cible est $MFT:$DATA, l'entrée vaut :
entry = (VCN × cluster_size + cluster_block_offset × 512) / mft_record_size
Il faut donc deux paramètres du volume. Un parseur peut les lire dans le secteur d'amorçage ou, comme le fait le parseur de $LogFile dans le navigateur, les calibrer à partir du journal lui-même : les enregistrements InitializeFileRecordSegment contiennent une image complète d'enregistrement FILE dont l'en-tête indique son propre numéro, et la combinaison taille de cluster / taille d'enregistrement qui les place correctement le plus souvent l'emporte.
La table des attributs ouverts se reconstruit à partir des enregistrements OpenNonresidentAttribute et des OpenAttributeTableDump et AttributeNamesDump écrits à chaque point de contrôle. La structure de ses entrées diffère selon la version du client NTFS (0x28 ou 0x2C octets), et c'est l'un des points où les parseurs peuvent diverger.
Les 38 opérations, par famille
Les noms ci-dessous sont ceux de l'enum NTFS_LOG_OPERATION de ntfs3, que le parseur reprend.
| Code | Opération | Famille | Valeur forensique |
|---|---|---|---|
0x00 | Noop | — | Côté undo de nombreux enregistrements sans annulation |
0x01 | CompensationLogRecord | Transaction | Écrit pendant une annulation |
0x02 | InitializeFileRecordSegment | Enregistrement FILE | Élevée : image complète d'une entrée nouvelle ou réutilisée |
0x03 | DeallocateFileRecordSegment | Enregistrement FILE | Élevée : entrée libérée (suppression) |
0x04 | WriteEndOfFileRecordSegment | Enregistrement FILE | Faible |
0x05 | CreateAttribute | Attribut | Élevée : nouvel attribut, par ex. $FILE_NAME ou $DATA résident |
0x06 | DeleteAttribute | Attribut | Élevée : attribut retiré, par ex. l'ancien $FILE_NAME lors d'un renommage |
0x07 | UpdateResidentValue | Attribut | Élevée : horodatages $STANDARD_INFORMATION, petites valeurs résidentes |
0x08 | UpdateNonresidentValue | Attribut | Moyenne : tampons d'index, écritures $UsnJrnl, autres flux |
0x09 | UpdateMappingPairs | Attribut | Moyenne : runs de données (croissance du fichier) |
0x0A | DeleteDirtyClusters | Attribut | Faible |
0x0B | SetNewAttributeSizes | Attribut | Moyenne : changements de taille |
0x0C / 0x0D | Add / DeleteIndexEntryRoot | Index | Élevée : entrées de répertoire dans $INDEX_ROOT |
0x0E / 0x0F | Add / DeleteIndexEntryAllocation | Index | Élevée : entrées de répertoire dans $INDEX_ALLOCATION |
0x10 | WriteEndOfIndexBuffer | Index | Faible |
0x11 / 0x12 | SetIndexEntryVcnRoot / Allocation | Index | Faible (mécanique du B-tree) |
0x13 / 0x14 | UpdateFileNameRoot / Allocation | Index | Moyenne : infos $FILE_NAME dupliquées dans $I30 |
0x15 / 0x16 | Set / ClearBitsInNonresidentBitMap | Bitmap | Faible isolément : allocation de clusters ou d'entrées MFT |
0x17 | HotFix | — | Rare |
0x18 | EndTopLevelAction | Transaction | Structure |
0x19–0x1B | Prepare / Commit / ForgetTransaction | Transaction | Bornes de transaction |
0x1C | OpenNonresidentAttribute | Table | Nécessaire pour résoudre les cibles |
0x1D–0x20 | Dumps OpenAttributeTable / AttributeNames / DirtyPageTable / TransactionTable | Point de contrôle | Nécessaires pour résoudre les cibles |
0x21 / 0x22 | UpdateRecordDataRoot / Allocation | Index | Moyenne : index hors $I30 ($ObjId, $Quota, $Secure) |
0x23 / 0x24 | UpdateRelativeDataInIndex / 2 | Index | Faible |
0x25 | ZeroEndOfFileRecord | Enregistrement FILE | Faible |
L'undo est généralement l'inverse du redo : AddIndexEntryRoot s'annule par DeleteIndexEntryRoot, CreateAttribute par DeleteAttribute, SetBits… par ClearBits…, et UpdateResidentValue par un autre UpdateResidentValue qui transporte les anciens octets. Les opérations dont l'annulation ne demande rien, comme l'initialisation d'un enregistrement FILE neuf, ont souvent Noop comme undo.
À quoi ressemblent les actions courantes
Ce sont les motifs que recherche un parseur. Les séquences réelles contiennent davantage d'enregistrements (bitmaps, tailles, sécurité, écritures USN) et l'ordre exact peut varier selon la version de Windows.
Création de fichier
InitializeFileRecordSegmentsur la nouvelle entrée. Redo = image complète de l'enregistrement FILE : en-tête (numéro de séquence, drapeaux),$STANDARD_INFORMATION,$FILE_NAME(nom, référence du parent, quatre horodatages), éventuellement un petit$DATA.AddIndexEntryRootouAddIndexEntryAllocationdans l'index$I30du répertoire parent. L'entrée d'index embarque une copie de$FILE_NAME.
Suppression
DeleteIndexEntryRoot/DeleteIndexEntryAllocationqui retire le nom du parent (l'entrée retirée se trouve dans les octets undo, parfois dans les octets redo).DeallocateFileRecordSegmentsur l'entrée, avecClearBitsInNonresidentBitMapsur le bitmap de la MFT.
Renommage ou déplacement
- dfir.ru décrit un renommage comme
DeleteIndexEntryRoot,DeleteAttribute(ancien$FILE_NAME),CreateAttribute(nouveau$FILE_NAME),AddIndexEntryRoot(How the $LogFile works?). Un déplacement est identique, avec un autre parent dans l'entrée ajoutée. Pour les gros répertoires, ce sont les variantes…Allocationqui apparaissent à la place de…Root.
Modification d'horodatage
UpdateResidentValuevisant l'attribut$STANDARD_INFORMATION(premier attribut d'un enregistrement FILE, à l'offset0x38dans les enregistrements NTFS 3.1). Le redo contient les nouveaux octets, l'undo les anciens. La mise à jour peut être partielle — dfir.ru signale qu'elle peut commencer à l'horodatage M — si bien qu'un parseur doit utiliser l'offset d'attribut pour savoir lequel des quatre horodatages il lit. La détection du timestomping repose sur ce point.
Contenu des petits fichiers
- Un attribut
$DATArésident créé parCreateAttributeou inclus dans l'image de l'enregistrement FILE peut contenir les premiers octets d'un petit fichier. Les modifications ultérieures sont une autre histoire : la documentation de LogFileParser indique que, sur les Windows récents, le contenu des modifications de données résidentes n'est généralement pas stocké, seulement le fait qu'un changement a eu lieu. Voir Retrouver les traces de fichiers supprimés.
Regrouper les enregistrements en actions
Deux approches :
- Par transaction. Utiliser l'ID de transaction et la chaîne des LSN précédents, bornée par les enregistrements
ForgetTransactionou de validation. La plus fidèle, mais plus délicate à réussir d'un point de contrôle à l'autre. - Par entrée MFT et proximité. Prendre les enregistrements qui touchent la même entrée dans une fenêtre de LSN. Plus simple et robuste face aux pages endommagées, mais deux actions sans rapport sur le même fichier, rapprochées dans le temps, peuvent fusionner.
Le parseur dans le navigateur utilise actuellement la seconde approche (une fenêtre de ±256 enregistrements par entrée MFT) et expose chaque enregistrement brut pour que vous puissiez vérifier le regroupement. Le regroupement par transaction est prévu ; utilisez LogFileParser et NTFS Log Tracker pour confirmer.
Lire les octets vous-même
Quand un événement compte, ouvrez l'enregistrement brut : contrôlez les opcodes redo/undo, l'attribut cible (par exemple #322:$STANDARD_INFORMATION ou $MFT:$DATA), l'offset d'enregistrement, l'offset d'attribut et l'hexadécimal. Un undo de 32 octets à l'offset d'attribut 0 de $STANDARD_INFORMATION contient quatre valeurs FILETIME en petit-boutiste : création, modification, modification de l'entrée MFT, dernier accès.
Questions fréquentes
Que sont les opérations redo et undo du $LogFile ?
Chaque enregistrement du journal NTFS décrit un changement de métadonnées deux fois : l'opération et les octets redo disent comment réappliquer le changement, l'opération et les octets undo comment l'annuler. La reprise rejoue le redo des transactions validées et applique l'undo des transactions non validées.
Quelles opérations du $LogFile montrent la création ou la suppression d'un fichier ?
Une création montre en général InitializeFileRecordSegment sur le nouvel enregistrement FILE, plus AddIndexEntryRoot ou AddIndexEntryAllocation dans le répertoire parent. Une suppression montre DeleteIndexEntryRoot ou DeleteIndexEntryAllocation suivi de DeallocateFileRecordSegment.
Pour aller plus loin
- Noyau Linux,
fs/ntfs3/fslog.c— définitions des structures et implémentation complète du rejeu. - Joakim Schicht, readme de LogFileParser — les opérations qu'il décode et ce qu'elles contiennent.
- Maxim Suhanov, How the $LogFile works?