Chaque enregistrement MFT fait 1 024 octets. Chacun est disposé de la même manière. Si vous savez en lire un dans un éditeur hexadécimal, vous savez tous les lire, et vous pouvez écrire un parser qui bat les outils commerciaux cassants pour récupérer des enregistrements endommagés. La disposition est assez petite pour qu'une paire d'heures et 010 Editor vous rendent fluide.
C'est la tournée octet par octet que je donne aux analystes qui veulent cesser de traiter les parsers MFT comme des boîtes noires.
La signature FILE
46 49 4C 45 ("FILE")
Quatre premiers octets. ASCII FILE. C'est la magie qui vous permet de tailler des enregistrements MFT à partir de clusters bruts quand $MFT elle-même est illisible. Parcourez une image disque aux frontières de 512 octets (NTFS plus ancien) ou de 4096 octets (Win10 avec enregistrements 4K, rare mais existant), faites un pattern-match sur 46 49 4C 45, et vous pouvez généralement récupérer la majeure partie d'une MFT même quand l'en-tête propre de la table est détruit.
L'autre signature que vous verrez dans la nature est BAAD (42 41 41 44). NTFS l'écrit quand chkdsk a décidé qu'un enregistrement était irréparable. L'emplacement est conservé, le numéro de séquence est préservé, mais il ne faut pas faire confiance au reste des octets. Traitez les enregistrements BAAD comme la preuve que chkdsk a tourné et comme des cibles pour l'analyse du contexte environnant. N'essayez pas de parser leurs attributs naïvement.
L'en-tête de l'enregistrement (offsets 0x00 à 0x37, à peu près)
Après les quatre octets de signature, NTFS dispose un en-tête qui varie légèrement selon la version NTFS. Les champs, avec offsets depuis le début de l'enregistrement, que vous utilisez vraiment :
0x00 4 octets Signature ("FILE" ou "BAAD")
0x04 2 octets Offset vers le update sequence array (USA)
0x06 2 octets Taille USA en mots 16 bits (records=count of (USN+fixup_entries))
0x08 8 octets Numéro de séquence $LogFile (LSN)
0x10 2 octets Numéro de séquence
0x12 2 octets Nombre de hard links
0x14 2 octets Offset vers le premier attribut
0x16 2 octets Drapeaux (bit 0 = IN_USE, bit 1 = DIRECTORY,
bit 2 = QUOTA_CHARGED, bit 3 = HAS_VIEW_INDEX)
0x18 4 octets Taille utilisée de l'enregistrement
0x1C 4 octets Taille allouée (toujours 1024 sur les volumes standards)
0x20 8 octets Référence à l'enregistrement de fichier de base (non nul sur les enregistrements d'extension)
0x28 2 octets ID du prochain attribut
0x2A 2 octets (padding/alignement sur NTFS 3.0)
0x2C 4 octets Numéro d'enregistrement (NTFS 3.1+ ; auto-référence)
Quelques-uns méritent une attention particulière.
Drapeaux à 0x16. Bit 0 effacé signifie supprimé. Bit 1 positionné signifie répertoire (l'enregistrement contient des attributs d'index plutôt que $DATA). La combinaison de ces deux drapeaux significatifs est ce qui fait qu'un octet vous en dit beaucoup sur une entrée.
Numéro de séquence à 0x10. Incrémenté à chaque réutilisation de l'emplacement. La référence fichier 64 bits (numéro d'enregistrement dans les 48 bits bas, numéro de séquence dans les 16 hauts) est l'identifiant unique réel pour l'existence d'un fichier particulier. Les références dans d'autres attributs (référence parent $FILE_NAME, entrées $ATTRIBUTE_LIST) utilisent cette forme 64 bits. Une référence dont la séquence ne correspond pas à l'enregistrement actuel pointe vers un occupant précédent ; généralement un fichier supprimé. C'est ainsi que le Sleuth Kit parcourt les chaînes de répertoires supprimés.
Référence à l'enregistrement de fichier de base à 0x20. Zéro sur un enregistrement de base ; non nul sur un enregistrement d'extension (quand les attributs d'un fichier débordent un seul emplacement). La valeur non nulle est la référence 64 bits de l'enregistrement de base auquel appartient cette extension. Les parsers doivent suivre les chaînes $ATTRIBUTE_LIST pour assembler le fichier complet.
Numéro de séquence $LogFile à 0x08. Pointe dans $LogFile. Utile pour la récupération au niveau transactionnel ; moins utile pour l'analyse de routine. Ça vaut la peine de savoir qu'il existe.
Le tableau fixup (update sequence)
NTFS protège contre les écritures déchirées avec un petit truc. Chaque enregistrement de 1 024 octets est divisé en deux secteurs de 512 octets. Avant d'écrire, NTFS :
- Choisit un numéro de séquence de mise à jour (USN) 16 bits (sans lien avec l'USN de
$UsnJrnlmalgré l'acronyme commun). - Range les deux derniers octets originaux de chaque secteur dans un tableau qui se trouve juste après l'en-tête.
- Remplace les deux derniers octets de chaque secteur par l'USN elle-même.
À la lecture, NTFS vérifie que les deux derniers octets de chaque secteur de 512 octets sont égaux à l'USN choisi. S'ils le sont, l'écriture était atomique ; tirez les octets originaux du tableau fixup et remettez-les en place. Si la queue d'un secteur ne correspond pas, l'écriture était déchirée et l'enregistrement est suspect.
Le tableau est disposé comme un mot USN suivi de N mots de fixup, où N est le nombre de secteurs. Pour un enregistrement de 1 024 octets sur un volume à secteurs de 512 octets, N = 2. Le tableau occupe donc 6 octets au total (USN, fixup_for_sector_0, fixup_for_sector_1). Son offset est la valeur 2 octets à 0x04 de l'enregistrement (typiquement 0x2A ou 0x30 selon la version NTFS).
La conséquence pratique : si vous lisez des morceaux bruts de 1 024 octets de $MFT sans appliquer les fixups, chaque enregistrement aura des ordures aux offsets 510 et 1022. Les $DATA résidentes qui traversent ces offsets seront corrompues. Les parsers comme MFTECmd, omerbenamram/mft, analyzeMFT et fls/istat du Sleuth Kit appliquent les fixups en première étape. Si vous écrivez votre propre parser (ce qui est un bon exercice ; voir parse-mft en Python), faites cela avant tout le reste.
Le flux d'attributs
Après l'en-tête et le tableau fixup, chaque enregistrement contient une série d'attributs typés empilés bout à bout, alignés sur 8 octets, terminés par la valeur sentinelle 0xFFFFFFFF là où viendrait le code de type de l'attribut suivant.
Chaque attribut commence par un petit en-tête standardisé :
0x00 4 octets Code de type d'attribut (0x10 = $STANDARD_INFORMATION, etc.)
0x04 4 octets Longueur de cet attribut (en-tête + données)
0x08 1 octet Drapeau non résident (0 = résident, 1 = non résident)
0x09 1 octet Longueur du nom (en caractères ; 0 si sans nom)
0x0A 2 octets Offset vers le nom (en caractères depuis le début de l'attribut)
0x0C 2 octets Drapeaux (compressé/chiffré/sparse)
0x0E 2 octets ID d'attribut
Pour les attributs résidents, les champs suivants sont longueur et offset du contenu ; pour les non résidents, ce sont VCN début/fin, offset de runlist, tailles compressée/allouée/réelle. Après tout cela, les données réelles. Les noms (s'ils existent) sont en Unicode et non alignés ; attendez-vous à ce que l'implémentation soit délicate.
Un enregistrement minimal porte trois attributs :
$STANDARD_INFORMATION(0x10) : horodatages, drapeaux DOS, ID de sécurité.$FILE_NAME(0x30) : nom, référence parent, second jeu d'horodatages, tailles allouée/réelle au moment où le nom a été positionné. Les enregistrements peuvent en porter plusieurs (un par hard link plus le nom court 8.3 sur les volumes où la génération 8.3 est activée).$DATA(0x80) : contenu du fichier, résident s'il rentre, runlist sinon. Les enregistrements peuvent porter plusieurs attributs$DATA; celui sans nom est le flux primaire, les nommés sont des alternate data streams.
Pour le catalogue d'attributs complet et où vit chacun, voir la référence de la Master File Table.
Pourquoi cela compte quand les enregistrements sont endommagés
La combinaison d'une disposition stable de 1 024 octets, de la signature FILE, du mécanisme fixup et des en-têtes d'attribut auto-descriptifs est ce qui rend possible le carving d'enregistrements NTFS supprimés. Même quand $MFT elle-même a disparu (corruption, gribouillis ransomware, effacement partiel), un scan de signature du volume brut pour les frontières 46 49 4C 45 récupère les enregistrements tant que les clusters sous-jacents n'ont pas été écrasés. La vérification fixup vous donne un contrôle d'intégrité par secteur ; les enregistrements qui échouent doivent être marqués mais leurs données d'attribut sont parfois encore partiellement lisibles.
C'est la fondation sous des outils comme mmls+fls du Sleuth Kit, le « deep scan » de R-Studio et les diverses suites commerciales de récupération. Ils parcourent tous la même disposition d'octets. La connaître soi-même est ce qui vous permet de faire un contrôle de cohérence de leur sortie.
En lire un à la main
Ouvrez une $MFT extraite dans 010 Editor avec le template NTFS MFT Record appliqué. Choisissez l'enregistrement 5 (le répertoire racine ; offset 5 * 1024 = 5120 depuis le début du fichier). Confirmez :
- Octets 0x00 à 0x03 :
46 49 4C 45. - Drapeaux à 0x16 :
0x03(IN_USE et DIRECTORY tous deux positionnés). - Premier type d'attribut à l'offset donné par 0x14 :
0x10($STANDARD_INFORMATION).
Si ça concorde, l'enregistrement a été parsé proprement. Sinon, soit le fichier est corrompu, soit le tableau fixup n'a pas été appliqué (le template de 010 Editor l'applique pour vous).
Une fois que vous avez fait cela une fois, le reste de la MFT devient lisible. Les champs cessent d'être arcaniques et deviennent un enregistrement que vous pouvez lire sans aide.
Lectures complémentaires
- Microsoft, Master File Table. La référence officielle, concise.
- La documentation NTFS du Sleuth Kit. Les notes de Brian Carrier restent la source unique la plus claire pour la disposition d'enregistrement.
- Russon et Fledel, NTFS Documentation (projet linux-ntfs). Offsets de champs pour chaque attribut, tirés directement d'années de reverse engineering.