Ogni record MFT è di 1.024 byte. Ognuno è disposto allo stesso modo. Se sai leggerne uno in un editor esadecimale, sai leggerli tutti, e puoi scrivere un parser che batte i fragili strumenti commerciali nel recupero di record danneggiati. Il layout è abbastanza piccolo che un paio d'ore e 010 Editor ti rendano fluente.
Questo è il tour a livello di byte che do agli analisti che vogliono smettere di trattare i parser MFT come scatole nere.
La firma FILE
46 49 4C 45 ("FILE")
Primi quattro byte. ASCII FILE. Questa è la magia che ti permette di ricavare per carving record MFT dai cluster grezzi quando $MFT stessa è illeggibile. Percorri un'immagine disco a limiti di 512 byte (NTFS più vecchio) o 4096 byte (Win10 con record da 4K, raro ma esiste), fai pattern-matching su 46 49 4C 45 e di solito puoi recuperare la maggior parte di una MFT anche quando l'header proprio della tabella è distrutto.
L'altra firma che vedrai in libertà è BAAD (42 41 41 44). NTFS la scrive quando chkdsk ha deciso che un record era irreparabile. Lo slot viene mantenuto, il numero di sequenza è preservato, ma del resto dei byte non bisogna fidarsi. Tratta i record BAAD come prova che chkdsk ha girato e come bersagli per l'analisi del contesto circostante. Non provare a parsare i loro attributi ingenuamente.
L'header del record (offset 0x00 a 0x37, più o meno)
Dopo i quattro byte di firma, NTFS dispone un header che varia leggermente per versione NTFS. I campi, con offset dall'inizio del record, che usi davvero:
0x00 4 byte Firma ("FILE" o "BAAD")
0x04 2 byte Offset all'update sequence array (USA)
0x06 2 byte Dimensione USA in parole a 16 bit (records=count of (USN+fixup_entries))
0x08 8 byte Numero di sequenza di $LogFile (LSN)
0x10 2 byte Numero di sequenza
0x12 2 byte Conteggio hard link
0x14 2 byte Offset al primo attributo
0x16 2 byte Flag (bit 0 = IN_USE, bit 1 = DIRECTORY,
bit 2 = QUOTA_CHARGED, bit 3 = HAS_VIEW_INDEX)
0x18 4 byte Dimensione usata del record
0x1C 4 byte Dimensione allocata (sempre 1024 su volumi standard)
0x20 8 byte Riferimento al record file base (non zero su record di estensione)
0x28 2 byte ID del prossimo attributo
0x2A 2 byte (padding/allineamento su NTFS 3.0)
0x2C 4 byte Numero del record (NTFS 3.1+; auto-riferimento)
Alcuni di questi meritano attenzione speciale.
Flag a 0x16. Bit 0 azzerato significa eliminato. Bit 1 impostato significa directory (il record contiene attributi di indice anziché $DATA). La combinazione del fatto che entrambi i flag siano significativi è ciò che fa sì che un byte ti dica molto su una voce.
Numero di sequenza a 0x10. Si incrementa ogni volta che lo slot viene riusato. Il riferimento file a 64 bit (numero di record nei 48 bit bassi, numero di sequenza nei 16 alti) è il vero identificatore unico per l'esistenza di un particolare file. I riferimenti dentro altri attributi (riferimento padre $FILE_NAME, voci $ATTRIBUTE_LIST) usano questa forma a 64 bit. Un riferimento la cui sequenza non corrisponde al record attuale punta a un inquilino precedente; di solito un file eliminato. È così che Sleuth Kit percorre le catene di directory eliminate.
Riferimento al record file base a 0x20. Zero su un record base; non zero su un record di estensione (quando gli attributi di un file traboccano da un singolo slot). Il valore non zero è il riferimento a 64 bit del record base a cui appartiene questa estensione. I parser devono seguire le catene di $ATTRIBUTE_LIST per assemblare il file completo.
Numero di sequenza di $LogFile a 0x08. Punta in $LogFile. Utile per il recupero a livello transazionale; meno utile per l'analisi di routine. Vale la pena sapere che esiste.
L'array fixup (update sequence)
NTFS protegge contro le scritture lacerate con un piccolo trucco. Ogni record da 1.024 byte è diviso in due settori da 512 byte. Prima di scrivere, NTFS:
- Sceglie un numero di sequenza di aggiornamento (USN) a 16 bit (senza relazione con la USN di
$UsnJrnlnonostante l'acronimo condiviso). - Mette via i due byte originali di coda di ogni settore in un array che vive subito dopo l'header.
- Sostituisce i due byte di coda di ogni settore con la USN stessa.
In lettura, NTFS verifica che i due byte di coda di ogni settore da 512 byte siano uguali alla USN scelta. Se lo sono, la scrittura era atomica; tira fuori i byte originali dall'array di fixup e rimettili al loro posto. Se la coda di un settore non corrisponde, la scrittura era lacerata e il record è sospetto.
L'array è disposto come una parola USN seguita da N parole di fixup, dove N è il numero di settori. Per un record da 1.024 byte su un volume con settori da 512 byte, N = 2. L'array occupa quindi 6 byte in totale (USN, fixup_for_sector_0, fixup_for_sector_1). Il suo offset è il valore a 2 byte all'offset 0x04 del record (tipicamente 0x2A o 0x30 a seconda della versione NTFS).
La conseguenza pratica: se leggi blocchi grezzi da 1.024 byte di $MFT senza applicare i fixup, ogni record avrà spazzatura agli offset 510 e 1022. I $DATA residenti che attraversano quegli offset saranno corrotti. Parser come MFTECmd, omerbenamram/mft, analyzeMFT e fls/istat del Sleuth Kit applicano i fixup come primo passo. Se stai scrivendo il tuo parser (che è un buon esercizio; vedi parse-mft in Python), fallo prima di qualsiasi altra cosa.
Il flusso di attributi
Dopo l'header e l'array di fixup, ogni record contiene una serie di attributi tipizzati impacchettati uno dietro l'altro, allineati a 8 byte, terminati dal valore sentinella 0xFFFFFFFF dove andrebbe il codice di tipo del prossimo attributo.
Ogni attributo inizia con un piccolo header standardizzato:
0x00 4 byte Codice di tipo dell'attributo (0x10 = $STANDARD_INFORMATION, ecc.)
0x04 4 byte Lunghezza di questo attributo (header + dati)
0x08 1 byte Flag non-residente (0 = residente, 1 = non residente)
0x09 1 byte Lunghezza del nome (in caratteri; 0 se senza nome)
0x0A 2 byte Offset al nome (in caratteri dall'inizio dell'attributo)
0x0C 2 byte Flag (compresso/cifrato/sparse)
0x0E 2 byte ID dell'attributo
Per gli attributi residenti i campi successivi sono lunghezza e offset del contenuto; per i non residenti sono VCN iniziale/finale, offset della runlist, dimensione compressa/allocata/reale. Dopo tutto ciò, i dati veri. I nomi (se presenti) sono Unicode e non allineati; aspettati che l'implementazione sia rognosa.
Un record minimo contiene tre attributi:
$STANDARD_INFORMATION(0x10): timestamp, flag DOS, ID di sicurezza.$FILE_NAME(0x30): nome, riferimento al padre, secondo set di timestamp, dimensione allocata/reale al momento in cui il nome è stato impostato. I record possono averne molti (uno per hard link più il nome breve 8.3 sui volumi con la generazione 8.3 abilitata).$DATA(0x80): il contenuto del file, residente se ci sta, altrimenti runlist. I record possono avere più attributi$DATA; quello senza nome è lo stream primario, quelli nominati sono alternate data stream.
Per il catalogo completo degli attributi e dove vive ognuno, vedi il riferimento della Master File Table.
Perché conta quando i record sono danneggiati
La combinazione di un layout stabile da 1.024 byte, la firma FILE, il meccanismo di fixup e gli header di attributo auto-descrittivi è ciò che rende possibile il carving di record NTFS eliminati. Anche quando $MFT stessa è sparita (corruzione, scarabocchi del ransomware, wipe parziale), una scansione di firma del volume grezzo per i confini 46 49 4C 45 recupera i record finché i cluster sottostanti non sono stati sovrascritti. La verifica del fixup ti dà un controllo di integrità per settore; i record che la falliscono dovrebbero essere segnalati, ma i loro dati di attributi sono a volte ancora parzialmente leggibili.
Questa è la fondazione sotto strumenti come mmls+fls del Sleuth Kit, il "deep scan" di R-Studio e le varie suite commerciali di recupero. Tutti percorrono lo stesso layout di byte. Conoscerlo tu stesso è ciò che ti permette di fare un sanity-check sul loro output.
Leggerne uno a mano
Apri una $MFT estratta in 010 Editor con il template NTFS MFT Record applicato. Scegli il record 5 (la directory radice; offset 5 * 1024 = 5120 dall'inizio del file). Conferma:
- Byte 0x00 a 0x03:
46 49 4C 45. - Flag a 0x16:
0x03(IN_USE e DIRECTORY entrambi impostati). - Primo tipo di attributo all'offset dato da 0x14:
0x10($STANDARD_INFORMATION).
Se quelli combaciano, il record è stato parsato in modo pulito. Se non lo fanno, o il file è corrotto o l'array di fixup non è stato applicato (il template di 010 Editor lo applica per te).
Una volta fatto questo una volta, il resto della MFT diventa leggibile. I campi smettono di essere arcani e diventano un record che puoi leggere senza aiuto.
Letture aggiuntive
- Microsoft, Master File Table. Il riferimento ufficiale, conciso.
- La documentazione NTFS del Sleuth Kit. Le note di Brian Carrier restano la singola fonte più chiara per il layout dei record.
- Russon e Fledel, NTFS Documentation (progetto linux-ntfs). Offset dei campi per ogni attributo, tratti direttamente da anni di reverse engineering.