Códigos de razão USN — ler nas entrelinhas dos bits

Passeio campo a campo pela bitmask Reason dos registos USN_RECORD e o que cada combinação revela sobre o ciclo de vida de um ficheiro em disco.

Por 6 min de leitura

Se passar algum tempo a ler journals USN, a bitmask Reason torna-se memória muscular como os EventIDs do EVTX. É o campo de 32 bits que o NTFS define em cada USN_RECORD_V2 para resumir o que acabou de mudar num ficheiro. Os bits são aditivos, não mutuamente exclusivos, e isso é a primeira coisa a interiorizar: um único registo pode carregar FileCreate | DataExtend | Close e essa combinação significa algo diferente de qualquer destes bits isolados.

O que se segue é a referência de campo que mantenho num post-it ao lado do monitor.

Os bits, pela ordem por que os vai encontrar

FlagHexO que significa na prática
DataOverwrite0x00000001Uma região do stream de dados principal foi sobrescrita no mesmo sítio
DataExtend0x00000002O stream de dados principal cresceu
DataTruncation0x00000004O stream de dados principal encolheu
NamedDataOverwrite / Extend / Truncation0x10 / 0x20 / 0x40Os mesmos três, mas num fluxo de dados alternativo
FileCreate0x00000100Foi criado um novo ficheiro ou directório
FileDelete0x00000200O ficheiro foi desligado do namespace
EaChange0x00000400Atributos estendidos foram alterados
SecurityChange0x00000800ACLs/owner foram alterados
RenameOldName0x00001000A metade «antes» de uma renomeação
RenameNewName0x00002000A metade «depois» de uma renomeação
IndexableChange0x00004000A flag indexable foi alternada
BasicInfoChange0x00008000Timestamps, atributos ou flags de compressão reescritos
HardLinkChange0x00010000Um hard link foi adicionado ou removido
CompressionChange0x00020000Compressão NTFS alternada
EncryptionChange0x00040000Estado EFS alterado
ObjectIdChange0x00080000Object ID definido ou limpo
ReparsePointChange0x00100000Reparse point adicionado/alterado/removido
StreamChange0x00200000Um fluxo de dados alternativo foi adicionado, renomeado ou eliminado
Close0x80000000O handle que produziu a mudança foi libertado

O bit Close merece um parágrafo à parte, porque interpretá-lo mal lança a sua contagem de eventos por uma ordem de magnitude. O NTFS coalesce operações sucessivas sob o mesmo handle e emite registos intermédios à medida que faz flush, depois um registo de resumo final com Close definido quando o handle desaparece. Os registos intermédios são reais, mas duplicam a contagem. Quando faz triagem a um journal parseado, filtre primeiro pelos registos com Close para desduplicar operações a uma linha por handle. Puxe os registos não-Close apenas quando precisa de granularidade sub-handle.

Padrões que ocorrem com frequência suficiente para memorizar

Algumas combinações de razões aparecem tão frequentemente em investigações que se deixa de lê-las carácter a carácter para se ler como palavras.

Ficheiro novo escrito por uma aplicação. FileCreate | DataExtend e depois mais registos DataExtend | Close à medida que o ficheiro cresce, terminados por BasicInfoChange | Close. O último registo é o ficheiro a ter o mtime carimbado no fecho.

Renomeação entre directorias. Dois registos a partilhar o mesmo FileReferenceNumber: RenameOldName | Close e depois RenameNewName | Close. A referência do pai difere entre os dois — esse delta é como reconstrói o movimento. Se só filtrar por um dos bits de rename perde metade da história.

Save-by-rename atómico. Office, VS Code, Notepad++, vim com backup, quase todos os editores modernos. Vê um FileCreate num ficheiro temporário (~$report.docx, report.docx.tmp, .swp, etc.), um ou mais registos DataExtend, um FileDelete | Close no original, depois RenameNewName | Close no temporário a apontar para o nome do original. A sequência completa acontece dentro de um único segundo de relógio.

