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:
- Dividir por 10 000 000 para pasar de tics de 100 ns a segundos.
- 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:
| Formato | Dónde aparece | Codificación |
|---|---|---|
FILETIME | $MFT, $UsnJrnl, registro, EVTX | 64 bits LE, tics de 100 ns desde 1601-01-01 UTC |
SYSTEMTIME | EVTX, algunas API COM | 8× campos de 16 bits (año, mes, día…) |
TIME_T (32 bits) | Claves antiguas del registro | Segundos Unix desde 1970, 32 bits |
DOSTIME | FAT (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.
xxdmuestra 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 = 0es 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
FILETIMEcomoint64con 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_INFORMATIONvs$FILE_NAME. Los dos viven en$MFT, los dos son FILETIME, los dos registran tiempos M/A/C/B. Las copias de$FILE_NAMEse 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:
| FILETIME | Fecha UTC |
|---|---|
0 | 1601-01-01 00:00:00 |
116444736000000000 | 1970-01-01 00:00:00 |
132923520000000000 | 2022-02-23 16:00:00 |
133593600000000000 | 2024-08-09 12:00:00 |
Si tu convertidor produce esos resultados, produce el correcto.