USN Reason Codes — leer entre los bits

Un recorrido campo a campo por la máscara de bits Reason de USN_RECORD y lo que cada combinación dice sobre el ciclo de vida de un archivo en disco.

Por 6 min de lectura

Si pasas algo de tiempo leyendo USN journals, la máscara de bits Reason se convierte en memoria muscular como pasa con los EventIDs de EVTX. Es el campo de 32 bits que NTFS pone en cada USN_RECORD_V2 para resumir qué acaba de cambiar en un archivo. Los bits son aditivos, no mutuamente excluyentes, que es lo primero que hay que interiorizar: un único registro puede llevar FileCreate | DataExtend | Close y esa combinación significa algo distinto a cualquiera de esos bits por separado.

Lo que sigue es la referencia de campo que mantengo en un post-it junto al monitor.

Los bits, en el orden en que realmente los encontrarás

FlagHexLo que significa en la práctica
DataOverwrite0x00000001Se sobrescribió en sitio una región del flujo de datos principal
DataExtend0x00000002El flujo de datos principal creció
DataTruncation0x00000004El flujo de datos principal se encogió
NamedDataOverwrite / Extend / Truncation0x10 / 0x20 / 0x40Lo mismo, pero sobre un flujo de datos alternativo
FileCreate0x00000100Se creó un archivo o directorio nuevo
FileDelete0x00000200El archivo fue desvinculado del espacio de nombres
EaChange0x00000400Atributos extendidos cambiaron
SecurityChange0x00000800ACLs/propietario cambiaron
RenameOldName0x00001000La mitad "antes" de un renombrado
RenameNewName0x00002000La mitad "después" de un renombrado
IndexableChange0x00004000Flag indexable cambiado
BasicInfoChange0x00008000Timestamps, atributos o flags de compresión reescritos
HardLinkChange0x00010000Se añadió o eliminó un hardlink
CompressionChange0x00020000Compresión NTFS conmutada
EncryptionChange0x00040000Estado EFS cambió
ObjectIdChange0x00080000Object ID establecido o borrado
ReparsePointChange0x00100000Reparse point añadido/cambiado/eliminado
StreamChange0x00200000Se añadió, renombró o eliminó un flujo de datos alternativo
Close0x80000000El handle que produjo el cambio ha sido liberado

El bit Close merece un párrafo aparte porque leerlo mal puede desviar tu cuenta de eventos en un orden de magnitud. NTFS coalisiona operaciones sucesivas bajo el mismo handle y emite registros intermedios al hacer flush, luego un registro de resumen final con Close activado una vez el handle se ha ido. Los registros intermedios son reales, pero cuentan doble. Cuando triages un journal parseado, filtra primero por registros con Close para deduplicar operaciones a una fila por handle. Tira de los registros sin Close solo cuando necesites granularidad sub-handle.

Patrones que se repiten lo bastante como para memorizarlos

Un puñado de combinaciones de Reason aparecen con tanta frecuencia en investigaciones que dejas de leerlas carácter a carácter y empiezas a leerlas como palabras.

Archivo nuevo escrito por una app. FileCreate | DataExtend seguido de más registros DataExtend | Close a medida que el archivo crece, cerrado por BasicInfoChange | Close. El último registro es el archivo recibiendo el stamp de su mtime al cerrar.

Renombrado entre directorios. Dos registros que comparten el mismo FileReferenceNumber: RenameOldName | Close y luego RenameNewName | Close. La referencia padre difiere entre los dos; ese delta es como reconstruyes el movimiento. Si solo filtras por uno de los bits de rename pierdes la mitad de la historia.

Save-by-rename atómico. Office, VS Code, Notepad++, vim con backup, casi cualquier editor moderno. Ves un FileCreate sobre un archivo temporal (~$report.docx, report.docx.tmp, .swp, etc.), uno o más registros DataExtend, un FileDelete | Close sobre el original y luego RenameNewName | Close sobre el temporal apuntando al nombre del original. La secuencia completa sucede en un único segundo de reloj.

