Todos os registos do MFT têm 1.024 bytes. Todos eles estão dispostos da mesma forma. Se conseguir ler um num editor hexadecimal, consegue ler todos, e consegue escrever um parser que bate as ferramentas comerciais frágeis na recuperação de registos danificados. A disposição é suficientemente pequena para que algumas horas e um 010 Editor o tornem fluente.
Este é o tour ao nível do byte que dou a analistas que querem deixar de tratar os parsers de MFT como caixas pretas.
A assinatura FILE
46 49 4C 45 ("FILE")
Primeiros quatro bytes. ASCII FILE. É a magia que permite extrair registos de MFT a partir de clusters brutos quando o próprio $MFT está ilegível. Percorra uma imagem de disco em fronteiras de 512 bytes (NTFS mais antigo) ou de 4096 bytes (Windows 10 com registos de 4K, raros mas existem), procure a sequência 46 49 4C 45, e geralmente consegue recuperar a maior parte de um MFT mesmo quando o cabeçalho da tabela está destruído.
A outra assinatura com que se vai cruzar é BAAD (42 41 41 44). O NTFS escreve isto quando o chkdsk decidiu que um registo era irreparável. O slot é mantido, o número de sequência é preservado, mas os restantes bytes não são de confiança. Trate os registos BAAD como evidência de que o chkdsk correu e como alvos para análise contextual circundante. Não tente analisar os seus atributos de forma ingénua.
O cabeçalho do registo (offsets 0x00 a 0x37, mais ou menos)
Depois dos quatro bytes de assinatura, o NTFS dispõe um cabeçalho que varia ligeiramente com a versão do NTFS. Os campos que efectivamente usa, com offsets a partir do início do registo:
0x00 4 bytes Assinatura ("FILE" ou "BAAD")
0x04 2 bytes Offset para o update sequence array (USA)
0x06 2 bytes Tamanho do USA em words de 16 bits (records=contagem de (USN+fixup_entries))
0x08 8 bytes Número de sequência do $LogFile (LSN)
0x10 2 bytes Número de sequência
0x12 2 bytes Contador de hard links
0x14 2 bytes Offset para o primeiro atributo
0x16 2 bytes Flags (bit 0 = IN_USE, bit 1 = DIRECTORY,
bit 2 = QUOTA_CHARGED, bit 3 = HAS_VIEW_INDEX)
0x18 4 bytes Tamanho usado do registo
0x1C 4 bytes Tamanho alocado (sempre 1024 em volumes padrão)
0x20 8 bytes Referência ao base file record (não zero em registos de extensão)
0x28 2 bytes Próximo ID de atributo
0x2A 2 bytes (padding/alinhamento em NTFS 3.0)
0x2C 4 bytes Número do registo (NTFS 3.1+; auto-referência)
Alguns destes merecem atenção especial.
Flags em 0x16. Bit 0 limpo significa eliminado. Bit 1 ligado significa directório (o registo contém atributos de índice em vez de $DATA). A combinação de ambas as flags serem significativas é o que faz com que um byte lhe diga muito sobre uma entrada.
Número de sequência em 0x10. Incrementado sempre que o slot é reutilizado. A referência de ficheiro de 64 bits (número de registo nos 48 bits inferiores, número de sequência nos 16 bits superiores) é o identificador único real para a existência de um ficheiro em particular. Referências dentro de outros atributos (referência ao pai em $FILE_NAME, entradas em $ATTRIBUTE_LIST) usam esta forma de 64 bits. Uma referência cuja sequência não corresponde ao registo actual aponta para um inquilino anterior; geralmente um ficheiro eliminado. É assim que o Sleuth Kit percorre cadeias de directórios eliminados.
Referência ao base file record em 0x20. Zero num registo base; não zero num registo de extensão (quando os atributos de um ficheiro transbordam um único slot). O valor não-zero é a referência de 64 bits do registo base a que esta extensão pertence. Os parsers têm de seguir as cadeias de $ATTRIBUTE_LIST para montar o ficheiro completo.
Número de sequência do $LogFile em 0x08. Aponta para o $LogFile. Útil para recuperação ao nível de transacção; menos útil para análise rotineira. Vale a pena saber que existe.
O array de fixup (update sequence)
O NTFS protege-se contra torn writes com um pequeno truque. Cada registo de 1.024 bytes é dividido em dois sectores de 512 bytes. Antes de escrever, o NTFS:
- Escolhe um update sequence number (USN, sem relação com o USN do
$UsnJrnlapesar do acrónimo partilhado) de 16 bits. - Guarda os dois bytes finais originais de cada sector num array que vive logo a seguir ao cabeçalho.
- Substitui os dois bytes finais de cada sector pelo próprio USN.
Na leitura, o NTFS verifica que os dois bytes finais de cada sector de 512 bytes são iguais ao USN escolhido. Se forem, a escrita foi atómica; tira os bytes originais do array de fixup e mete-os de volta. Se a cauda de algum sector não corresponder, a escrita foi rasgada e o registo é suspeito.
O array é disposto como uma word USN seguida de N words de fixup, onde N é o número de sectores. Para um registo de 1.024 bytes num volume com sectores de 512 bytes, N = 2. O array ocupa por isso 6 bytes no total (USN, fixup_for_sector_0, fixup_for_sector_1). O seu offset é o valor de 2 bytes em 0x04 do registo (tipicamente 0x2A ou 0x30 conforme a versão NTFS).
A consequência prática: se ler chunks brutos de 1.024 bytes do $MFT sem aplicar fixups, todos os registos terão lixo nos offsets 510 e 1022. Um $DATA residente que atravesse esses offsets ficará corrompido. Parsers como MFTECmd, omerbenamram/mft, analyzeMFT e o fls/istat do Sleuth Kit aplicam fixups como primeiro passo. Se está a escrever o seu próprio parser (o que é um bom exercício; veja parse-mft em Python), faça isto antes de tudo o resto.
O fluxo de atributos
Depois do cabeçalho e do array de fixup, cada registo contém uma série de atributos tipados empacotados uns a seguir aos outros, com alinhamento de 8 bytes, terminados pelo valor sentinela 0xFFFFFFFF onde estaria o código de tipo do próximo atributo.
Cada atributo começa com um pequeno cabeçalho padronizado:
0x00 4 bytes Código de tipo do atributo (0x10 = $STANDARD_INFORMATION, etc.)
0x04 4 bytes Comprimento deste atributo (cabeçalho + dados)
0x08 1 byte Flag de não residente (0 = residente, 1 = não residente)
0x09 1 byte Comprimento do nome (em caracteres; 0 se sem nome)
0x0A 2 bytes Offset para o nome (em caracteres desde o início do atributo)
0x0C 2 bytes Flags (compresso/encriptado/sparse)
0x0E 2 bytes ID do atributo
Para atributos residentes, os campos seguintes são o comprimento e o offset do conteúdo; para não residentes são o VCN inicial/final, o offset da runlist, e os tamanhos comprimido/alocado/real. Depois de tudo isso, vêm os dados propriamente ditos. Os nomes (se houver) são em Unicode e não alinhados; espere uma implementação chata.
Um registo mínimo carrega três atributos:
$STANDARD_INFORMATION(0x10): carimbos temporais, flags DOS, ID de segurança.$FILE_NAME(0x30): nome, referência ao pai, um segundo conjunto de quatro carimbos temporais, tamanhos alocado/real desde que o nome foi definido. Os registos podem carregar muitos (um por hard link, mais o nome curto 8.3 em volumes onde a geração 8.3 está activada).$DATA(0x80): o conteúdo do ficheiro, residente se couber, runlist caso contrário. Os registos podem carregar múltiplos$DATA; o sem nome é o fluxo primário, os com nome são Alternate Data Streams.
Para o catálogo completo de atributos e onde vive cada um, veja a referência do Master File Table.
Porque isto importa quando os registos estão danificados
A combinação de uma disposição estável de 1.024 bytes, a assinatura FILE, o mecanismo de fixup, e os cabeçalhos de atributo auto-descritivos é o que torna possível carving de registos NTFS eliminados. Mesmo quando o próprio $MFT desapareceu (corrupção, garatujas de ransomware, wipe parcial), uma varredura por assinatura do volume bruto à procura de fronteiras 46 49 4C 45 recupera os registos desde que os clusters subjacentes não tenham sido sobrescritos. A verificação de fixup dá-lhe uma verificação de integridade por sector; registos que falhem nela devem ser sinalizados mas os seus dados de atributo às vezes ainda são parcialmente legíveis.
Esta é a base sobre a qual assentam ferramentas como o mmls+fls do Sleuth Kit, o "deep scan" do R-Studio, e as várias suites comerciais de recuperação. Todas percorrem a mesma disposição de bytes. Conhecê-la por si próprio é o que lhe permite validar as suas saídas.
Ler um à mão
Abra um $MFT extraído no 010 Editor com o template NTFS MFT Record aplicado. Escolha o registo 5 (o directório raiz; offset 5 * 1024 = 5120 desde o início do ficheiro). Confirme:
- Bytes 0x00 a 0x03:
46 49 4C 45. - Flags em 0x16:
0x03(IN_USE e DIRECTORY ambas ligadas). - Primeiro tipo de atributo no offset dado por 0x14:
0x10($STANDARD_INFORMATION).
Se isso bate certo, o registo foi analisado correctamente. Se não, ou o ficheiro está corrompido ou o array de fixup não foi aplicado (o template do 010 Editor aplica-o por si).
Uma vez que tenha feito isto uma vez, o resto do MFT torna-se legível. Os campos deixam de ser arcanos e passam a ser um registo que consegue ler sem ajuda.
Leituras adicionais
- Microsoft, Master File Table. A referência oficial, sucinta.
- Documentação NTFS do Sleuth Kit. As notas de Brian Carrier continuam a ser a fonte única mais clara para a disposição dos registos.
- Russon e Fledel, NTFS Documentation (projecto linux-ntfs). Offsets de campo para cada atributo, retirados directamente de anos de engenharia reversa.