Cada registro MFT mide 1.024 bytes. Cada uno está dispuesto de la misma manera. Si puedes leer uno en un editor hexadecimal, puedes leerlos todos, y puedes escribir un parser que supere a las quebradizas herramientas comerciales recuperando registros dañados. La disposición es lo bastante pequeña como para que un par de horas y 010 Editor te hagan fluido.
Este es el recorrido a nivel de byte que doy a los analistas que quieren dejar de tratar los parsers MFT como cajas negras.
La firma FILE
46 49 4C 45 ("FILE")
Primeros cuatro bytes. ASCII FILE. Esta es la magia que te permite tallar registros MFT a partir de clusters crudos cuando la propia $MFT no se puede leer. Recorre una imagen de disco en límites de 512 bytes (NTFS antiguo) o de 4096 bytes (Win10 con registros 4K, raro pero existe), busca el patrón 46 49 4C 45, y normalmente puedes recuperar la mayor parte de una MFT aunque la propia cabecera de la tabla esté destruida.
La otra firma que verás en libertad es BAAD (42 41 41 44). NTFS la escribe cuando chkdsk decidió que un registro era irreparable. El slot se mantiene, el número de secuencia se preserva, pero no se debe confiar en el resto de los bytes. Trata los registros BAAD como evidencia de que chkdsk corrió y como objetivos para el análisis del contexto circundante. No intentes parsear sus atributos ingenuamente.
La cabecera del registro (offsets 0x00 a 0x37, más o menos)
Tras los cuatro bytes de firma, NTFS coloca una cabecera que varía ligeramente según la versión de NTFS. Los campos, con offsets desde el inicio del registro, que realmente usas:
0x00 4 bytes Firma ("FILE" o "BAAD")
0x04 2 bytes Offset al update sequence array (USA)
0x06 2 bytes Tamaño USA en palabras de 16 bits (records=count of (USN+fixup_entries))
0x08 8 bytes Número de secuencia de $LogFile (LSN)
0x10 2 bytes Número de secuencia
0x12 2 bytes Recuento de hard links
0x14 2 bytes Offset al primer atributo
0x16 2 bytes Banderas (bit 0 = IN_USE, bit 1 = DIRECTORY,
bit 2 = QUOTA_CHARGED, bit 3 = HAS_VIEW_INDEX)
0x18 4 bytes Tamaño usado del registro
0x1C 4 bytes Tamaño asignado (siempre 1024 en volúmenes estándar)
0x20 8 bytes Referencia al registro de archivo base (no cero en registros de extensión)
0x28 2 bytes ID del siguiente atributo
0x2A 2 bytes (padding/alineamiento en NTFS 3.0)
0x2C 4 bytes Número de registro (NTFS 3.1+; autorreferencia)
Algunos de ellos merecen atención especial.
Banderas en 0x16. Bit 0 limpio significa eliminado. Bit 1 puesto significa directorio (el registro contiene atributos de índice en vez de $DATA). La combinación de ambas banderas siendo significativas es lo que hace que un byte te diga mucho sobre una entrada.
Número de secuencia en 0x10. Se incrementa cada vez que el slot se reutiliza. La referencia de archivo de 64 bits (número de registro en los 48 bits bajos, número de secuencia en los 16 altos) es el identificador único real para la existencia de un archivo concreto. Las referencias dentro de otros atributos (referencia padre en $FILE_NAME, entradas de $ATTRIBUTE_LIST) usan esta forma de 64 bits. Una referencia cuya secuencia no coincida con el registro actual apunta a un inquilino anterior; normalmente un archivo eliminado. Así es como el Sleuth Kit recorre cadenas de directorios eliminadas.
Referencia al registro de archivo base en 0x20. Cero en un registro base; no cero en un registro de extensión (cuando los atributos de un archivo desbordan un único slot). El valor no cero es la referencia de 64 bits del registro base al que pertenece esta extensión. Los parsers deben seguir cadenas de $ATTRIBUTE_LIST para ensamblar el archivo completo.
Número de secuencia de $LogFile en 0x08. Apunta a $LogFile. Útil para recuperación a nivel transaccional; menos útil para análisis rutinario. Vale la pena saber que existe.
El array fixup (update sequence)
NTFS protege contra escrituras desgarradas con un pequeño truco. Cada registro de 1.024 bytes se divide en dos sectores de 512 bytes. Antes de escribir, NTFS:
- Elige un número de secuencia de actualización de 16 bits (USN, no relacionado con la USN de
$UsnJrnla pesar del acrónimo compartido). - Guarda los dos últimos bytes originales de cada sector en un array que vive justo después de la cabecera.
- Sustituye los dos últimos bytes de cada sector con la propia USN.
Al leer, NTFS verifica que los dos últimos bytes de cada sector de 512 bytes son iguales a la USN elegida. Si lo son, la escritura fue atómica; saca los bytes originales del array de fixup y vuélvelos a colocar. Si la cola de algún sector no coincide, la escritura fue desgarrada y el registro es sospechoso.
El array se dispone como una palabra USN seguida de N palabras de fixup, donde N es el número de sectores. Para un registro de 1.024 bytes en un volumen con sectores de 512 bytes, N = 2. El array ocupa por tanto 6 bytes en total (USN, fixup_for_sector_0, fixup_for_sector_1). Su offset es el valor de 2 bytes en 0x04 del registro (típicamente 0x2A o 0x30 según la versión de NTFS).
La consecuencia práctica: si lees trozos crudos de 1.024 bytes de $MFT sin aplicar los fixups, cada registro tendrá basura en los offsets 510 y 1022. Los datos residentes $DATA que crucen esos offsets se corromperán. Los parsers como MFTECmd, omerbenamram/mft, analyzeMFT y fls/istat del Sleuth Kit aplican los fixups como primer paso. Si escribes tu propio parser (lo cual es un buen ejercicio; mira parse-mft en Python), haz esto antes que cualquier otra cosa.
El flujo de atributos
Tras la cabecera y el array de fixup, cada registro contiene una serie de atributos tipados empacados consecutivamente, alineados a 8 bytes, terminados por el valor centinela 0xFFFFFFFF donde iría el código de tipo del siguiente atributo.
Cada atributo empieza con una pequeña cabecera estandarizada:
0x00 4 bytes Código de tipo de atributo (0x10 = $STANDARD_INFORMATION, etc.)
0x04 4 bytes Longitud de este atributo (cabecera + datos)
0x08 1 byte Bandera no-residente (0 = residente, 1 = no residente)
0x09 1 byte Longitud del nombre (en caracteres; 0 si no tiene nombre)
0x0A 2 bytes Offset al nombre (en caracteres desde el inicio del atributo)
0x0C 2 bytes Banderas (comprimido/cifrado/sparse)
0x0E 2 bytes ID de atributo
Para atributos residentes, los siguientes campos son longitud y offset del contenido; para no residentes son VCN inicial/final, offset de runlist, tamaños comprimido/asignado/real. Después de todo eso, los datos reales. Los nombres (si los hay) son Unicode y no alineados; espera que la implementación sea pegajosa.
Un registro mínimo lleva tres atributos:
$STANDARD_INFORMATION(0x10): marcas de tiempo, banderas DOS, ID de seguridad.$FILE_NAME(0x30): nombre, referencia padre, segundo conjunto de marcas de tiempo, tamaños asignado/real del momento en que el nombre se fijó. Los registros pueden llevar muchos de estos (uno por hard link más el nombre corto 8.3 en volúmenes donde la generación 8.3 está habilitada).$DATA(0x80): contenido del archivo, residente si cabe, runlist en caso contrario. Los registros pueden llevar múltiples atributos$DATA; el sin nombre es el flujo primario, los nombrados son alternate data streams.
Para el catálogo completo de atributos y dónde vive cada uno, mira la referencia de la Master File Table.
Por qué esto importa cuando los registros están dañados
La combinación de un layout estable de 1.024 bytes, la firma FILE, el mecanismo de fixup y las cabeceras de atributo autodescriptivas es lo que hace posible el carving de registros NTFS eliminados. Aun cuando la propia $MFT ha desaparecido (corrupción, garabatos de ransomware, borrado parcial), un escaneo de firma del volumen crudo en busca de límites 46 49 4C 45 recupera los registros mientras los clusters subyacentes no se hayan sobrescrito. La verificación de fixup te da una comprobación de integridad por sector; los registros que fallen deben ser marcados pero a veces sus datos de atributos siguen siendo parcialmente legibles.
Esta es la base bajo herramientas como mmls+fls del Sleuth Kit, el "deep scan" de R-Studio y las distintas suites comerciales de recuperación. Todas recorren el mismo layout de bytes. Conocerlo tú mismo es lo que te permite hacer un sanity-check de su salida.
Leer uno a mano
Abre una $MFT extraída en 010 Editor con la plantilla NTFS MFT Record aplicada. Elige el registro 5 (el directorio raíz; offset 5 * 1024 = 5120 desde el inicio del archivo). Confirma:
- Bytes 0x00 a 0x03:
46 49 4C 45. - Banderas en 0x16:
0x03(IN_USE y DIRECTORY ambos puestos). - Primer tipo de atributo en el offset dado por 0x14:
0x10($STANDARD_INFORMATION).
Si esos cuadran, el registro se parseó limpiamente. Si no, o el archivo está corrupto o no se aplicó el array de fixup (la plantilla de 010 Editor sí lo aplica por ti).
Una vez que hayas hecho esto una vez, el resto de la MFT se vuelve legible. Los campos dejan de ser arcanos y empiezan a ser un registro que puedes leer sin ayuda.
Lecturas adicionales
- Microsoft, Master File Table. La referencia oficial, escueta.
- Documentación NTFS del Sleuth Kit. Las notas de Brian Carrier siguen siendo la fuente única más clara para el layout de registros.
- Russon y Fledel, NTFS Documentation (proyecto linux-ntfs). Offsets de campo para cada atributo, sacados directamente de años de ingeniería inversa.