Dynamic analysis: victim timestamps as seed anchors (run-4 outputs, 2026-10-12)
Question
recover needs a window for the key2 seed, which is the machine’s uptime in ns when the file was
processed. So far that window came from debugger captures. Can artifacts that a responder actually
has (file times, boot records, the operator log) place it instead? Run 4 left 21 .akira files on
the VM whose exact seeds were logged in-process, so every timestamp can be scored against the truth.
Method (read-only on the VM; no sample code ran)
- Probe 1 (
timestamps/probe1_timestamps.ps1, outputtimestamps/ts_probe.txt). PowerShell launched from x64dbg with no breakpoints of interest. The script was written into the process withWriteMemToAddressand saved withsavedata. It recorded:- 8 pairs of QPC and
GetSystemTimePreciseAsFileTime(plus interrupt time and tick count); - the timer resolution;
- the time zone and WMI
LastBootUpTime; - for every file in the samples folder: size, creation / last-write / last-access FILETIME, and the first 32 bytes;
- the operator log and its contents;
- System-log boot, time-change and power events;
- USN journal lines.
- 8 pairs of QPC and
- Probe 2 (
timestamps/probe2_boot_anchors.ps1, outputtimestamps/ts_probe2.txt): the raw XML of Kernel-General event 12, and a search for the Circular Kernel Context Logger traces (BootCKCL.etl,ShutdownCKCL.etl). - Both output files were pulled to the host with a Ghidra raw import +
read_memory. - Analysis (
timestamps/analyze_timestamps.py, outputtimestamps/analysis_output.txt):- Each
.akirafile is matched to its seed row by KCipher-2 over its first 16 bytes. Known plaintext: zeros for the test files, and the spdlog prefix[2026-10-11 16:0for the pre-existing run-3 log file. - Then FILETIME = QPC + offset is evaluated, where the offset is the FILETIME of QPC 0.
- Each
Units: QPC runs at 10 MHz, so QPC ticks and FILETIME share the 100 ns unit, and seed = QPC × 100.
Results
1. The QPC offset measured on the live VM
- Seven clean pairs (the first call is slowed by JIT and was excluded) agree within 121 ticks (12 µs). The offset is 134,362,008,901,479,792, i.e. QPC 0 = 2026-10-11T14:01:30.148Z.
- The offset measured ~10 h after run 4 still fits the run-4 data to about 1 ms (section 2). So the system clock did not step and drifted at most ~1 ms between the run and the probe.
2. .akira last-write time vs the logged seeds (21 of 21 files matched to one seed row)
| Difference (live offset) | Min | Median | Max | |—|—|—|—| | mtime − key1 | +1.28 ms | +2.34 ms | +6.28 ms | | mtime − key2 | +0.45 ms | +1.39 ms | +5.36 ms | | mtime − iv2 | +0.06 ms | +0.98 ms | +3.82 ms | | mtime − per-file coroutine end | −1.43 ms | −0.53 ms | −0.12 ms |
- The footer write sets the last-write time, just before the per-file coroutine returns. The sample overwrites files in place, so the creation time remains the original file’s and carries no information.
- Clock tick: the mtimes advance in ~1 ms steps, so NTFS took the coarse system time with a 1 ms clock tick (timer resolution at probe time: current 1 ms, minimum 15.625 ms). On a host left at the default 15.6 ms tick, the mtime can lag the write by up to one tick. That is inferred, not measured here.
- File sizes: all test files were 12 KB, so the key2 → footer time is mostly seeding plus bookkeeping. For large files the encryption time adds to mtime − key2. That was not measured.
3. Other anchors from the same run
| Artifact | Relation found | Use |
|—|—|—|
| akira_readme.txt mtime | 1.3 ms before the folder’s first key1 seed | marks the start of a folder’s processing |
| Operator log name Log-11-10-2026-23-26-18.txt | local start time to the second (main start by QPC: 22:26:18.250Z) | coarse run start |
| Operator log creation time | 3.6 ms before the main-start QPC read | precise run start in system time |
| Operator log last-write time | 0.03 ms before the main-end QPC read | precise run end in system time |
| Last log line 423038 ms | equals (main-end − main-start) QPC / 10⁴, truncated | confirms the sample’s own timer is QPC |
The log fixes the run’s start and end in system time, not in QPC, so it does not remove the need for the offset.
4. Boot-time sources a responder has after a reboot
| Source | Value | Error vs the QPC offset |
|—|—|—|
| WMI LastBootUpTime | 14:01:30.500Z | +352 ms |
| Kernel-General 12 StartTime (event XML) | 14:01:30.5000000Z | +352 ms (KeBootTime, kept at 0.5 s granularity) |
| Kernel-General 12 TimeCreated | 14:01:31.645Z | +1,497 ms |
| EventLog 6005 TimeCreated | 14:01:39.768Z | +9,620 ms |
| BootCKCL.etl / ShutdownCKCL.etl | not present on this VM (WDI\LogFiles missing) | untested |
So after a reboot the standard sources fix the offset only to a few hundred ms. The error is one constant per boot, though, and every file encrypted in that boot shares it.
Blind recovery test
recover --qpc-offset <live offset> --mtime <probe mtime> with the default window (8 ms back, 1 ms
forward) and default --gap 3800:8000 used no seed values. Results:
artifacts/timestamp-blind-recover-2026-10-12.txt.
| File | key2 candidates | KCipher-2 pairs | Result |
|---|---|---|---|
gap_00.txt.akira (Spot; its key2 → iv2 gap is the 7,404-tick outlier) |
90,001 | 378,094,201 | true key2/iv2 pair found after 1,704 s (28 min); decrypted to all zeros |
gap_db_0.sqlite.akira |
– | – | stopped at the user’s request seconds after it started: it competed with the VM for CPU, and gap_00 already shows the method |
So the seeds were found from victim-side data alone: the file’s last-write time and the QPC offset measured on the same boot.
On speed: the window is split into 1,407 slices of 64 key2 seeds, taken roughly in order by 20 workers, and the search stops at the first hit. The true key2 lay in slice 957, 68% of the way through, so ~2.6·10⁸ pairs were tested in 1,704 s. That is ~150k pairs/s, as the cost tables assume; a full sweep of this window takes ~42 min.
What this means for recovery
| Situation | key2 window for the first file | Pairs at gap span 4,200 | Time at ~150k pairs/s |
|—|—|—|—|
| Victim still on the same boot (or a memory image): offset measured live, 1 ms tick | 9 ms (−8/+1) | 3.8·10⁸ | ~42 min (~14 min with --gap 3800:5200) |
| Same, host at the default 15.6 ms tick | ~25 ms | 1.1·10⁹ | ~2 h |
| Rebooted: offset from KeBootTime (LastBootUpTime / event 12 StartTime) | ~±0.6 s | ~5·10¹⁰ | ~4 days (one file only) |
Procedure:
- Before any reboot, record QPC and precise system time on the victim. A memory image serves the
same purpose, since it holds both clocks in
KUSER_SHARED_DATA. - Copy the
.akirafiles with their timestamps preserved, or record each file’s LastWriteTime. - Start with the smallest files. Their mtime sits closest to key2.
- After the first file is solved, its seeds pin the offset to about ±2.5 ms even if the boot source was coarse. Every other file of that boot then costs the same as the live case.
Untested:
- the 15.6 ms tick case;
- large files, where encryption time widens mtime − key2;
- whether some trace file kept from the attack boot (for example a
ShutdownCKCL.etl) records an exact QPC↔system-time pair.
Artifacts
04-dynamic/timestamps/: probe scripts, raw probe outputs,analyze_timestamps.py,analysis_output.txt.artifacts/timestamp-blind-recover-2026-10-12.txt: blindrecoverruns.06-decryptor/akira_decrypt.py:recover --qpc-offset / --mtime / --mtime-window.