← Volver al blog

El FILETIME de Windows explicado — convertir timestamps de NTFS en algo legible

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

4 min de lectura

Abre cualquier registro del journal USN, cualquier entrada MFT, cualquier rama del registro, y los timestamps que encuentras tienen todos la misma forma: un entero de 64 bits que no significa nada sin conversión. Ese formato es FILETIME, y una vez que le coges el truco, todos los demás formatos de tiempo de Windows tienen sentido.

Qué es FILETIME

Un FILETIME es un entero de 64 bits sin signo. Su valor es el número de intervalos de 100 nanosegundos desde el 1 de enero de 1601 a las 00:00:00 UTC. Eso es todo.

La elección de 1601 no es arbitraria — es el inicio del ciclo gregoriano de 400 años que contiene hoy, lo que mantiene consistente la aritmética calendario sin casos límite por años bisiestos. Microsoft documenta el tipo en la referencia de la estructura FILETIME.

Un ejemplo pequeño: el FILETIME 133593600000000000 corresponde a 2024-08-09 12:00:00 UTC.

Conversión a tiempo Unix

La conversión es de dos pasos aritméticos:

  1. Dividir por 10 000 000 para pasar de tics 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 (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:

unix_seconds = filetime / 10_000_000 - 11_644_473_600
# o 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:

// filetime es un BigInt para preservar la precisión de 64 bits
const unixMs = Number((filetime - 116444736000000000n) / 10000n);
const date = new Date(unixMs);

En SQL (Postgres):

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

Precisión sub-segundo

La resolución completa de 100 ns rara vez importa en forense — los timestamps en disco están cuantizados a lo que el SO decidió registrar, y $STANDARD_INFORMATION se actualiza por syscall, no por reloj. Pero para los casos en que importa (correlación con capturas de paquetes, por ejemplo), conservar el valor en tics 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")

La resolución de microsegundos del datetime de Python cubre todo excepto el último dígito de ticks_100ns. La mayoría de los analistas truncan a milisegundos sin perder precisión significativa.

Variantes con las que te toparás

Varios artefactos Windows usan codificaciones ligeramente distintas de la misma idea:

FormatoDónde apareceCodificación
FILETIME$MFT, $UsnJrnl, registro, EVTX64 bits LE, tics de 100 ns desde 1601-01-01 UTC
SYSTEMTIMEEVTX, algunas API COM8× campos de 16 bits (año, mes, día…)
TIME_T (32 bits)Claves antiguas del registroSegundos Unix desde 1970, 32 bits
DOSTIMEFAT (a veces se cuela en metadatos NTFS)Fecha 16 bits + hora 16 bits empaquetadas, en hora local

Al parsear una estructura binaria, el orden de bytes es little-endian — el LSB del FILETIME viene primero.

Trampas habituales

  • Endianness en dumps brutos. xxd muestra los bytes izquierda a derecha; los bytes de FILETIME necesitan ser invertidos antes de interpretarse. Las herramientas y parsers lo hacen automáticamente; el análisis manual suele requerir invertir el orden primero.
  • Valor cero. FILETIME = 0 es un valor real (1601-01-01) pero también un centinela para "nunca asignado". Tratarlo como null al mostrar.
  • Rarezas del complemento a dos. Algunas implementaciones (incluidas algunas internas de Windows) tratan FILETIME como int64 con signo, lo que hace que valores más allá de ~30828 d.C. envuelvan en negativo. En la duda, usar sin signo.
  • Local vs UTC. FILETIME está siempre en UTC. Si una herramienta te muestra hora local, ha convertido al salir. Para DFIR casi siempre quieres UTC para correlar entre hosts.
  • Timestamps de $STANDARD_INFORMATION vs $FILE_NAME. Los dos viven en $MFT, los dos son FILETIME, los dos registran tiempos M/A/C/B. Las copias de $FILE_NAME se actualizan menos a menudo, y por eso la detección de timestomping compara ambos. Las notas de Brian Carrier y el paper de Mandiant sobre timestomp (hoy por detrás de las notas de X-Ways) son las referencias estándar.

Una pequeña tabla de conversión para validar

Si escribes tu propio convertidor, estos valores son útiles para comprobarlo:

FILETIMEFecha UTC
01601-01-01 00:00:00
1164447360000000001970-01-01 00:00:00
1329235200000000002022-02-23 16:00:00
1335936000000000002024-08-09 12:00:00

Si tu convertidor produce esos resultados, produce el correcto.