Sistemas de archivos · Lección 6 de 8
ext4: inodos, extensiones, diario y marcas de tiempo
13 min de lectura
ext4 es el sistema de archivos que encontrará con mayor frecuencia en un servidor Linux, un equipo de escritorio Linux o un teléfono Android antiguo. Su diseño difiere del de NTFS. Los nombres residen únicamente en los directorios, los demás metadatos residen en inodos de tamaño fijo, el contenido se localiza mediante extensiones y un diario puede conservar metadatos anteriores. Esta lección muestra cómo leer estas estructuras, convertir sus marcas de tiempo a UTC y evaluar qué queda tras la eliminación.
Dónde aparece ext4
ext4 es el sistema predeterminado en Debian, Ubuntu y muchas otras distribuciones de Linux, en servidores, máquinas virtuales en la nube y equipos de escritorio. No es universal: Red Hat Enterprise Linux utiliza XFS de forma predeterminada desde la versión 7, y algunas distribuciones usan Btrfs. En Android, la partición de datos de usuario (/data) utilizó ext4 en la mayoría de los dispositivos durante muchos años, aunque muchos dispositivos más recientes usan F2FS. ext4 también aparece en dispositivos como unidades NAS, grabadores de video y enrutadores.
Estructura en disco
Un volumen ext4 se divide en bloques, normalmente de 4.096 bytes (en volúmenes pequeños pueden ser de 1 KiB y 2 KiB). Los bloques se organizan en grupos de bloques. Con bloques de 4 KiB, un grupo contiene 32.768 bloques (tantos como bits caben en un bloque de mapa de bits), lo que equivale a 128 MiB.
- Superbloque. El superbloque siempre comienza en el byte 1.024 del volumen, independientemente del tamaño de bloque, y tiene una longitud de 1.024 bytes. Su número mágico 0xEF53 se encuentra en el desplazamiento 56 (0x38) dentro del superbloque, que corresponde al byte 1.080 del volumen. Como ext4 es little-endian, los bytes en disco son
53 EF. Otros campos útiles:s_log_block_size(0x18; tamaño de bloque = 1.024 << valor),s_inodes_per_group(0x28), hora del último montajes_mtime(0x2C), hora de la última escrituras_wtime(0x30),s_inode_size(0x58), UUID del volumen (0x68), etiqueta (0x78) ys_last_mounted(0x88), el directorio donde se montó por última vez. El grupo 0 contiene el superbloque principal, y existen copias de respaldo en el grupo 1 y en los grupos que son potencias de 3, 5 y 7 (sparse_super). - Tabla de descriptores de grupo. Esta tabla se encuentra después del superbloque (a partir del bloque 1 con bloques de 4 KiB). Contiene un descriptor por grupo: 32 bytes, o 64 bytes cuando la característica
64bitestá activada. Cada descriptor proporciona los números de bloque del mapa de bits de bloques, el mapa de bits de inodos y la tabla de inodos de ese grupo. - Mapa de bits de bloques y mapa de bits de inodos. Cada uno contiene un bit por bloque o inodo: 1 significa asignado y 0 significa libre. Con la característica
flex_bg, los mapas de bits y las tablas de inodos de varios grupos (16 de forma predeterminada) se agrupan juntos, por lo que la tabla de inodos de un grupo no está necesariamente dentro de ese grupo. - Tabla de inodos. Es una matriz de inodos. En ext4, cada inodo tiene 256 bytes de forma predeterminada; existen inodos de 128 bytes en sistemas de archivos antiguos o muy pequeños.
El inodo
Un inodo contiene toda la información sobre un archivo excepto su nombre. Campos clave y sus desplazamientos:
| Desplazamiento | Campo | Significado |
|---|---|---|
| 0x00 | i_mode | Tipo de archivo (archivo regular, directorio, enlace simbólico…) y bits de permisos |
| 0x02 / 0x18 | i_uid / i_gid | ID del usuario y del grupo propietarios (16 bits inferiores; los bits superiores se almacenan más adelante) |
| 0x04 | i_size_lo | Tamaño del archivo en bytes (32 bits inferiores; los bits superiores están en 0x6C) |
| 0x08 | i_atime | Último acceso |
| 0x0C | i_ctime | Cambio del inodo (cambio de metadatos) |
| 0x10 | i_mtime | Última modificación del contenido |
| 0x14 | i_dtime | Hora de eliminación (0 mientras el inodo está en uso) |
| 0x1A | i_links_count | Número de entradas de directorio (enlaces duros) que apuntan aquí |
| 0x28 | i_block | 60 bytes: cabecera de extensiones y extensiones, o el antiguo mapa de bloques |
| 0x84–0x8C | i_ctime_extra, i_mtime_extra, i_atime_extra | Nanosegundos y bits adicionales de época |
| 0x90 / 0x94 | i_crtime / i_crtime_extra | Hora de nacimiento (creación) |
El error de interpretación más común en el análisis forense de Linux es considerar ctime como la "hora de creación". Es la hora de cambio del inodo, que se actualiza cada vez que cambian los metadatos: permisos, propietario, número de enlaces, cambios de nombre o escrituras de contenido. La verdadera hora de creación es crtime, que ext4 almacena únicamente en inodos de 256 bytes o más. Las herramientas la etiquetan como "Nacimiento" (stat) o "Archivo creado" (istat de TSK). Interprete atime con cautela: la opción de montaje predeterminada relatime solo lo actualiza cuando es anterior a mtime o ctime, o cuando tiene más de 24 horas de antigüedad.
Las cuatro marcas de tiempo representan segundos desde la época Unix (1970-01-01T00:00:00Z) en UTC, almacenados como valores de 32 bits con signo. Un valor de 32 bits con signo alcanza su límite en 2038-01-19T03:14:07Z. ext4 resuelve esto con los campos _extra. Cada uno tiene 32 bits: los 2 bits inferiores son bits de época que amplían el valor de segundos (se suma época × 2³²), y los 30 bits superiores son nanosegundos. Esto proporciona precisión de nanosegundos y fechas hasta el año 2446. Los sistemas de archivos con inodos de 128 bytes carecen de los campos adicionales y de crtime, y mantienen el límite de 2038.
Los primeros diez inodos están reservados. El inodo 2 es el directorio raíz, y el inodo 8 es el diario. El primer inodo ordinario es el 11, normalmente utilizado por lost+found. La numeración de inodos comienza en 1, no en 0.
Extensiones frente al antiguo mapa de bloques
ext2 y ext3 utilizaban i_block como un mapa de bloques: 12 punteros directos más punteros indirectos simples, dobles y triples (15 × 4 bytes). ext4 utiliza extensiones, cada una de las cuales describe una secuencia contigua de bloques.
Los 60 bytes de i_block contienen una cabecera de extensiones de 12 bytes seguida de hasta cuatro extensiones de 12 bytes. La cabecera comienza con el número mágico 0xF30A (en disco, 0A F3), seguido de eh_entries (entradas en uso), eh_max (capacidad, 4 dentro del inodo) y eh_depth. Cada extensión contiene el primer bloque lógico que abarca (ee_block, 4 bytes), una longitud (ee_len, 2 bytes) y un bloque físico inicial de 48 bits (ee_start_hi, 2 bytes + ee_start_lo, 4 bytes). Una extensión inicializada abarca como máximo 32.768 bloques (128 MiB con bloques de 4 KiB); un valor de ee_len superior a 32.768 indica una extensión no escrita (preasignada). Cuando un archivo necesita más de cuatro extensiones, eh_depth es mayor que 0 y las entradas dentro del inodo pasan a ser entradas de índice que apuntan a bloques del árbol de extensiones situados en otra parte del volumen. Los archivos en volúmenes actualizados desde ext3 pueden seguir utilizando un mapa de bloques, por lo que debe comprobar primero la presencia de 0xF30A.
Entradas de directorio
Un directorio es un archivo cuyos bloques de datos contienen entradas en el formato ext4_dir_entry_2:
| Desplazamiento | Tamaño | Campo |
|---|---|---|
| 0 | 4 | Número de inodo |
| 4 | 2 | Longitud del registro (rec_len, distancia hasta la siguiente entrada) |
| 6 | 1 | Longitud del nombre |
| 7 | 1 | Tipo de archivo (1 = archivo regular, 2 = directorio, 7 = enlace simbólico) |
| 8 | n | Nombre (sin terminación nula; el registro se rellena hasta un múltiplo de 4) |
Para report.pdf, el registro ocupa 8 + 10 = 18 bytes y se rellena hasta 20. Los directorios grandes utilizan un árbol B con hash (htree), la característica dir_index. El primer bloque contiene un índice hash que apunta a bloques hoja, y los bloques hoja contienen entradas normales. Por lo tanto, los nombres están ordenados por hash, no por orden de creación. Cuando se elimina una entrada, el campo rec_len de la entrada anterior suele ampliarse para abarcarla, y en kernels anteriores a 5.13 el nombre y el número de inodo antiguos suelen seguir siendo legibles en ese espacio; así es como las herramientas enumeran los nombres eliminados. Desde Linux 5.13, el kernel pone a cero la entrada eliminada, por lo que los nombres eliminados con kernels más recientes suelen desaparecer.
Ejemplo resuelto: localizar un inodo y leer su hora
fsstat informa de 8.192 inodos por grupo y bloques de 4.096 bytes. Desea localizar el inodo 393.231.
- Grupo = (393.231 − 1) / 8.192 = 393.230 / 8.192 = 48 (división entera).
- Índice = (393.231 − 1) mod 8.192 = 393.230 − 48 × 8.192 = 393.230 − 393.216 = 14.
- Desplazamiento en bytes dentro de la tabla de inodos = 14 × 256 = 3.584.
- Si el descriptor del grupo 48 indica que la tabla de inodos está en el bloque 1.572.896, el inodo se encuentra a 1.572.896 × 4.096 + 3.584 = 6.442.585.600 bytes desde el inicio del volumen.
debugfs puede confirmarlo con imap <393231>, que muestra el bloque y el desplazamiento de un inodo.
En el desplazamiento 0x10 del inodo (i_mtime) se lee 80 B4 1D 67. En little-endian, eso es 0x671DB480 = 1.730.000.000 segundos. 1.730.000.000 / 86.400 = 20.023 días con un resto de 12.800 segundos. El día 20.023 después de 1970-01-01 es 2024-10-27, y 12.800 s equivalen a 03:33:20. En 0x88 (i_mtime_extra) se lee 54 34 6F 1D = 0x1D6F3454. Los 2 bits inferiores son 00 (época 0, sin ajuste), y 0x1D6F3454 >> 2 = 123.456.789 ns. El valor de mtime es 2024-10-27T03:33:20.123456789Z.
El diario jbd2
ext4 utiliza el diario jbd2, normalmente almacenado en el inodo 8. A diferencia del resto de ext4, las estructuras de jbd2 son big-endian, y los bloques de control del propio diario (su superbloque y sus bloques de descriptor, confirmación y revocación) comienzan con el número mágico 0xC03B3998. El diario es un registro circular de transacciones. Cada transacción contiene bloques de descriptor, luego copias completas de los bloques del sistema de archivos que se están modificando y, después, un bloque de confirmación. La opción de montaje data= establece qué se registra en el diario:
| Modo | Qué se registra en el diario |
|---|---|
data=ordered (predeterminado) | Solo metadatos; los datos del archivo se escriben en su ubicación definitiva antes de que se confirmen los metadatos |
data=writeback | Solo metadatos, sin garantía de orden para los datos |
data=journal | Metadatos y datos de archivos |
Como se registran bloques completos de metadatos, el diario puede contener versiones anteriores de bloques de tablas de inodos, directorios y mapas de bits. Tras una eliminación reciente, una copia anterior del bloque que contiene el inodo puede seguir mostrando sus extensiones. Herramientas como ext4magic y extundelete automatizan esta búsqueda, y debugfs logdump -i <inode> y jls/jcat de TSK permiten inspeccionar el diario manualmente. Las limitaciones son reales. El diario es pequeño y circular, por lo que, en un sistema con mucha actividad, las copias antiguas se sobrescriben rápidamente. En el modo ordenado, el contenido no se registra en el diario, por lo que un mapa de extensiones recuperado solo resulta útil si esos bloques de datos no se han reutilizado. Ambas herramientas son proyectos antiguos que pueden manejar incorrectamente las características más recientes de ext4, así que valide sus resultados. Tenga en cuenta también que montar un volumen ext4 con cambios pendientes reproduce el diario incluso en modo de solo lectura, a menos que se especifique noload.
Eliminación en ext4
Cuando se elimina un archivo, se desvincula su entrada de directorio. El contador de enlaces del inodo pasa a 0, i_size se establece en 0, se establece i_dtime y se ponen a cero los bits correspondientes en los mapas de bits de inodos y de bloques. Lo fundamental es que las entradas de extensiones de i_block se ponen a cero, y mtime y ctime normalmente cambian a la hora de eliminación, mientras que crtime se conserva. El inodo ya no indica dónde estaba el contenido. ext2 dejaba intactos los punteros de bloques, por lo que los archivos eliminados eran fáciles de recuperar a partir de los metadatos; ext3 borraba sus punteros y ext4 borra sus extensiones. Las opciones realistas en ext4 son el análisis del diario y la recuperación por firmas (carving) de bloques no asignados (lección 1.8). El inodo eliminado sigue mostrando el modo, el propietario, dtime y crtime, y en kernels anteriores a 5.13 la antigua entrada de directorio suele seguir vinculando el nombre con el inodo.
Lectura de ext4 con herramientas comunes
Ejemplos con The Sleuth Kit (-o es el desplazamiento de la partición en sectores) y debugfs, que recibe el desplazamiento en bytes como parte del nombre del archivo (2.048 × 512 = 1.048.576):
| Tarea | Ejemplo |
|---|---|
| Superbloque, tamaño de bloque, inodos por grupo, rangos de grupos | fsstat -o 2048 image.dd |
Enumerar nombres de forma recursiva, con los eliminados marcados con * | fls -r -p -o 2048 image.dd |
| Detalles del inodo: horas, tamaño, extensiones, hora de "Eliminación" | istat -o 2048 image.dd 393231 |
| Extraer contenido por inodo | icat -o 2048 image.dd 393231 > out.bin |
| Información del inodo con crtime y nanosegundos | debugfs -R "stat <393231>" "image.dd?offset=1048576" |
| Copias en el diario del bloque de un inodo | debugfs -R "logdump -i <393231>" "image.dd?offset=1048576" |
debugfs abre en modo de solo lectura a menos que se especifique -w. Cuando un valor sea importante, contraste los resultados de las herramientas con una lectura hexadecimal directa del inodo.
Enfoque del examen: Deberá saber localizar un inodo a partir de su número (grupo, índice y desplazamiento), convertir una marca de tiempo de ext4, incluidos sus nanosegundos de _extra, a un valor UTC en formato ISO 8601, y distinguir crtime de ctime. Las preguntas también pueden evaluar las firmas 0xEF53 (superbloque, byte 1.080) y 0xF30A (cabecera de extensiones), los inodos especiales 2 y 8 y el primer inodo ordinario 11, así como por qué los archivos ext4 eliminados suelen requerir análisis del diario o recuperación por firmas en lugar de recuperación a partir de metadatos.
Puntos clave
- El superbloque comienza en el byte 1.024; el número mágico 0xEF53 está en el desplazamiento 56 dentro de él. Los descriptores de grupo localizan los mapas de bits y la tabla de inodos de cada grupo.
- Grupo = (inodo − 1) / inodos_por_grupo; índice = (inodo − 1) mod inodos_por_grupo; desplazamiento = índice × tamaño del inodo.
- ctime es la hora de cambio del inodo. La hora de nacimiento es crtime, presente únicamente en inodos de 256 bytes o más. Los campos
_extraañaden nanosegundos y los bits de época que permiten superar el año 2038. i_blockcontiene una cabecera de extensiones (0xF30A) y hasta cuatro extensiones; los archivos más grandes utilizan un árbol de extensiones.- La eliminación pone a cero las extensiones del inodo, por lo que la recuperación depende de copias antiguas de metadatos en el diario jbd2 o de la recuperación por firmas.
- El inodo 2 es la raíz, el inodo 8 es el diario y el inodo 11 suele ser
lost+found.