File system · Lezione 6 di 8
ext4: inode, extent, journal e timestamp
13 min di lettura
ext4 è il file system che si incontra più spesso su un server Linux, un desktop Linux o un telefono Android meno recente. La sua struttura è diversa da quella di NTFS. I nomi risiedono solo nelle directory, gli altri metadati risiedono in inode di dimensione fissa, il contenuto viene individuato tramite extent e un journal può conservare metadati precedenti. Questa lezione illustra come leggere queste strutture, convertire i relativi timestamp in UTC e valutare ciò che rimane dopo l'eliminazione.
Dove si trova ext4
ext4 è il file system predefinito in Debian, Ubuntu e molte altre distribuzioni Linux, su server, macchine virtuali nel cloud e desktop. Non è universale: Red Hat Enterprise Linux utilizza XFS come file system predefinito dalla versione 7 e alcune distribuzioni utilizzano Btrfs. Su Android, la partizione dei dati utente (/data) è stata formattata in ext4 sulla maggior parte dei dispositivi per molti anni, sebbene molti dispositivi più recenti utilizzino F2FS. ext4 si trova anche in dispositivi dedicati come unità NAS, videoregistratori e router.
Organizzazione su disco
Un volume ext4 è suddiviso in blocchi, solitamente di 4.096 byte (sui volumi piccoli sono possibili blocchi da 1 KiB e 2 KiB). I blocchi sono organizzati in gruppi di blocchi. Con blocchi da 4 KiB, un gruppo contiene 32.768 blocchi (tanti quanti i bit contenuti in un blocco bitmap), equivalenti a 128 MiB.
- Superblocco. Il superblocco inizia sempre al byte 1.024 del volume, indipendentemente dalla dimensione dei blocchi, ed è lungo 1.024 byte. Il suo numero magico 0xEF53 si trova all'offset 56 (0x38) all'interno del superblocco, corrispondente al byte 1.080 del volume. Poiché ext4 è little-endian, i byte su disco sono
53 EF. Altri campi utili:s_log_block_size(0x18; dimensione del blocco = 1.024 << valore),s_inodes_per_group(0x28), ora dell'ultimo montaggios_mtime(0x2C), ora dell'ultima scritturas_wtime(0x30),s_inode_size(0x58), UUID del volume (0x68), etichetta (0x78) es_last_mounted(0x88), la directory in cui è stato montato l'ultima volta. Il gruppo 0 contiene il superblocco primario; copie di backup sono presenti nel gruppo 1 e nei gruppi i cui numeri sono potenze di 3, 5 e 7 (sparse_super). - Tabella dei descrittori di gruppo. Questa tabella segue il superblocco (a partire dal blocco 1 con blocchi da 4 KiB). Contiene un descrittore per gruppo: 32 byte, oppure 64 byte quando la funzionalità
64bitè attiva. Ogni descrittore indica i numeri dei blocchi della bitmap dei blocchi, della bitmap degli inode e della tabella degli inode del relativo gruppo. - Bitmap dei blocchi e bitmap degli inode. Ciascuna contiene un bit per blocco o inode: 1 significa allocato, 0 significa libero. Con la funzionalità
flex_bg, le bitmap e le tabelle degli inode di più gruppi (16 per impostazione predefinita) vengono raggruppate insieme, pertanto la tabella degli inode di un gruppo non si trova necessariamente all'interno di quel gruppo. - Tabella degli inode. È un array di inode. In ext4 ogni inode occupa 256 byte per impostazione predefinita; nei file system vecchi o molto piccoli esistono inode da 128 byte.
L'inode
Un inode contiene tutte le informazioni su un file tranne il nome. Campi principali e relativi offset:
| Offset | Campo | Significato |
|---|---|---|
| 0x00 | i_mode | Tipo di file (regolare, directory, collegamento simbolico…) e bit dei permessi |
| 0x02 / 0x18 | i_uid / i_gid | ID dell'utente proprietario e del gruppo (16 bit meno significativi; i bit più significativi sono memorizzati più avanti) |
| 0x04 | i_size_lo | Dimensione del file in byte (32 bit meno significativi; i bit più significativi si trovano a 0x6C) |
| 0x08 | i_atime | Ultimo accesso |
| 0x0C | i_ctime | Modifica dell'inode (modifica dei metadati) |
| 0x10 | i_mtime | Ultima modifica del contenuto |
| 0x14 | i_dtime | Ora di eliminazione (0 mentre l'inode è in uso) |
| 0x1A | i_links_count | Numero di voci di directory (collegamenti fisici) che puntano a questo inode |
| 0x28 | i_block | 60 byte: intestazione degli extent ed extent, oppure la vecchia mappa dei blocchi |
| 0x84–0x8C | i_ctime_extra, i_mtime_extra, i_atime_extra | Nanosecondi e bit aggiuntivi dell'epoca |
| 0x90 / 0x94 | i_crtime / i_crtime_extra | Ora di nascita (creazione) |
L'errore di interpretazione più comune nell'analisi forense di Linux consiste nel considerare ctime come "ora di creazione". È invece l'ora di modifica dell'inode, aggiornata ogni volta che cambiano i metadati: permessi, proprietà, numero di collegamenti, ridenominazioni o scritture del contenuto. La vera ora di creazione è crtime, che ext4 memorizza solo negli inode da 256 byte o più. Gli strumenti la etichettano come "Nascita" (stat) o "File creato" (TSK istat). Occorre trattare atime con cautela: l'opzione di montaggio predefinita relatime lo aggiorna solo quando è precedente a mtime o ctime, oppure risale a più di 24 ore prima.
Tutti e quattro i timestamp rappresentano i secondi trascorsi dall'epoca Unix (1970-01-01T00:00:00Z) in UTC, memorizzati come valori a 32 bit con segno. Un valore a 32 bit con segno esaurisce il proprio intervallo il 2038-01-19T03:14:07Z. ext4 risolve questo problema con i campi _extra. Ciascuno è di 32 bit: i 2 bit meno significativi sono bit dell'epoca che estendono il valore dei secondi (aggiungendo epoca × 2³²), mentre i 30 bit più significativi rappresentano i nanosecondi. Ciò offre precisione al nanosecondo e date fino al 2446. I file system con inode da 128 byte non dispongono dei campi aggiuntivi né di crtime e mantengono il limite del 2038.
I primi dieci inode sono riservati. L'inode 2 è la directory radice e l'inode 8 è il journal. Il primo inode ordinario è l'11, normalmente utilizzato da lost+found. La numerazione degli inode parte da 1, non da 0.
Extent e vecchia mappa dei blocchi
ext2 ed ext3 utilizzavano i_block come mappa dei blocchi: 12 puntatori diretti più puntatori indiretti singoli, doppi e tripli (15 × 4 byte). ext4 utilizza gli extent, ciascuno dei quali descrive una sequenza contigua di blocchi.
I 60 byte di i_block contengono un'intestazione degli extent di 12 byte seguita da un massimo di quattro extent da 12 byte. L'intestazione inizia con il numero magico 0xF30A (su disco 0A F3), seguito da eh_entries (voci in uso), eh_max (capacità, pari a 4 all'interno dell'inode) ed eh_depth. Ogni extent contiene il primo blocco logico che copre (ee_block, 4 byte), una lunghezza (ee_len, 2 byte) e un blocco fisico iniziale a 48 bit (ee_start_hi 2 byte + ee_start_lo 4 byte). Un extent inizializzato copre al massimo 32.768 blocchi (128 MiB con blocchi da 4 KiB); un valore di ee_len superiore a 32.768 contrassegna un extent non scritto (preallocato). Quando un file richiede più di quattro extent, eh_depth è maggiore di 0 e le voci all'interno dell'inode diventano voci di indice che puntano a blocchi dell'albero degli extent situati altrove nel volume. I file su volumi aggiornati da ext3 possono ancora utilizzare una mappa dei blocchi, quindi occorre verificare prima la presenza di 0xF30A.
Voci di directory
Una directory è un file i cui blocchi dati contengono voci nel formato ext4_dir_entry_2:
| Offset | Dimensione | Campo |
|---|---|---|
| 0 | 4 | Numero di inode |
| 4 | 2 | Lunghezza del record (rec_len, distanza dalla voce successiva) |
| 6 | 1 | Lunghezza del nome |
| 7 | 1 | Tipo di file (1 = file regolare, 2 = directory, 7 = collegamento simbolico) |
| 8 | n | Nome (non terminato da un carattere nullo; record completato con riempimento fino a un multiplo di 4) |
Per report.pdf il record è di 8 + 10 = 18 byte, portati a 20 mediante riempimento. Le directory di grandi dimensioni utilizzano un B-tree basato su hash (htree), la funzionalità dir_index. Il primo blocco contiene un indice hash che punta ai blocchi foglia, e i blocchi foglia contengono voci normali. I nomi sono pertanto ordinati per hash, non per ordine di creazione. Quando una voce viene rimossa, il valore rec_len della voce precedente solitamente aumenta fino a coprirla e, nei kernel precedenti alla versione 5.13, il vecchio nome e il numero di inode rimangono spesso leggibili in quello spazio; è così che gli strumenti elencano i nomi eliminati. A partire da Linux 5.13 il kernel azzera la voce rimossa, quindi i nomi eliminati con kernel più recenti solitamente non sono più disponibili.
Esempio svolto: individuare un inode e leggerne il timestamp
fsstat riporta 8.192 inode per gruppo e blocchi da 4.096 byte. Si vuole individuare l'inode 393.231.
- Gruppo = (393.231 − 1) / 8.192 = 393.230 / 8.192 = 48 (divisione intera).
- Indice = (393.231 − 1) mod 8.192 = 393.230 − 48 × 8.192 = 393.230 − 393.216 = 14.
- Offset in byte all'interno della tabella degli inode = 14 × 256 = 3.584.
- Se il descrittore del gruppo 48 indica una tabella degli inode al blocco 1.572.896, l'inode si trova a 1.572.896 × 4.096 + 3.584 = 6.442.585.600 byte dall'inizio del volume.
debugfs può confermarlo con imap <393231>, che visualizza il blocco e l'offset di un inode.
All'offset 0x10 dell'inode (i_mtime) si legge 80 B4 1D 67. In little-endian, corrisponde a 0x671DB480 = 1.730.000.000 secondi. 1.730.000.000 / 86.400 = 20.023 giorni con un resto di 12.800 secondi. Il giorno 20.023 dopo il 1970-01-01 è il 2024-10-27 e 12.800 s corrispondono alle 03:33:20. All'offset 0x88 (i_mtime_extra) si legge 54 34 6F 1D = 0x1D6F3454. I 2 bit meno significativi sono 00 (epoca 0, nessuna correzione) e 0x1D6F3454 >> 2 = 123.456.789 ns. Il valore di mtime è 2024-10-27T03:33:20.123456789Z.
Il journal jbd2
ext4 utilizza il journal jbd2, normalmente memorizzato nell'inode 8. A differenza del resto di ext4, le strutture jbd2 sono big-endian e i blocchi di controllo del journal (il suo superblocco e i blocchi descrittore, di commit e di revoca) iniziano con il numero magico 0xC03B3998. Il journal è un registro circolare di transazioni. Ogni transazione contiene blocchi descrittore, poi copie complete dei blocchi del file system che vengono modificati, quindi un blocco di commit. L'opzione di montaggio data= determina ciò che viene registrato nel journal:
| Modalità | Cosa viene registrato nel journal |
|---|---|
data=ordered (predefinita) | Solo metadati; i dati dei file vengono scritti nella posizione definitiva prima del commit dei metadati |
data=writeback | Solo metadati, senza garanzie sull'ordine di scrittura dei dati |
data=journal | Metadati e dati dei file |
Poiché vengono registrati interi blocchi di metadati, il journal può contenere versioni precedenti dei blocchi della tabella degli inode, delle directory e delle bitmap. Dopo un'eliminazione recente, una copia precedente del blocco contenente l'inode potrebbe ancora mostrarne gli extent. Strumenti come ext4magic ed extundelete automatizzano questa ricerca, mentre debugfs logdump -i <inode> e gli strumenti TSK jls/jcat consentono di esaminare manualmente il journal. I limiti sono concreti. Il journal è piccolo e circolare, quindi su un sistema molto attivo le copie precedenti vengono sovrascritte rapidamente. In modalità ordered il contenuto non viene registrato nel journal, quindi una mappa degli extent recuperata è utile solo se quei blocchi dati non sono stati riutilizzati. Entrambi gli strumenti sono progetti datati che potrebbero gestire in modo errato le funzionalità più recenti di ext4, pertanto occorre verificarne i risultati. Si noti inoltre che il montaggio di un volume ext4 in stato non pulito comporta la riapplicazione del journal anche in sola lettura, a meno che non venga specificato noload.
Eliminazione su ext4
Quando un file viene eliminato, la sua voce di directory viene scollegata. Il numero di collegamenti dell'inode scende a 0, i_size viene impostato a 0, i_dtime viene impostato e i bit delle bitmap degli inode e dei blocchi vengono azzerati. È fondamentale notare che le voci degli extent in i_block vengono azzerate e mtime e ctime normalmente assumono l'ora di eliminazione, mentre crtime rimane invariato. L'inode non indica più dove si trovava il contenuto. ext2 lasciava intatti i puntatori ai blocchi, quindi i file eliminati erano facili da recuperare dai metadati; ext3 azzerava i puntatori ed ext4 azzera gli extent. Le opzioni realistiche su ext4 sono l'analisi del journal e il carving dei blocchi non allocati in base alla firma (lezione 1.8). L'inode eliminato mostra ancora tipo e permessi, proprietario, dtime e crtime e, nei kernel precedenti alla versione 5.13, la vecchia voce di directory spesso collega ancora il nome all'inode.
Lettura di ext4 con strumenti comuni
Esempi con The Sleuth Kit (-o è l'offset della partizione in settori) e debugfs, che accetta l'offset in byte come parte del nome del file (2.048 × 512 = 1.048.576):
| Attività | Esempio |
|---|---|
| Superblocco, dimensione dei blocchi, inode per gruppo, intervalli dei gruppi | fsstat -o 2048 image.dd |
Elencare i nomi ricorsivamente, con quelli eliminati contrassegnati da * | fls -r -p -o 2048 image.dd |
| Dettagli dell'inode: timestamp, dimensione, extent, ora di "eliminazione" | istat -o 2048 image.dd 393231 |
| Estrarre il contenuto tramite inode | icat -o 2048 image.dd 393231 > out.bin |
| Informazioni stat dell'inode con crtime e nanosecondi | debugfs -R "stat <393231>" "image.dd?offset=1048576" |
| Copie nel journal del blocco di un inode | debugfs -R "logdump -i <393231>" "image.dd?offset=1048576" |
debugfs apre in sola lettura, a meno che non venga specificato -w. Quando un valore è importante, occorre verificare i risultati degli strumenti confrontandoli con una lettura esadecimale grezza dell'inode.
Focus sull'esame: Occorre saper individuare un inode a partire dal suo numero (gruppo, indice e offset), convertire un timestamp ext4, inclusi i nanosecondi del campo _extra, in un valore UTC ISO 8601 e distinguere crtime da ctime. Le domande possono anche verificare la conoscenza delle firme 0xEF53 (superblocco, byte 1.080) e 0xF30A (intestazione degli extent), degli inode speciali 2 e 8 e del primo inode ordinario 11, nonché del motivo per cui i file ext4 eliminati richiedono solitamente l'analisi del journal o il carving anziché il recupero dai metadati.
Punti chiave
- Il superblocco inizia al byte 1.024; il numero magico 0xEF53 si trova all'offset 56 al suo interno. I descrittori di gruppo consentono di individuare le bitmap e la tabella degli inode di ciascun gruppo.
- Gruppo = (inode − 1) / inodes_per_group; indice = (inode − 1) mod inodes_per_group; offset = indice × dimensione dell'inode.
- ctime è l'ora di modifica dell'inode. L'ora di nascita è crtime, presente solo negli inode da 256 byte o più. I campi
_extraaggiungono nanosecondi e i bit dell'epoca che consentono di superare il 2038. i_blockcontiene un'intestazione degli extent (0xF30A) e fino a quattro extent; i file più grandi utilizzano un albero degli extent.- L'eliminazione azzera gli extent nell'inode, quindi il recupero dipende da copie precedenti dei metadati nel journal jbd2 oppure dal carving.
- L'inode 2 è la radice, l'inode 8 è il journal e l'inode 11 è solitamente
lost+found.