Cifrar-depois-renomear de ransomware. Registos DataOverwrite sustentados a cobrir megabytes por ficheiro, seguidos de pares RenameOldName / RenameNewName com um sufixo uniforme ou extensão aleatória de comprimento fixo. A taxa de DataOverwrite e o cluster de mesma extensão nas renomeações são as duas metades do diagnóstico. O artigo de detecção de ransomware é o playbook.

Quarentena de antivírus. Um FileDelete | Close imediatamente a seguir a um FileCreate recente na mesma entrada MFT, muitas vezes com um SecurityChange no meio à medida que o agente AV move o ficheiro para um directório restrito. O journal preserva prova de que o AV tocou no ficheiro mesmo depois do binário em si ter desaparecido.

Upload por sincronização na cloud. RenameNewName | Close cujo novo pai é \Users\<u>\OneDrive\, \Dropbox\ ou \Google Drive\ e cujo pai antigo é o perfil do utilizador. O próprio agente produz registos DataExtend | Close em ficheiros dentro da pasta de sincronização para downloads — uploads tendem a parecer renomeações para dentro da pasta.

Timestomping. Um BasicInfoChange | Close isolado sem DataOverwrite, DataExtend, FileCreate ou rename em redor no mesmo FileReferenceNumber. Um touch legítimo é emparelhado com a escrita que despoletou a actualização do timestamp. O artigo sobre timestomping explora isto em pormenor.

Injecção em stream / abuso de ADS. StreamChange por si só geralmente indica um Zone.Identifier de um download de browser (emparelhado com o FileCreate do ficheiro original). Registos StreamChange repetidos no mesmo FileReferenceNumber longe de Downloads\, particularmente em binários de sistema assinados, merecem investigação.

O que os bits nunca lhe vão dizer

O campo Reason é sobre o ficheiro, não sobre o actor. Não há utilizador, nem PID, nem linha de comandos, nem nível de integridade. Anexar um actor a uma operação implica sempre ir a outro lado: Security.evtx 4663 (acesso a objecto, apenas se houvesse SACLs configurados, o que geralmente não havia), evento 11 do Sysmon (criação de ficheiro) e evento 1 (criação de processo com linha de comandos), ou evidência de execução de processo e entradas de Shimcache que põem um binário em disco no momento certo.

Os bits também não lhe dizem sobre conteúdo. O journal sabe que uma região foi sobrescrita, mas não que bytes lá estavam antes. Para isso precisa de before-images do $LogFile ou de uma Volume Shadow Copy.

Uma receita de leitura que funciona

Quando abro um journal parseado a frio:

  1. Filtre primeiro por FileCreate. O que é genuinamente novo na janela? Procure caminhos em directorias graváveis pelo utilizador (\AppData\Local\Temp\, \Users\Public\, \ProgramData\ fora de subdirectórios conhecidos de fabricante, \PerfLogs\).
  2. Passe a RenameNewName | Close e agrupe pela nova extensão ou pelo novo pai. Save-by-rename, movimentações por sincronização na cloud e sufixos de ransomware aparecem todos aqui.
  3. Agrupe DataExtend por minuto. Escritas em massa — backups, cifragem, crescimento de arquivos de exfiltração — aparecem como picos sustentados contra uma baseline de resto silenciosa.
  4. Leia primeiro os registos com Close. Trate os intermédios como evidência de apoio, não eventos independentes.
  5. Quando tiver um ficheiro candidato, agrupe todos os registos no mesmo FileReferenceNumber e leia o ciclo de vida completo por ordem cronológica. Essa sequência é quase sempre a parte que vai para o relatório.

O parser desta página expõe directamente o filtro por razão, pelo que os passos 1 a 3 são alguns cliques. Para o passo 5, ordene por FileReferenceNumber e depois por USN.

Leituras adicionais

  • Microsoft Learn — a referência USN_RECORD_V2 cobre cada definição de bit directamente da fonte.
  • MFTECmd do Eric Zimmerman — o seu parser de $J mostra a bitmask Reason num CSV plano que aguenta pivots em folha de cálculo.
  • usnrs da Airbus CERT — a implementação open-source em Rust; ler src/reason.rs é o caminho mais curto para interiorizar o layout dos bits.