Cifrado-y-renombrado de ransomware. Registros sostenidos de DataOverwrite cubriendo megabytes por archivo, seguidos de pares RenameOldName / RenameNewName con un sufijo uniforme o una extensión aleatoria de longitud fija. La tasa de DataOverwrite y el cluster de misma extensión en los renombrados son las dos mitades del diagnóstico. El post de detección de ransomware es el playbook.

Cuarentena de antivirus. Un FileDelete | Close inmediatamente después de un FileCreate reciente sobre la misma entrada MFT, a menudo con un SecurityChange en medio mientras el agente AV mueve el archivo a un directorio restringido. El journal preserva la prueba de que el AV tocó el archivo incluso después de que el binario en sí haya desaparecido.

Subida a cloud-sync. RenameNewName | Close cuyo padre nuevo es \Users\<u>\OneDrive\, \Dropbox\ o \Google Drive\ y cuyo padre antiguo es el perfil del usuario. El propio agente produce registros DataExtend | Close sobre archivos dentro de la carpeta de sync para descargas; las subidas tienden a parecer renombrados hacia dentro de la carpeta.

Timestomping. Un BasicInfoChange | Close solo, sin DataOverwrite, DataExtend, FileCreate o renombrado alrededor en el mismo FileReferenceNumber. Un toque legítimo va acompañado de la escritura que disparó la actualización de timestamp. El post de timestomping es donde esto se trabaja con detalle.

Inyección de stream / abuso de ADS. StreamChange por sí solo suele indicar un Zone.Identifier por una descarga del navegador (emparejado con el FileCreate del archivo original). Registros StreamChange repetidos sobre el mismo FileReferenceNumber fuera de Downloads\, en particular sobre binarios firmados del sistema, merecen abrirse.

Lo que los bits no te dirán nunca

El campo Reason va sobre el archivo, no sobre el actor. No hay usuario, ni PID, ni línea de comandos, ni nivel de integridad. Atar un actor a una operación siempre significa ir a otro sitio: Security.evtx 4663 (object access, solo si las SACL fueron configuradas, lo cual normalmente no), Sysmon evento 11 (file create) y evento 1 (process create con línea de comandos), o evidencia de ejecución de procesos y entradas Shimcache que ponen un binario en disco en el momento justo.

Los bits tampoco te dicen del contenido. El journal sabe que una región fue sobrescrita, no qué bytes había allí antes. Para eso necesitas imágenes "antes" de $LogFile o una Volume Shadow Copy.

Una receta de lectura que aguanta

Cuando abro un journal parseado en frío:

  1. Filtro por FileCreate primero. ¿Qué es genuinamente nuevo en la ventana? Busco rutas en directorios escribibles por usuario (\AppData\Local\Temp\, \Users\Public\, \ProgramData\ fuera de subdirectorios conocidos de proveedor, \PerfLogs\).
  2. Cambio a RenameNewName | Close y agrupo por extensión nueva o padre nuevo. Save-by-rename, movimientos de cloud-sync y sufijos de ransomware emergen todos aquí.
  3. Bucketing de DataExtend por minuto. Escrituras masivas — backups, cifrado, crecimiento de archivos de exfiltración — aparecen como picos sostenidos contra una base por lo demás silenciosa.
  4. Leo primero los registros con Close. Trato los intermedios como evidencia de apoyo, no como eventos independientes.
  5. Una vez tengo un archivo candidato, agrupo cada registro con el mismo FileReferenceNumber y leo el ciclo de vida completo en orden cronológico. Esa secuencia es casi siempre la parte que entra en el informe.

El parser de esta página expone el filtrado por Reason directamente, así que los pasos 1-3 son unos pocos clics. Para el paso 5, ordena por FileReferenceNumber y luego por USN.

Lecturas adicionales

  • Microsoft Learn — la referencia USN_RECORD_V2 cubre cada definición de bit directamente desde la fuente.
  • MFTECmd de Eric Zimmerman — su parser de $J expone la máscara Reason en un CSV plano que aguanta pivotes en hoja de cálculo.
  • usnrs de Airbus CERT — la implementación open source en Rust; leer src/reason.rs es el camino más corto para interiorizar el layout de bits.