FILETIME de Windows explicado — convertir timestamps de NTFS a algo legible

FILETIME es el formato de timestamp de Windows que encontrarás en $MFT, $UsnJrnl, el registro, $Recycle.Bin y casi cualquier artefacto de Windows. Una referencia corta y completa: qué es, cómo convertirlo y las trampas.

Por 5 min de lectura

Abre cualquier registro USN, cualquier entrada MFT, cualquier hive del registro, cualquier archivo LNK, cualquier traza Prefetch, cualquier jump list, y los timestamps que encuentres son todos de la misma forma: un entero de 64 bits que se lee como un sinsentido sin conversión. Ese formato es FILETIME. Apréndelo una vez, lee cada artefacto de Windows el resto de tu carrera.

La definición

Un FILETIME es un entero sin signo de 64 bits que cuenta intervalos de 100 nanosegundos desde 1601-01-01 00:00:00 UTC. Esa es la especificación entera.

La elección de 1601 no es arbitraria: es el inicio del ciclo gregoriano de 400 años que contiene el presente, lo que permite a la aritmética de calendario saltarse los casos límite de años bisiestos que el tiempo Unix tiene que manejar. Microsoft documenta el tipo en la referencia de estructura FILETIME.

Un ejemplo trabajado: FILETIME 133593600000000000 es 2024-08-09 12:00:00 UTC. Verifica tu convertidor contra ese valor antes de confiar en él con datos de caso.

Conversión a tiempo Unix

Dos pasos aritméticos:

  1. Dividir entre 10.000.000 para pasar de ticks de 100 ns a segundos.
  2. Restar 11.644.473.600 — el número de segundos entre 1601-01-01 y 1970-01-01.

En pseudocódigo:

unix_seconds = filetime / 10_000_000 - 11_644_473_600

En Rust, que es exactamente lo que hace usnrs::Entry::unix_timestamp:

pub fn unix_timestamp(&self) -> i64 {
    (self.timestamp as i64) / 10_000_000 - 11_644_473_600
}

En Python, donde la biblioteca estándar maneja la aritmética de calendario por ti:

unix_seconds = filetime / 10_000_000 - 11_644_473_600

# con precisión sub-segundo:
from datetime import datetime, timezone, timedelta
EPOCH = datetime(1601, 1, 1, tzinfo=timezone.utc)
dt = EPOCH + timedelta(microseconds=filetime / 10)

En JavaScript, donde necesitas BigInt para preservar la precisión de 64 bits:

const unixMs = Number((filetime - 116444736000000000n) / 10000n);
const date = new Date(unixMs);

En SQL (Postgres), útil cuando tienes una columna de valores FILETIME en bruto volcados desde un export de Plaso:

SELECT TIMESTAMP '1601-01-01 00:00:00' + (filetime / 10000000) * INTERVAL '1 second';

Precisión sub-segundo

La resolución completa de 100 ns raramente importa para forense. Los timestamps de disco están cuantizados a lo que NTFS se molestó en registrar, y las actualizaciones de $STANDARD_INFORMATION están guiadas por syscalls más que por el reloj del sistema. Pero los casos donde sí importa — correlacionar un registro USN con una captura de paquetes, alinear un evento 11 de Sysmon contra un DataExtend — quieres mantener el valor en ticks de 100 ns hasta el último paso:

import datetime as dt
EPOCH = dt.datetime(1601, 1, 1, tzinfo=dt.timezone.utc)
ticks_100ns = 133593612345678901
microseconds = ticks_100ns // 10
nanoseconds_remainder = (ticks_100ns % 10) * 100
timestamp = EPOCH + dt.timedelta(microseconds=microseconds)
print(timestamp, f"+{nanoseconds_remainder}ns")

El datetime de Python cubre todo excepto el último dígito. La mayoría de analistas trunca a milisegundos sin perder precisión significativa.

Variantes con las que te toparás

Windows envía varias codificaciones de timestamp y filtran entre artefactos de formas que pillan a los analistas nuevos:

FormatoDonde apareceCodificación
FILETIME$MFT, $UsnJrnl, registro, EVTXLE de 64 bits, ticks de 100 ns desde 1601-01-01 UTC
SYSTEMTIMESalida renderizada EVTX, algunas APIs COM8 campos de 16 bits (año, mes, día, ...)
TIME_T (32-bit)Claves del registro más viejas, cabeceras de pagefileSegundos Unix desde 1970, 32 bits
DOSTIMEFAT (ocasionalmente se filtra a metadatos NTFS copiados desde FAT)Fecha 16 bits + hora 16 bits empaquetadas, hora local

En disco, FILETIME es little-endian: el LSB va primero. xxd muestra bytes de izquierda a derecha; invierte el orden de bytes antes de interpretar si lees a mano. Las herramientas lo manejan automáticamente.

Trampas que han costado tiempo real

Endianness en volcados en bruto. Los visores hex muestran en orden de memoria; la aritmética de conversión asume que tienes el valor entero. Hay que invertir los bytes primero.

El centinela cero. FILETIME = 0 es técnicamente 1601-01-01 00:00:00 UTC. En la práctica Windows lo usa para decir "nunca establecido". Trata el cero como null en la capa de presentación o publicarás informes que afirman que se accedió por última vez a un archivo en 1601. La misma trampa aplica a ciertos timestamps $FILE_NAME que NTFS deja sin establecer.

Signed vs unsigned. Algunos internos de Windows (y algunos parsers construidos sobre ellos) tratan FILETIME como int64 con signo. Los valores más allá de aproximadamente 30828 d.C. dan la vuelta a negativos, lo cual obviamente no importa en la práctica — pero un valor manipulado deliberadamente puede caer en ese rango y romper un convertidor con bugs. Quédate en unsigned.

Local vs UTC. FILETIME es siempre UTC. Si una herramienta muestra hora local, ha convertido en la salida. Para DFIR casi siempre quieres UTC en la capa de almacenamiento — convierte a zonas locales solo al renderizar para un stakeholder. La correlación entre hosts a través de zonas horarias se cae si no.

$STANDARD_INFORMATION vs $FILE_NAME. Ambos viven en cada entrada $MFT, ambos almacenan tiempos M/A/C/B en FILETIME. Los valores SI son escribibles desde espacio de usuario; las copias FN se actualizan menos a menudo y son mucho más difíciles de cambiar. Comparar los dos es la detección clásica de timestomping, y el post de timestomping detalla exactamente cómo el USN journal complementa esa comparación.

Tabla de verificación

Si escribes tu propio convertidor — y en algún momento todos lo hacen, porque el lenguaje en el que están atascados no maneja la conversión nativamente — estos son los valores contra los que pruebo:

FILETIMEFecha UTC
01601-01-01 00:00:00 (o null, por convención)
1164447360000000001970-01-01 00:00:00
1329235200000000002022-02-23 16:00:00
1335936000000000002024-08-09 12:00:00

Si tu convertidor produce esos cuatro, produce lo correcto para todo lo demás.

Lecturas adicionales

  • Microsoft Learn — estructura FILETIME y la función relacionada FileTimeToSystemTime.
  • libfwnt de Joachim Metz — la implementación C de referencia para la conversión FILETIME que casi cualquier parser libyal usa.
  • SANS DFIR — el cheat sheet de reglas de tiempo de Windows cubre cómo se actualizan los timestamps $STANDARD_INFORMATION y $FILE_NAME bajo distintas operaciones, que es el complemento práctico al formato FILETIME en sí.