← Sistemas de Archivos y E/SBloque 11 · Sistemas de archivos reales · Tema 38 de 50

Tema 38

NTFS

NTFS describe archivos mediante registros de la MFT y atributos que pueden contener valores o localizar almacenamiento externo. Estudiaremos residencia, data runs, protección multisector y flujos alternativos, distinguiendo el registro de recuperación del diario de cambios. Dos programas en C, un laboratorio sin montaje y dos simuladores conectarán estas estructuras con observaciones verificables.

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 metadatosFunción que estudiaremos
$MFTAlmacena los registros de archivos, incluidos los de metadatos.
$MFTMirrReplica una parte inicial crítica de la MFT; no toda ella.
$BitmapRegistra la asignación de clústeres del volumen.
$LogFileParticipa en la recuperación de operaciones de metadatos.
$SecureOrganiza descriptores de seguridad.
$ExtendDirectorio 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.

subconjunto del sector inicial · posiciones en bytes
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 × ClusterBytes

Si 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.

Verificación de los finales de sectorUn registro FILE de 1024 bytes ocupa dos unidades de 512 bytes. Cada final contiene el mismo marcador. La USA conserva el marcador y las dos palabras originales; primero se verifican y después se restauran en memoria. USA: marcador + palabra original A + palabra original B Primer sector del registrofinal: debe coincidir con marcador Segundo sector del registrofinal: debe coincidir con marcador Si coinciden, restaurar A y B en una copia en memoria.
Ejemplo con registro de 1024 bytes y unidades de protección de 512 bytes.

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 habitualCódigoContenido o responsabilidad
$STANDARD_INFORMATION0x10Información básica del archivo.
$ATTRIBUTE_LIST0x20Referencias a atributos que pueden residir en otros registros.
$FILE_NAME0x30Nombre y relación con el directorio padre.
$DATA0x80Flujo de datos, sin nombre o nombrado.
$INDEX_ROOT / $INDEX_ALLOCATION0x90 / 0xA0Índices y su almacenamiento.
$BITMAP0xB0Asignació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.

ejemplo conceptual · dos formas de un flujo $DATA
Residente:
    registro FILE → encabezado $DATA → bytes del contenido

No residente:
    registro FILE → encabezado $DATA → mapping pairs
                                              ↓
                                  rangos de clústeres externos

Un 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.

tres runs · los intervalos excluyen el extremo superior
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

MecanismoObjetivoQué no debe suponerse
$LogFileApoyar la recuperación de operaciones de metadatos.No es una copia histórica de todos los bytes de todos los archivos.
$Extend/$UsnJrnl, cuando existeDescribir cambios para que aplicaciones puedan detectar actividad.No contiene necesariamente los datos anteriores ni registra para siempre cada detalle.
USA de un registroComprobar 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.

sintaxis de Windows · nombre de flujo y tipo de atributo
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 independiente

Leer 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.

inspeccionar-ntfs.c · límites, fixups y avance acotado por atributos
#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.

runs-ntfs.c · longitudes y diferencias con signo sin casts ambiguos
#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;
}
terminal · ejemplo conocido
$ ./runs-ntfs 1103641102f6010400
VCN=0 LENGTH=3 LCN=100
VCN=3 LENGTH=2 LCN=90
VCN=5 LENGTH=4 HOLE
END_VCN=9

Se 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.

terminal · compilar y preparar una ubicación exclusiva
$ 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.img

volumen.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

terminal · escritura offline solo sobre la imagen nueva
$ 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" 1103641102f6010400

Busca 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

terminal · destinos de salida nuevos del ensayo
$ 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.bin

Las 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.

Python 3 · contraste y protección multisector
$ 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')
PY

La 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.

PowerShell · flujo principal y flujo notas
$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 notas

Get-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?

modelo de residencia · clúster externo de 4096 bytes

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

ubicación y semántica de lectura del modelo

17. Diagnóstico y resumen

ObservaciónInterpretació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.

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

  1. Identifica los parámetros del volumen usando ntfsinfo y compáralos con el inspector C.
  2. Localiza el $DATA sin nombre del registro 0 y distingue su forma del flujo pequeño del laboratorio.
  3. Comprueba los dos $DATA de pequeno.txt y extrae cada uno por separado.
  4. Explica por qué aumentar otros atributos puede impedir que un mismo valor siga siendo residente.
  5. Decodifica manualmente los bytes 11 03 64 11 02 F6 01 04 00.
  6. Prueba una cadena hexadecimal truncada y otra sin terminador en runs-ntfs.
  7. Ejecuta el verificador y explica qué demuestra la alteración de un final protegido.
  8. En el simulador de mapeo, compara el último byte inicializado con el primero posterior a la longitud válida.
  9. Contrasta un hueco con una región asignada sin inicializar: ambos pueden leerse como ceros.
  10. Enumera qué información adicional necesita un extractor para manejar compresión y extensiones de atributos.
  11. Si utilizas Windows con NTFS, observa ambos flujos del ejemplo PowerShell.
  12. 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.