"""Run-4 timestamp study: can victim-side timestamps locate the QPC seeds?

Inputs (all in this case folder):
  04-dynamic/timestamps/ts_probe.txt   read-only probe output from the VM (2026-10-12)
  04-dynamic/seed-gap/seed_gap_files.csv / qpc_log.csv   seeds logged in-process in run 4
Every FILETIME and QPC value is in 100 ns units (QPC frequency 10 MHz), so
FILETIME = QPC + offset, where offset is the FILETIME of QPC = 0.
"""
import csv, datetime, os, statistics, sys

HERE = os.path.dirname(os.path.abspath(__file__))
CASE = os.path.dirname(os.path.dirname(HERE))
sys.path.insert(0, os.path.join(CASE, "06-decryptor"))
import akira_decrypt as ad

def ft2s(ft):
    return (datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=ft // 10)).isoformat() + "Z"

def ms(t):
    return "%+.3f ms" % (t / 10000.0)

probe = [l.rstrip("\r\n") for l in open(os.path.join(HERE, "ts_probe.txt"), encoding="ascii", errors="replace")]
pairs = [list(map(int, l.split(",")[1:])) for l in probe if l.startswith("PAIR,")]
files = {}
for l in probe:
    if l.startswith("FILE,"):
        p = l.split(",")
        files[p[1]] = dict(size=int(p[2]), ctime=int(p[3]), mtime=int(p[4]), atime=int(p[5]), head=bytes.fromhex(p[6]))
boot_wmi = int([l for l in probe if l.startswith("LASTBOOT_WMI,")][0].split(",")[1])
evt = [l.split(",") for l in probe if l.startswith("EVT,")]
kg12 = max(int(e[3]) for e in evt if e[1] == "Microsoft-Windows-Kernel-General" and e[2] == "12")
el6005 = max(int(e[3]) for e in evt if e[1] == "EventLog" and e[2] == "6005")

print("== 1. QPC -> FILETIME offset at probe time ==")
offs = []
for q0, ft, coarse, q1, it, ub, tc in pairs:
    offs.append(ft - (q0 + q1) // 2)
    print("  pair: QPC %d..%d (read %d ticks)  precise FT %s  offset %d  interrupt-time - QPC %d" % (q0, q1, q1 - q0, ft2s(ft), ft - (q0 + q1) // 2, it - q0))
off_now = int(statistics.median(offs))
print("  offset (median) %d = boot by QPC at %s; spread %d ticks over all pairs, %d without the first (slow) call" % (
    off_now, ft2s(off_now), max(offs) - min(offs), max(offs[1:]) - min(offs[1:])))
print("  boot anchors a responder can read after a reboot:")
for label, ft in (("WMI LastBootUpTime", boot_wmi), ("Kernel-General 12 TimeCreated", kg12), ("EventLog 6005 TimeCreated", el6005)):
    print("    %-32s %s  %s vs QPC offset" % (label, ft2s(ft), ms(ft - off_now)))
import re
p2 = [l.rstrip("\r\n") for l in open(os.path.join(HERE, "ts_probe2.txt"), encoding="ascii")]
st = re.search(r"Name='StartTime'>([^<]+)<", [l for l in p2 if l.startswith("KG12XML,")][0]).group(1)
print("    %-32s %s  %s vs QPC offset (KeBootTime, 0.5 s granularity; same value WMI reports)" % (
    "Kernel-General 12 StartTime", st, ms(ad._parse_filetime(st) - off_now)))
print("  CKCL boot/shutdown traces on this VM: %s" % [l for l in p2 if l.startswith("ETL,ERR")][0].split(",", 3)[3])

print("\n== 2. Map every run-4 output to its logged seeds (KCipher-2 over the first 16 bytes) ==")
rows = list(csv.DictReader(open(os.path.join(CASE, "04-dynamic", "seed-gap", "seed_gap_files.csv"))))
for r in rows:
    k = ad.keys_from_seeds([r["key1_seed"], r["iv1_seed"], r["key2_seed"], r["iv2_seed"]])
    r["ks"] = ad.KCipher2(k[2], k[3]).keystream(16)
known = {n: b"\0" * 16 for n in files if n.startswith("gap_")}
known["Log-11-10-2026-16-08-27.txt.akira"] = b"[2026-10-11 16:0"      # spdlog line prefix of the run-3 log
mapping = []
for name, plain in sorted(known.items()):
    head = files[name]["head"][:16]
    hits = [r for r in rows if bytes(a ^ b for a, b in zip(head, r["ks"])) == plain]
    if len(hits) != 1:
        print("  %-36s %d matches" % (name, len(hits))); continue
    mapping.append((name, hits[0]))
print("  mapped %d of %d files to exactly one seed row" % (len(mapping), len(known)))

print("\n== 3. File timestamps vs the seeds (offset fixed at the probe-time value) ==")
print("  %-36s %-6s %14s %14s %14s %14s" % ("file", "tid", "mtime-key1", "mtime-key2", "mtime-iv2", "mtime-fileend"))
d_k2, d_end, d_k1 = [], [], []
for name, r in sorted(mapping, key=lambda x: int(x[1]["key2_seed"])):
    mt = files[name]["mtime"]
    k1, k2, v2, end = int(r["key1_seed"]) // 100, int(r["key2_seed"]) // 100, int(r["iv2_seed"]) // 100, int(r["file_end_qpc"])
    d_k1.append(mt - (k1 + off_now)); d_k2.append(mt - (k2 + off_now)); d_end.append(mt - (end + off_now))
    print("  %-36s %-6s %14s %14s %14s %14s" % (name, r["tid"], ms(d_k1[-1]), ms(d_k2[-1]), ms(mt - (v2 + off_now)), ms(d_end[-1])))
for label, d in (("mtime - key2", d_k2), ("mtime - file end", d_end), ("mtime - key1", d_k1)):
    print("  %-17s min %s  median %s  max %s  (n=%d)" % (label, ms(min(d)), ms(statistics.median(d)), ms(max(d)), len(d)))
print("  creation time differs from mtime on every .akira: %s (creation time is the original file's; the sample overwrites in place)" %
      all(files[n]["ctime"] != files[n]["mtime"] for n, _ in mapping))
mts = sorted(set(files[n]["mtime"] for n, _ in mapping))
print("  distinct mtimes %d for %d files; steps between them (ms): %s" % (len(mts), len(mapping), " ".join("%.3f" % ((b - a) / 1e4) for a, b in zip(mts, mts[1:]))))

print("\n== 4. Other run-4 anchors ==")
q = {r["caller_role"]: int(r["qpc_ticks"]) for r in csv.DictReader(open(os.path.join(CASE, "04-dynamic", "seed-gap", "qpc_log.csv"))) if r["caller_role"] in ("main-start", "main-end")}
log = files["Log-11-10-2026-23-26-18.txt"]
print("  operator log creation - main-start QPC: %s" % ms(log["ctime"] - (q["main-start"] + off_now)))
print("  operator log name 23:26:18 local (UTC+1) -> %s; main-start by QPC %s" % ("2026-10-11T22:26:18Z", ft2s(q["main-start"] + off_now)))
if "main-end" in q:
    print("  operator log mtime - main-end QPC: %s" % ms(log["mtime"] - (q["main-end"] + off_now)))
print("  akira_readme.txt mtime - first key1: %s" % ms(files["akira_readme.txt"]["mtime"] - (min(int(r["key1_seed"]) for r in rows) // 100 + off_now)))

print("\n== 5. What a blind key2 estimate would have cost ==")
spread = max(d_k2) - min(d_k2)
print("  using key2 ~= mtime - offset - median(mtime-key2): residuals span %s" % ms(spread))
for name, anchor in (("QPC offset measured on the live victim", off_now), ("WMI LastBootUpTime", boot_wmi), ("Kernel-General 12 TimeCreated", kg12)):
    err = [(files[n]["mtime"] - anchor - statistics.median(d_k2)) - int(r["key2_seed"]) // 100 for n, r in mapping]
    print("  boot anchor %-40s key2 error min %s max %s" % (name, ms(min(err)), ms(max(err))))
