← Voltar ao blog

Dentro de um registo MFT, byte a byte

· 7 min de leitura

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:

  1. Escolhe um update sequence number (USN, sem relação com o USN do $UsnJrnl apesar do acrónimo partilhado) de 16 bits.
  2. Guarda os dois bytes finais originais de cada sector num array que vive logo a seguir ao cabeçalho.
  3. 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

Recursos externos