1. La MFT y los archivos de metadatos
En ext4 relacionamos nombres, inodos y extents. NTFS organiza su información alrededor de la Master File Table, MFT: una secuencia de registros que describen archivos y sus atributos. La MFT es, a su vez, un archivo del volumen.
Registro MFT: segmento de información de un archivo, con encabezado y atributos. Atributo NTFS: elemento tipado, opcionalmente nombrado, cuyo valor puede estar dentro del registro o en almacenamiento descrito por él.
| Archivo de metadatos | Función que estudiaremos |
|---|---|
| $MFT | Almacena los registros de archivos, incluidos los de metadatos. |
| $MFTMirr | Replica una parte inicial crítica de la MFT; no toda ella. |
| $Bitmap | Registra la asignación de clústeres del volumen. |
| $LogFile | Participa en la recuperación de operaciones de metadatos. |
| $Secure | Organiza descriptores de seguridad. |
| $Extend | Directorio para metadatos adicionales, como el diario USN si existe. |
Un archivo puede necesitar registros adicionales. No se deduce una relación de «un archivo, exactamente un registro». La documentación de la MFT describe archivos, atributos y registros reservados; el registro 0 corresponde a la propia $MFT.
2. Del sector inicial al primer registro
El sector inicial del volumen contiene parámetros como bytes por sector, sectores por clúster y ubicación inicial de la MFT. Los enteros se decodifican explícitamente como little-endian, sin hacer casts a estructuras nativas de C.
0x03: identificador de ocho bytes "NTFS "
0x0B: bytes por sector 2 bytes
0x0D: sectores por clúster 1 byte
0x28: sectores declarados 8 bytes
0x30: LCN inicial de $MFT 8 bytes
0x38: LCN inicial de $MFTMirr 8 bytes
0x40: codificación del tamaño FILE 1 byte con signo
ClusterBytes = BytesPerSector × SectorsPerCluster
OffsetInicialMFT = MftLCN × ClusterBytesSi la codificación del tamaño de registro es positiva, expresa un número de clústeres; si es negativa, expresa una potencia de dos: -10 corresponde a 2^10 = 1024 bytes. No hay que fijar siempre el registro en 1024 ni el clúster en 4096.
El programa se limita a sectores de 512 bytes, clústeres de hasta 64 KiB y registros que caben en un clúster. Acepta una imagen de volumen que empieza en cero; no localiza particiones MBR/GPT. Los campos y codificaciones pueden contrastarse con layout.h de NTFS-3G.
El LCN inicial permite comenzar por el primer registro, pero no ubicar todos los demás con una suma física simple: $MFT también puede fragmentarse. Para localizar un registro arbitrario habría que seguir el mapa de su flujo de datos y, cuando corresponda, extensiones de atributos.
3. Registros FILE y protección multisector
Un registro de archivo comienza con la firma FILE, seguida de información sobre su protección multisector, secuencia, estado y ubicación de los atributos. Una firma reconocible no demuestra que todo el registro sea coherente.
La Update Sequence Array, USA, conserva palabras originales y un marcador. Al leer se comprueban los finales protegidos y se restauran los valores antes de interpretar atributos que podrían cruzarlos. Una discrepancia detecta una transferencia incoherente bajo este mecanismo; no autoriza a inventar los bytes faltantes.
Esto no es un checksum criptográfico del contenido. Tampoco debe confundirse la secuencia de protección con el USN del diario de cambios. Véanse FILE_RECORD_SEGMENT_HEADER y MULTI_SECTOR_HEADER. El inspector verifica todos los finales antes de restaurar una copia, sin escribir la imagen.
4. Tipos, nombres y formas de los atributos
| Tipo habitual | Código | Contenido o responsabilidad |
|---|---|---|
| $STANDARD_INFORMATION | 0x10 | Información básica del archivo. |
| $ATTRIBUTE_LIST | 0x20 | Referencias a atributos que pueden residir en otros registros. |
| $FILE_NAME | 0x30 | Nombre y relación con el directorio padre. |
| $DATA | 0x80 | Flujo de datos, sin nombre o nombrado. |
| $INDEX_ROOT / $INDEX_ALLOCATION | 0x90 / 0xA0 | Índices y su almacenamiento. |
| $BITMAP | 0xB0 | Asignación dentro del contexto que lo contiene. |
Tipo, nombre y forma son ejes diferentes: un $DATA puede tener nombre, y su valor puede ser residente o no residente. $Bitmap, el archivo del volumen, no es lo mismo que cualquier atributo de tipo $BITMAP.
El encabezado incluye longitud total y forma. Un lector debe validar cada longitud antes de avanzar y reconocer el terminador de atributos; buscar el byte 0x80 por toda la imagen no es una forma válida de localizar datos.
La representación de ambas formas y de los mapping pairs se documenta en ATTRIBUTE_RECORD_HEADER. Nuestro ejemplo enumera atributos del registro inicial, pero no resuelve $ATTRIBUTE_LIST ni reconstruye todos los de un archivo.
5. Qué significa que un atributo sea residente
En un atributo residente, el valor se almacena dentro del propio registro MFT. Para ciertos flujos pequeños, sus bytes no requieren clústeres externos exclusivos. Eso no los hace gratuitos: ocupan espacio en la MFT, que también utiliza almacenamiento.
En un atributo no residente, el encabezado conserva tamaños y referencias a rangos externos. El número de bytes que cabe como residente depende del espacio restante, otros atributos, nombres y tamaño de registro. No existe un umbral universal aplicable a cualquier archivo.
Residente:
registro FILE → encabezado $DATA → bytes del contenido
No residente:
registro FILE → encabezado $DATA → mapping pairs
↓
rangos de clústeres externosUn archivo no se reduce a su flujo principal: este podría ser residente y otro flujo ser no residente. Decir «archivo residente» suele abreviar la forma de su $DATA sin nombre, pero es más preciso indicar qué atributo se está observando.
6. VCN, LCN y data runs
VCN, Virtual Cluster Number, numera clústeres dentro de un flujo; LCN, Logical Cluster Number, los ubica dentro del volumen. Aunque LCN contiene la palabra logical, en este contexto identifica la ubicación del volumen frente a la posición del flujo.
VCN [0,3) → LCN [100,103) longitud 3
VCN [3,5) → LCN [90,92) longitud 2, diferencia de LCN = -10
VCN [5,9) → hueco longitud 4, sin campo LCN
En el segundo run: VCN 4 → LCN 91.
Un delta negativo no significa tamaño negativo ni recorrido lógico hacia atrás.Los mapping pairs codifican la longitud del run y una diferencia con signo respecto del LCN anterior. El primer byte indica cuántos bytes ocupa cada campo. Un ancho de desplazamiento cero representa un run disperso; no se suma entonces un delta ni se pierde la base anterior.
Al analizar un segmento de atributo se parte de su LowestVcn. El decodificador del tema empieza en VCN cero y LCN acumulado cero; es una demostración de un segmento, no un lector de todos los atributos de un volumen. Debe rechazar campos truncados, longitudes nulas y acumulaciones fuera del rango admitido.
7. Tamaño, inicialización, huecos y compresión
Un flujo tiene longitud lógica, longitud válida inicializada y asignación. No se interpreta la longitud asignada como cantidad de bytes que debe devolver una lectura. La región lógica posterior a la longitud válida se entrega como ceros, no como datos antiguos del almacenamiento.
Los huecos de un flujo sparse tampoco se reconstruyen leyendo clústeres inexistentes. En compresión, la relación entre runs y bytes lógicos requiere interpretar unidades de compresión; concatenar rangos físicos sin más no reconstruye el contenido.
EFS, compresión y otras representaciones necesitan soporte específico y, cuando corresponde, claves. Un extractor de bytes crudos no puede tratarlas como un archivo ordinario no comprimido. El lector didáctico solo enumera tamaños y formas: no extrae esos flujos.
La semántica de tamaños y estados por flujo se describe en File streams. La enumeración de atributos no certifica que todos sus datos externos sean accesibles o íntegros.
8. Nombres, referencias, permisos y reparse points
Un directorio utiliza índices para relacionar nombres y referencias. Las referencias de archivo incluyen información de secuencia para ayudar a distinguir reutilizaciones de registros. Un número de registro aislado no debe tratarse como una identidad eterna.
Hard links proporcionan nombres distintos del mismo archivo; sus flujos pertenecen al objeto compartido. Los descriptores de seguridad organizan permisos con un modelo que no se reduce a los bits rwx de Unix. La presentación desde Linux depende del controlador y sus opciones.
Un reparse point contiene una etiqueta y datos que pueden requerir un componente que interprete la operación. No todo reparse point es un enlace simbólico: también se utiliza para otras funcionalidades. Véase Reparse points.
Los límites y características soportados dependen del sistema operativo y sus versiones. La descripción general de NTFS presenta ese contexto; no deducimos compatibilidad completa de la sola existencia de una partición NTFS.
9. $LogFile y $UsnJrnl resuelven problemas diferentes
| Mecanismo | Objetivo | Qué no debe suponerse |
|---|---|---|
| $LogFile | Apoyar la recuperación de operaciones de metadatos. | No es una copia histórica de todos los bytes de todos los archivos. |
| $Extend/$UsnJrnl, cuando existe | Describir cambios para que aplicaciones puedan detectar actividad. | No contiene necesariamente los datos anteriores ni registra para siempre cada detalle. |
| USA de un registro | Comprobar y restaurar palabras protegidas al leer una estructura multisector. | No es ninguno de los dos diarios. |
El diario USN ayuda, por ejemplo, a aplicaciones que necesitan identificar cambios sin recorrer siempre todo el volumen. Sus registros describen cambios y pueden agrupar razones; las aplicaciones deben manejar pérdida de continuidad y retención limitada. Véanse Change journals y Change journal records.
La consistencia de metadatos no convierte una actualización de varios archivos en una transacción de negocio ni elimina la necesidad de copias externas. Los protocolos de confirmación deben implementarse con los contratos del proveedor usado: las llamadas y garantías del ejemplo Linux del tema 35 no se trasladan mecánicamente a todas las API de Windows.
10. Flujos alternativos de datos
El flujo principal suele ser el atributo $DATA sin nombre. Un archivo puede tener otros $DATA nombrados: Alternate Data Streams, ADS. Son flujos asociados al mismo archivo, no necesariamente archivos hijos visibles en un listado ordinario.
informe.txt flujo principal sin nombre
informe.txt:notas flujo llamado notas
informe.txt:notas:$DATA forma que explicita el tipo
Mismo archivo:
$DATA sin nombre → contenido principal
$DATA "notas" → contenido alternativo independienteLeer el flujo principal no lee automáticamente los alternativos. Su tamaño tampoco expresa la suma de todos ellos. Una copia, archivo comprimido, transferencia o sistema de destino puede conservarlos u omitirlos según la herramienta y las opciones: debe verificarse el procedimiento concreto.
Un ADS no es por sí mismo una marca de ocultación maliciosa ni una frontera de seguridad. Forma parte de la representación del archivo y debe considerarse cuando la tarea exige conservar todo su contenido. El laboratorio creará un flujo notas de texto propio y lo extraerá por separado.
11. Qué observan las herramientas y el proveedor
El laboratorio Linux usa utilidades NTFS-3G sobre una imagen propia desmontada. ntfsinfo muestra atributos; ntfscp incorpora contenido y puede seleccionar un nombre de atributo; ntfscat extrae un archivo o flujo. No se añade una opción de fuerza para ignorar un volumen montado.
Estas herramientas offline no son el controlador ntfs3 del kernel ni el proveedor nativo de Windows. La documentación de NTFS3 describe su propio soporte y opciones. La forma de exponer flujos, permisos y otras características debe consultarse para cada proveedor.
Trabajaremos exclusivamente con archivos de imagen nuevos, sin particiones de un Windows activo, hibernado o con inicio rápido pendiente. No hace falta montar ni reparar nada para observar los atributos de la práctica.
12. Programa en C: inspeccionar el registro inicial de la MFT
Guarda inspeccionar-ntfs.c. Es una herramienta de lectura para el subconjunto declarado de NTFS: obtiene la ubicación inicial de $MFT, verifica el registro 0, restaura la USA en memoria y enumera los atributos de ese registro. No busca un nombre arbitrario ni sigue extensiones de $ATTRIBUTE_LIST.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
static int imagen;
static uint64_t limite;
static void error(const char *s) { fprintf(stderr, "%s\n", s); exit(1); }
static uint16_t u16(const unsigned char *p)
{
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}
static uint32_t u32(const unsigned char *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static uint64_t u64(const unsigned char *p)
{
uint64_t r = 0;
for (unsigned i = 0; i < 8; i++) r |= (uint64_t)p[i] << (i * 8);
return r;
}
static void leer(uint64_t offset, void *buffer, size_t n)
{
if (offset > limite || n > limite - offset) error("Lectura fuera del volumen.");
unsigned char *p = buffer;
while (n) {
ssize_t r = pread(imagen, p, n, (off_t)offset);
if (r > 0) { p += r; n -= (size_t)r; offset += (uint64_t)r; }
else if (r == -1 && errno == EINTR) continue;
else { if (r == -1) perror("pread"); error("Lectura incompleta."); }
}
}
static void atributos(unsigned char *r, size_t bytes)
{
if (memcmp(r, "FILE", 4)) error("El registro inicial no tiene firma FILE.");
size_t usa = u16(r + 4), palabras = u16(r + 6);
size_t primero = u16(r + 20), usados = u32(r + 24);
if (palabras != bytes / 512 + 1 || usa < 48 || usa >= 510 ||
palabras * 2 > 510 - usa || primero < usa + palabras * 2 ||
primero % 8 || usados > bytes || usados < 4 || primero > usados - 4 ||
u32(r + 28) != bytes || !(u16(r + 22) & 1) || u64(r + 32) != 0)
error("Encabezado FILE fuera del subconjunto admitido.");
for (size_t i = 1; i < palabras; i++)
if (u16(r + i * 512 - 2) != u16(r + usa))
error("USA: final de sector no coincide con el marcador.");
for (size_t i = 1; i < palabras; i++) {
r[i * 512 - 2] = r[usa + i * 2];
r[i * 512 - 1] = r[usa + i * 2 + 1];
}
puts("USA=verificada_y_restaurada_en_memoria");
size_t pos = primero;
while (pos <= usados - 4) {
uint32_t tipo = u32(r + pos);
if (tipo == UINT32_MAX) { puts("END=atributos"); return; }
if (usados - pos < 16) error("Encabezado de atributo truncado.");
const unsigned char *a = r + pos;
uint32_t largo = u32(a + 4);
unsigned forma = a[8], nombre = a[9];
size_t minimo = forma == 0 ? 24 : 64;
if (forma > 1 || largo < minimo || largo % 8 || largo > usados - pos)
error("Longitud o forma de atributo invalida.");
size_t nom_off = u16(a + 10);
if (nombre && (nom_off < minimo || nom_off > largo ||
(size_t)nombre * 2 > largo - nom_off))
error("Nombre de atributo fuera de sus limites.");
printf("ATTR=%08" PRIx32 " FORM=%s NAME_UNITS=%u LENGTH=%" PRIu32,
tipo, forma ? "nonresident" : "resident", nombre, largo);
if (forma == 0) {
uint32_t valor = u32(a + 16);
size_t off = u16(a + 20);
if (off < 24 || off > largo || valor > largo - off)
error("Valor residente fuera de sus limites.");
printf(" VALUE_BYTES=%" PRIu32, valor);
} else {
size_t pares = u16(a + 32);
if (pares < 64 || pares >= largo) error("Mapping pairs fuera de sus limites.");
printf(" LOWEST_VCN=%" PRIu64 " SIZE=%" PRIu64 " INITIALIZED=%" PRIu64,
u64(a + 16), u64(a + 48), u64(a + 56));
}
putchar('\n');
pos += largo;
}
error("No se encontro terminador de atributos.");
}
int main(int argc, char **argv)
{
if (argc != 2) { fprintf(stderr, "Uso: %s volumen.img\n", argv[0]); return 2; }
imagen = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
if (imagen == -1) { perror("open"); return 1; }
struct stat st;
if (fstat(imagen, &st) == -1) { perror("fstat"); return 1; }
if (!S_ISREG(st.st_mode) || st.st_size < 512) error("Se requiere una imagen regular.");
limite = (uint64_t)st.st_size;
unsigned char b[512]; leer(0, b, sizeof b);
if (memcmp(b + 3, "NTFS ", 8) || b[510] != 0x55 || b[511] != 0xaa)
error("Identificador o firma NTFS incorrectos.");
uint32_t sector = u16(b + 11), spc = b[13];
if (sector != 512 || spc == 0 || spc > 128 || (spc & (spc - 1)))
error("Geometria fuera del subconjunto de sectores de 512 bytes.");
uint64_t cluster = (uint64_t)sector * spc;
uint64_t sectores = u64(b + 40), mft = u64(b + 48);
if (sectores == 0 || sectores > limite / sector)
error("Volumen fuera de la imagen o truncado.");
limite = sectores * sector;
int codigo = b[64] < 128 ? b[64] : (int)b[64] - 256;
uint64_t registro;
if (codigo > 0) registro = (uint64_t)codigo * cluster;
else if (codigo < 0 && codigo >= -16) registro = UINT64_C(1) << -codigo;
else error("Codificacion de registro no admitida.");
if (registro < 512 || registro > 65536 || registro % 512 || registro > cluster ||
mft >= limite / cluster) error("Registro o posicion MFT no admitidos.");
uint64_t offset = mft * cluster;
if (registro > limite - offset) error("Registro inicial fuera del volumen.");
unsigned char *r = malloc((size_t)registro);
if (!r) { perror("malloc"); return 1; }
leer(offset, r, (size_t)registro);
printf("SECTOR_BYTES=%u\nCLUSTER_BYTES=%" PRIu64 "\nRECORD_BYTES=%" PRIu64
"\nMFT_LCN=%" PRIu64 "\nMFT_OFFSET=%" PRIu64 "\n",
(unsigned)sector, cluster, registro, mft, offset);
atributos(r, (size_t)registro);
free(r);
if (close(imagen) == -1) { perror("close"); return 1; }
return 0;
}Los errores terminan el proceso y el sistema libera los recursos pendientes. No hay aperturas de escritura. La herramienta comprueba límites estructurales del subconjunto, sin validar todos los valores, índices, referencias ni mapas externos. Mantén la imagen estable: un descriptor de lectura no impide que otro proceso la cambie.
13. Programa en C: decodificar data runs
Guarda runs-ntfs.c. Recibe hasta 256 bytes expresados en hexadecimal sin espacios. La demostración usa un delta negativo y un hueco. El programa se limita a LCN no negativos de hasta INT64_MAX; no recibe una geometría de volumen y, por ello, no valida sus límites físicos.
#include <stdint.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
static int hex(char c)
{
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
static uint64_t numero(const unsigned char *p, unsigned n)
{
uint64_t v = 0;
for (unsigned i = 0; i < n; i++) v |= (uint64_t)p[i] << (i * 8);
return v;
}
static int decodificar(const unsigned char *b, size_t bytes)
{
size_t pos = 0;
uint64_t vcn = 0, lcn = 0;
while (pos < bytes) {
unsigned h = b[pos++];
if (!h) { printf("END_VCN=%" PRIu64 "\n", vcn); return 0; }
unsigned n = h & 15, d = h >> 4;
if (n == 0 || n > 8 || d > 8 || n + d > bytes - pos) return 1;
uint64_t largo = numero(b + pos, n); pos += n;
if (largo == 0 || largo > INT64_MAX || vcn > UINT64_MAX - largo) return 1;
if (d) {
uint64_t raw = numero(b + pos, d);
if (b[pos + d - 1] & 0x80) {
uint64_t mascara = d == 8 ? UINT64_MAX : (UINT64_C(1) << (d * 8)) - 1;
uint64_t magnitud = ((~raw) & mascara) + 1;
if (magnitud > lcn) return 1;
lcn -= magnitud;
} else {
if (raw > INT64_MAX || lcn > (uint64_t)INT64_MAX - raw) return 1;
lcn += raw;
}
pos += d;
if (largo - 1 > (uint64_t)INT64_MAX - lcn) return 1;
printf("VCN=%" PRIu64 " LENGTH=%" PRIu64 " LCN=%" PRIu64 "\n", vcn, largo, lcn);
} else printf("VCN=%" PRIu64 " LENGTH=%" PRIu64 " HOLE\n", vcn, largo);
vcn += largo;
}
return 1;
}
int main(int argc, char **argv)
{
if (argc != 2) { fprintf(stderr, "Uso: %s hexadecimal-sin-espacios\n", argv[0]); return 2; }
size_t n = strlen(argv[1]);
if (n == 0 || n > 512 || n % 2) { fprintf(stderr, "Longitud hexadecimal invalida.\n"); return 2; }
unsigned char b[256];
for (size_t i = 0; i < n; i += 2) {
int a = hex(argv[1][i]), c = hex(argv[1][i + 1]);
if (a < 0 || c < 0) { fprintf(stderr, "Digito hexadecimal invalido.\n"); return 2; }
b[i / 2] = (unsigned char)(a * 16 + c);
}
int estado = decodificar(b, n / 2);
if (estado) fprintf(stderr, "Mapping pairs truncados, fuera de rango o sin terminador.\n");
return estado;
}$ ./runs-ntfs 1103641102f6010400
VCN=0 LENGTH=3 LCN=100
VCN=3 LENGTH=2 LCN=90
VCN=5 LENGTH=4 HOLE
END_VCN=9Se detiene en el primer terminador cero; los bytes posteriores se consideran fuera de la secuencia. No descomprime, no descifra y no interpreta estos runs como evidencia de que el contenido está inicializado. Esas decisiones necesitan los demás campos del atributo.
14. Laboratorio sobre una imagen NTFS propia
Usa Linux con compilador C, Python 3 y utilidades NTFS-3G: mkntfs, ntfsinfo, ntfscp y ntfscat. No hace falta montar ni ejecutar como administrador. Mantén una misma sesión Bash y detén la secuencia ante cualquier fallo.
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 inspeccionar-ntfs.c -o inspeccionar-ntfs
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 runs-ntfs.c -o runs-ntfs
$ BIN38="$PWD"
$ LAB38=$(mktemp -d "$HOME/ntfs38.XXXXXX")
$ cd -- "$LAB38"
$ printf 'contenido principal\n' > pequeno.txt
$ printf 'anotacion del curso\n' > notas.txt
$ python3 - <<'PY'
from pathlib import Path
with Path('grande.bin').open('xb') as f:
f.write(bytes(range(256)) * 8192)
PY
$ truncate -s 128M volumen.img
$ mkntfs -F -Q -s 512 -c 4096 -L CURSO38 volumen.imgvolumen.img es nuevo porque el directorio es exclusivo. -F permite al formateador trabajar con ese archivo regular; no se usa sobre un disco físico. -Q selecciona formato rápido. El volumen de práctica no pretende ser arrancable y puede mostrar avisos sobre parámetros de arranque que no necesitamos. Véase mkntfs(8).
Incorporar datos y crear un flujo nombrado
$ ntfscp volumen.img pequeno.txt /pequeno.txt
$ ntfscp volumen.img grande.bin /grande.bin
$ ntfscp -N notas volumen.img notas.txt /pequeno.txt
$ ntfsinfo -m volumen.img
$ ntfsinfo -i 0 volumen.img
$ ntfsinfo -F /pequeno.txt volumen.img
$ ntfsinfo -F /grande.bin volumen.img
$ "$BIN38/inspeccionar-ntfs" volumen.img
$ "$BIN38/runs-ntfs" 1103641102f6010400Busca los $DATA del archivo pequeño: uno sin nombre y otro llamado notas. El flujo grande debe requerir almacenamiento no residente; el pequeño suele caber como residente en este ensayo, pero la forma concreta se observa en ntfsinfo. No fijes los números de registro de esos archivos.
El inspector C trabaja sobre el registro 0 de $MFT, por lo que sus atributos no son los de pequeno.txt. ntfsinfo resuelve ese nombre mediante estructuras que el ejemplo C no implementa. Las opciones de copia y selección de atributo se describen en ntfscp(8).
Extraer cada flujo por separado
$ ntfscat volumen.img /pequeno.txt > principal-extraido.txt
$ ntfscat -n notas volumen.img /pequeno.txt > notas-extraidas.txt
$ ntfscat volumen.img /grande.bin > grande-extraido.bin
$ cmp -- pequeno.txt principal-extraido.txt
$ cmp -- notas.txt notas-extraidas.txt
$ cmp -- grande.bin grande-extraido.binLas redirecciones crean archivos del anfitrión, no ADS en él. Comprueba el estado de ntfscat antes de interpretar las salidas: un archivo de salida creado no prueba que la extracción haya terminado bien. La interfaz se documenta en ntfscat(8).
15. Verificación y práctica de flujos en Windows
El verificador confirma campos, contenido de ambos flujos y rechazo de una copia cuyo primer final de sector protegido se alteró. No modifica la imagen original ni repara la copia. Ejecuta desde LAB38 después de la práctica.
$ python3 - "$BIN38/inspeccionar-ntfs" <<'PY'
from pathlib import Path
import hashlib
import re
import shutil
import subprocess
import sys
programa = str(Path(sys.argv[1]).resolve())
imagen = Path('volumen.img')
def huella(ruta):
h = hashlib.sha256()
with ruta.open('rb') as f:
for bloque in iter(lambda: f.read(1024 * 1024), b''):
h.update(bloque)
return h.digest()
antes = huella(imagen)
r = subprocess.run([programa, str(imagen)], capture_output=True, text=True, check=True)
campos = dict(re.findall(r'^([A-Z_]+)=(\d+)$', r.stdout, re.M))
if campos.get('SECTOR_BYTES') != '512' or campos.get('CLUSTER_BYTES') != '4096':
raise SystemExit('Geometria inesperada')
if 'USA=verificada_y_restaurada_en_memoria' not in r.stdout or 'END=atributos' not in r.stdout:
raise SystemExit('No se completo la lectura del registro')
for opciones, origen in [([], 'pequeno.txt'), (['-n', 'notas'], 'notas.txt')]:
datos = subprocess.run(['ntfscat', *opciones, str(imagen), '/pequeno.txt'],
capture_output=True, check=True).stdout
if datos != Path(origen).read_bytes():
raise SystemExit('Contenido inesperado: ' + origen)
for origen, salida in [('pequeno.txt', 'principal-extraido.txt'),
('notas.txt', 'notas-extraidas.txt'),
('grande.bin', 'grande-extraido.bin')]:
if huella(Path(origen)) != huella(Path(salida)):
raise SystemExit('Extraccion diferente: ' + salida)
copia = Path('usa-alterada.img')
with imagen.open('rb') as src, copia.open('xb') as dst:
shutil.copyfileobj(src, dst)
with copia.open('r+b') as f:
offset = int(campos['MFT_OFFSET']) + 510
f.seek(offset)
previo = f.read(1)
f.seek(offset)
f.write(bytes([previo[0] ^ 1]))
fallo = subprocess.run([programa, str(copia)], capture_output=True, text=True)
if fallo.returncode == 0 or 'USA:' not in fallo.stderr:
raise SystemExit('No se rechazo la proteccion alterada')
if huella(imagen) != antes:
raise SystemExit('La imagen original cambio')
print('OK: geometria, registro, flujos, extraccion y rechazo USA; original conservado')
PYLa copia se crea con exclusión. Para repetir el verificador, usa otro laboratorio o un nombre nuevo. La prueba verifica casos concretos; no acredita que el inspector sea un comprobador completo de NTFS.
Práctica opcional de ADS en PowerShell
Ejecuta esto en Windows sobre una ubicación personal que resida en NTFS. Crea un directorio exclusivo y solo dos flujos de un archivo propio. No requiere cambiar la configuración del volumen.
$LabAds38 = Join-Path ([System.IO.Path]::GetTempPath()) ('ads38-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $LabAds38 | Out-Null
$ArchivoAds38 = Join-Path $LabAds38 'informe.txt'
Set-Content -LiteralPath $ArchivoAds38 -Value 'contenido principal' -Encoding utf8
Set-Content -LiteralPath $ArchivoAds38 -Stream notas -Value 'anotacion del curso' -Encoding utf8
Get-Item -LiteralPath $ArchivoAds38 -Stream *
Get-Content -LiteralPath $ArchivoAds38
Get-Content -LiteralPath $ArchivoAds38 -Stream notasGet-Content sin -Stream lee el principal; con -Stream notas selecciona el alternativo. El ejemplo conserva los archivos para inspección. Si el proveedor no admite streams, el fallo no se resuelve interpretando un archivo ordinario con dos puntos en otro sistema como si fuera un ADS de NTFS.
16. Simuladores de residencia y mapeo
Un presupuesto de registro, no un umbral universal
El modelo reserva los bytes de otros metadatos indicados y calcula un $DATA sin nombre. Supone 24 bytes de encabezado residente o 96 bytes para encabezado y mapa no residente, alineados a ocho. Omite detalles reales; su resultado no predice la decisión de Windows ni NTFS-3G.
¿Cabe el valor en el registro?
VCN, ubicación y lectura lógica
El flujo tiene once clústeres de 4096 bytes. Sus runs son [0,3) → LCN 100, [3,5) → LCN 90, [5,9) → hueco y [9,11) → LCN 200. Su longitud válida inicializada termina en el byte 40960, al comienzo del último clúster. Se omiten compresión y cifrado.
Mapear no siempre significa leer datos físicos
17. Diagnóstico y resumen
| Observación | Interpretación a comprobar |
|---|---|
| El valor pequeño está dentro del registro. | Se está observando un atributo residente, no un archivo sin coste de almacenamiento. |
| Un delta de LCN es negativo. | El siguiente run comienza antes en el volumen; la longitud sigue siendo positiva. |
| Un final protegido no coincide con la USA. | No interpretar el registro como coherente ni restaurarlo en la imagen. |
| El flujo principal coincide, pero faltan anotaciones. | La copia pudo omitir flujos nombrados. |
| Hay asignación física después de la longitud válida. | Eso no permite entregar sus bytes crudos como contenido inicializado. |
| Un registro USN describe una modificación. | No significa que contenga la versión anterior completa. |
| La posición de un registro calculada por suma no funciona. | La MFT puede fragmentarse o necesitar extensiones de atributos. |
- NTFS organiza archivos mediante registros MFT y atributos tipados y opcionalmente nombrados.
- La residencia es una propiedad del atributo y depende del presupuesto del registro.
- Los data runs mapean VCN a LCN mediante longitudes y diferencias con signo.
- La USA protege transferencias de estructuras multisector; no sustituye una validación global.
- Tamaño lógico, longitud válida y asignación no son equivalentes.
- $LogFile, diario USN y USA cumplen objetivos diferentes.
- Un archivo puede tener varios flujos; conservar solo el principal puede perder contenido.
- El soporte de API, controlador y herramientas determina cómo se accede a estas funciones.
18. Preguntas de repaso y práctica
1. ¿Qué almacena la MFT?
Registros con información y atributos de archivos, incluidos archivos de metadatos del propio volumen.
2. ¿Un archivo siempre ocupa exactamente un registro?
No. Puede utilizar registros adicionales y referencias mediante $ATTRIBUTE_LIST.
3. ¿$MFTMirr duplica todo el contenido de la MFT?
No. Mantiene una parte inicial crítica, no una réplica completa de todos los registros.
4. ¿Qué significa el código -10 para el tamaño de registro?
Una longitud de 2^10, es decir, 1024 bytes.
5. ¿El LCN inicial basta para ubicar cualquier registro por suma física?
No. La MFT puede fragmentarse; hace falta interpretar su mapa para posiciones posteriores.
6. ¿Cuándo se restaura la USA en el ejemplo?
Después de verificar todos los finales protegidos y solo en el buffer de memoria.
7. ¿La USA verifica criptográficamente todos los datos?
No. Es un mecanismo de protección multisector con un alcance distinto.
8. ¿Qué diferencia hay entre tipo y nombre de atributo?
El tipo indica su clase, por ejemplo $DATA; el nombre puede distinguir varios flujos de esa clase.
9. ¿Un atributo residente necesita clústeres externos para su valor?
No para ese valor, aunque el registro MFT que lo contiene sí ocupa almacenamiento.
10. ¿Existe un tamaño universal que determina la residencia?
No. Depende del espacio disponible y de las demás estructuras del registro.
11. ¿Qué distingue VCN de LCN?
VCN es una posición de clúster dentro del flujo; LCN identifica un clúster del volumen.
12. ¿Un delta negativo significa longitud negativa?
No. Cambia el comienzo físico del run; la longitud sigue indicando cuántos clústeres lógicos abarca.
13. ¿Cómo representa el decodificador un run disperso?
Con un ancho de campo de desplazamiento igual a cero y una longitud positiva.
14. ¿Se entregan bytes crudos posteriores a la longitud válida?
No. La lectura lógica de esa región debe devolver ceros dentro del tamaño del flujo.
15. ¿Concatenar runs basta para extraer un flujo comprimido?
No. Deben interpretarse también sus unidades y reglas de compresión.
16. ¿$LogFile y $UsnJrnl son el mismo mecanismo?
No. Uno participa en recuperación; el otro describe cambios para consumidores de esa información.
17. ¿Leer informe.txt incluye automáticamente informe.txt:notas?
No. El flujo nombrado se selecciona por separado.
18. ¿Todo reparse point es un enlace simbólico?
No. Su etiqueta y sus datos pueden representar otras funciones.
19. ¿El inspector C enumera los atributos de pequeno.txt?
No. Enumera únicamente los presentes en el registro inicial de la propia $MFT.
20. ¿La extracción correcta del flujo principal prueba que la copia es completa?
No si la tarea requiere conservar también flujos alternativos y otros metadatos.
Para practicar
- Identifica los parámetros del volumen usando ntfsinfo y compáralos con el inspector C.
- Localiza el $DATA sin nombre del registro 0 y distingue su forma del flujo pequeño del laboratorio.
- Comprueba los dos $DATA de pequeno.txt y extrae cada uno por separado.
- Explica por qué aumentar otros atributos puede impedir que un mismo valor siga siendo residente.
- Decodifica manualmente los bytes 11 03 64 11 02 F6 01 04 00.
- Prueba una cadena hexadecimal truncada y otra sin terminador en runs-ntfs.
- Ejecuta el verificador y explica qué demuestra la alteración de un final protegido.
- En el simulador de mapeo, compara el último byte inicializado con el primero posterior a la longitud válida.
- Contrasta un hueco con una región asignada sin inicializar: ambos pueden leerse como ceros.
- Enumera qué información adicional necesita un extractor para manejar compresión y extensiones de atributos.
- Si utilizas Windows con NTFS, observa ambos flujos del ejemplo PowerShell.
- Diseña una prueba de copia que verifique contenido principal, flujos nombrados y los metadatos que tu aplicación necesita.
En el tema 39 compararemos ext4, NTFS y FAT: límites, permisos, journal, portabilidad y criterios para elegir según el uso y el entorno.