Breaking Akira's Clock: Recovering Files Without the Ransom Key
An Akira-branded Windows encryptor builds every file key from the machine’s uptime. We take it apart function by function, show how a responder can turn that into decrypted files, prove it on real documents, and spell out exactly when it will not work.
📦 Decryptor, detections and the full case files are in the Source repository. Samples use password
infected.
Key findings
- The encryptor seeds a fresh Yarrow-256 generator for every key with one thing only: the machine’s uptime counter, written as a decimal string.
- That turns “guess a 128-bit key” into “guess when the file was encrypted, to a few milliseconds”. The operator’s RSA key is never needed.
- In a blind test on a freshly reverted VM, we recovered all four real files (a PDF, a DOCX and two PNG screenshots) byte-for-byte, SHA-256 identical, using only what an incident responder would have. Three took about 30 minutes each; one timing outlier needed a second, 21-minute pass.
- A second, fully independent run repeated the test on files the analyst picked and we had not seen, with every recovery rule fixed before encryption. Six of seven in-scope files came back SHA-256 identical in about 25 seconds each. For the seventh, a 482 KB PDF, the frozen tool found its main keys but not the key for its ChaCha8-encrypted middle.
- A second build (
988776358d0e…) generates its keys the same way. The unchanged decryptor recovered six of its seven in-scope files hash-exact, again in about 25 seconds each, and stopped on a large PDF at the same point. - Both PDFs are now recovered hash-exact. Their keys were inside the search window all along; the generic check simply cannot recognise compressed data. A PDF-aware check, added to the decryptor after the runs, closes the gap: 7 of 7 in-scope files on each build are recoverable. The blind scores stay 6 of 7, because the frozen tool did not have it.
- It works best on a machine that has not been rebooted, or from a memory image. After a reboot, the first file becomes a multi-day search.
How this page is organised. Part I (sections 01–08) is the story: what the sample does, where its cryptography breaks, and how we got real files back. Part II (sections 09–18) is the full reverse engineering behind it: every function, structure, constant and measurement, with the addresses to check them yourself. Sections 19–21 hold the indicators, detections and downloads.
Part I · The story
01 · A self-contained C++ encryptor that calls itself Akira
The binary is a 64-bit Windows executable written in modern C++ (C++20 coroutines on the asio I/O library), with the Nettle crypto library compiled in. It brands itself Akira: .akira extension, akira_readme.txt notes and Akira onion addresses. This build has no command-and-control, no persistence and no data theft. It encrypts and leaves. The data theft its note claims is not done by this file.
| Property | Value |
|---|---|
| SHA-256 | 101bd45ab673fd6547386dff4339f1649e61b9f5c318260d365f766fbdc2d3c9 |
| MD5 | 427eba9f6ff2cd312c72770f13e279c1 |
| Type | PE32+ x86-64, GUI subsystem |
| Compiled | 2023-12-28 14:49 UTC |
| Toolchain | MSVC 14.37, C++20, asio, Nettle |
| Ciphers | ChaCha8 + KCipher-2, RSA-4096 key wrap |
Every finding in this post comes from our own static analysis in Ghidra and controlled detonations under x64dbg in an isolated VM: six runs of this build and one of a second build. Each cryptographic primitive was re-implemented in Python and matched against data captured from the running sample.
02 · From launch to .akira in six moves
- Reads its targets from
--encryption_path=, a share list (--share_file=), or, with neither, a sweep of the first five drive letters.-localonlyskips network shares. A bare-pis silently ignored, which turns a targeted run into a whole-machine run. - Opens an operator log named
Log-DD-MM-YYYY-HH-MM-SS.txtin its working directory. - Deletes Volume Shadow Copies by asking WMI to start a hidden
powershell.exe -Command "Get-WmiObject Win32_Shadowcopy | Remove-WmiObject". The PowerShell process appears as a child ofWmiPrvSE.exe, not of the ransomware. - Frees locked files with the Restart Manager, force-closing whatever holds them while sparing a built-in list of 20 system processes.
- Drops
akira_readme.txtin each folder it enters, then hands files to a pool of encryptor threads (two on our 2-CPU VM). - Encrypts each file in place, appends a 512-byte RSA footer and renames it to
name.akira. A temporaryname.arikajournal lets an interrupted file be resumed and is deleted afterwards.
main, then every file goes through the same six-step coroutine. Section 10 walks the left column and section 14 the right, function by function.03 · Strong ciphers, fed by a stopwatch
Each file gets four fresh 16-byte values, generated back to back: key1, iv1, key2 and iv2. Each value comes from its own new random generator, and every generator is seeded the same way.
| Stage | What | Detail |
|---|---|---|
| Seed ⚠️ | Uptime counter × 100 | QueryPerformanceCounter at 10 MHz, as decimal text, e.g. 870401719600. The only input. |
| Generator | Fresh Yarrow-256 | Nettle’s Yarrow: SHA-256 pools, AES-256 output. New instance per value. |
| Keys | First 16 bytes | key1, iv1, key2, iv2 in turn, about 0.4 ms apart. |
| Ciphers | KCipher-2 + ChaCha8 | key2/iv2 drive KCipher-2; key1/iv1 drive an 8-round ChaCha with a 128-bit key. |
| Wrap | RSA-4096 footer | A 91-byte blob (mode, percent, size, all four values) sealed into 512 bytes. |
Key generation, per file. Everything below the first row is sound cryptography. The first row decides everything.
FUN_140036740. Right: the file pipeline in FUN_1400ae840. Section 15 has the cipher-level detail.Three modes, chosen by extension and size
The sample rarely encrypts a whole file. It reads the file in 0xFFFF-byte chunks. The first and last chunk of each encrypted region use KCipher-2, and the chunks in between use ChaCha8.
04 · A 128-bit key with a few thousand possible values
The uptime counter ticks ten million times a second. If you know when a file was encrypted to within one second, there are only 107 possible seeds for each key, and every seed gives exactly one key. If you know it to within a few milliseconds, there are tens of thousands.
The four values of a file are also generated in a fixed order and very close together. On our test VM, consecutive seeds were about 0.4 ms apart (median 4,121 ticks between key2 and iv2; 20 of 21 files under 8,000). So once you have one value, the next is never far away. Section 17 shows how we measured this without disturbing the sample.
Why the RSA layer does not help the attacker here
The RSA-4096 footer protects the keys from anyone without the operator’s private key. But we never need to open it. We rebuild the keys from the same uptime values the sample used, so the strong outer layer is simply bypassed.
05 · Decryption in plain English
Guess the uptime at the moment a file was encrypted and you can rebuild that file’s keys. Many files only need key2 and iv2: at the default 50%, a Spot-mode file under about 250 KB is encrypted in at most two chunks, and both use KCipher-2.
- Narrow down when the file was encrypted. On the still-running victim machine, read its clock and its uptime counter at the same moment to learn how one converts to the other. The encrypted file’s “last modified” time then gives its uptime at encryption to within a few milliseconds.
- Guess the key. Try every uptime value in that window, about 90,000 candidates for a 9 ms window. Each candidate rebuilds a key exactly as the ransomware did.
- Guess the companion value. It was made about 0.4 ms after the key, so for each key candidate try the next few thousand uptime values. That is roughly 380 million pairs in total.
- Check each guess. Every PDF starts with
%PDF-1., every PNG with the same 16 bytes. Decrypt the first few bytes with each pair; the pair that produces the right header is the right one. - Decrypt the file. Replay the sample’s own chunk plan with the recovered keystream, strip the 512-byte footer, and the original is back byte for byte.
recover needs no key material at all; section 18 explains its two search stages and the PDF fallback.Steps 2–4 are the slow part. In pure Python they took about 30 minutes per file on our 20-thread analysis host. With the optional compiled engine (pip install numpy numba) the same search takes about 20 seconds. In the decryptor this is one command:
python akira_decrypt.py recover report.pdf.akira \
--qpc-offset 134362707471549158 \
--mtime 134362716175578720 \
--known-hex 255044462d312e \
--out report.pdf
--qpc-offset is the clock-to-uptime offset from step 1 and --mtime the file’s last-write time, both as Windows FILETIME values. --known-hex here is %PDF-1.. The values shown are from our run-5 PDF.
If the first pass misses. Occasionally the companion value comes much later than usual (section 06). Run a second pass anchored on the companion instead. On all eighteen files whose keys we have found so far, across both builds, it landed 0.5–1.9 ms before the file’s timestamp, so you can afford to try much larger gaps:
python akira_decrypt.py recover photo.png.akira \
--qpc-offset 134362707471549158 \
--mtime 134362716175680085 \
--anchor iv2 --mtime-window 2.5:0.1 --gap 8001:30000 \
--out photo.png
Large PDFs need nothing extra. When the generic ChaCha8 check finds nothing in a .pdf, recover retries the same window with two PDF-specific checks (section 18). That took 2–6 minutes per file in pure Python.
06 · Real documents recovered blind, in three runs across two builds
Run 5. For the first test we reverted the VM, placed four real files in a folder, and let the sample encrypt them with no hooks and no breakpoints in its crypto path. Two safety gates stayed in place: the shadow-copy deletion was skipped, and the target folder was confirmed before anything ran. Afterwards we used only what a responder would collect:
- the
.akirafiles and their last-write times; - one clock reading (system time and uptime counter together) taken on the same boot, after the attack;
- known bytes from the file type, or from the file itself: the standard PNG header,
%PDF-1., and for the DOCX the first ZIP header rebuilt from its central directory, which Spot mode leaves unencrypted.
| File | Size | Known bytes | Search time (pure Python) | Re-run, compiled engine | Result |
|---|---|---|---|---|---|
1.pdf |
97,577 B | 7 (%PDF-1.) |
31 min | 21 s | ✅ SHA-256 identical |
QC-STC-T-45-26_Sample_IR_Deliverables_v1.0.docx |
42,333 B | 28 (ZIP header) | 28 min | 26 s | ✅ SHA-256 identical |
Screenshot 2026-10-05 102304.png |
117,188 B | 16 (PNG header) | 30 min | 22 s | ✅ SHA-256 identical |
Screenshot 2026-09-29 125745.png |
47,803 B | 16 (PNG header) | 81 min missed, then 21 min (second pass) | 13 s missed, then 16 s | ✅ SHA-256 identical |
The fourth file: a timing outlier. The first pass searched the whole span from 2 ms before the ransom note to just after the file’s own timestamp, allowing any gap up to 8,000 ticks between the two seeds, and found nothing in 81 minutes. Its seeds turned out to be 26,718 ticks (2.67 ms) apart, almost seven times the usual 0.4 ms. That fits the one outlier we measured earlier (27,109 ticks), on a VM that had been up for only 15 minutes. The second pass searched from the companion value, which still sat 0.93 ms before the timestamp, and allowed gaps from 8,001 to 30,000 ticks. It found the keys in 21 minutes, and the decrypted screenshot was SHA-256 identical to the original.
Run 6: a fully independent re-test
Run 5 had a weakness: we designed the second pass while solving its fourth file. So we ran the whole test again, with three changes:
- Rules fixed in advance. Before anything was encrypted, we recorded the SHA-256 of the decryptor, the search engine and the recovery script, together with the rules they apply: the default pass, an automatic second pass if it misses, and which known bytes to use for each file type. The run log prints the same hashes, so nothing could be tuned afterwards.
- Files we had not seen. After a full VM revert, the analyst chose eight files and placed them in the target folder. The hashes of the originals were sealed before encryption and read only after every file had been processed.
- Nothing in the crypto path. As in run 5, the same two safety gates were applied, then every breakpoint was removed. One clock reading after the attack gave an offset whose seven samples agreed within 7 µs.
| File | Size | Known bytes | Pass | Search time | key2→iv2 gap | Result |
|---|---|---|---|---|---|---|
20358696.jpg |
24,287 B | 11 (JFIF header) | 1 | 14 s | 4,173 | ✅ SHA-256 identical |
photo_2026-09-23_15-02-25.jpg |
172,871 B | 11 (JFIF header) | 1 | 26 s | 4,072 | ✅ SHA-256 identical |
photo_2026-09-24_12-44-15.jpg |
214,866 B | 11 (JFIF header) | 1 | 26 s | 4,139 | ✅ SHA-256 identical |
QC-STC-T-45-26_Sample_IR_Deliverables_v1.0.docx |
42,333 B | 28 (ZIP header) | 1 | 26 s | 3,933 | ✅ SHA-256 identical |
Screenshot 2026-09-10 152951.png |
26,953 B | 16 (PNG header) | 1 | 26 s | 3,993 | ✅ SHA-256 identical |
Screenshot 2026-09-29 125745.png |
47,803 B | 16 (PNG header) | 1 | 24 s | 5,570 | ✅ SHA-256 identical |
Autopsy_DFIR_Validation_Playbook_Extended.pdf |
481,630 B | 7 (%PDF-1.) |
1 | 22 s, then 2 min | 4,141 | ⚠️ Keys found, middle not decrypted (recovered afterwards) |
Screen Recording 2026-09-24 104237.mp4 |
6,799,834 B | none | – | – | – | Out of scope |
What run 6 shows.
- Six of seven in-scope files are hash-exact. All six were found by the default pass in about 25 seconds each, against roughly 30 minutes per file in run 5. That covers every file without ChaCha8 chunks.
- No timing outlier this time. The widest seed gap was 5,570 ticks, so the automatic second pass never ran.
- The timestamp anchor held. The companion value sat 0.55–1.86 ms before each file’s last-write time.
- The JPEG rule worked first time. This run tested it for the first time, and all three JPEGs recovered.
- Two inputs repeat run 5: the DOCX and one screenshot. They were encrypted with fresh keys, and the search uses nothing from a file except its type header, so they still count.
The PDF: a limitation we predicted
The PDF is over 262 KB, so the middle of its encrypted half is ChaCha8, not KCipher-2. We found its KCipher-2 keys in 22 seconds, so the first and last 64 KB of the encrypted half decrypted correctly. The ChaCha8 search tested 24 million candidate pairs. It accepts a key only if the output looks like zeros or plain text, and nothing passed. We flagged this risk in writing before the run. The MP4 was out of scope because no fixed header rule exists for it yet.
A second build, same flaw
A different Akira-branded build, SHA-256 988776358d0e45a4907dc1f4906a916f1b3595a31fa44d8e04e563a32557eb42, was the next test. It is the same C++ codebase recompiled with new string-obfuscation constants, a new victim chat code and its own operator RSA key. We read its code before running it:
- Same random source. It imports no secure random API. Each key and IV still comes from a fresh Yarrow-256 seeded with nothing but the uptime counter, written as a decimal string (
FUN_140082c90in this build). - Same key set and footer. It draws the same four values in the same order (
FUN_140036730), packs them into the same 91-byte blob, and wraps that in the same 512-byte RSA footer. - Same file handling. It keeps the same ciphers, the same 2,000,000-byte Part-mode threshold and the same command-line options.
- Different hiding. Its shadow-copy command decodes with different constants,
((0x32 − b)·0x2A) mod 0x7F, but it is the same command.
We then repeated run 6 exactly:
- a fresh revert;
- the analyst’s files;
- the original hashes sealed before encryption;
- the same two safety gates, located in this build’s own code (the WMI executor
FUN_140078890forced to return, the root queueFUN_14007ac00checked); - no breakpoints during encryption.
Recovery used build 1’s decryptor byte-for-byte unchanged.
| File | Size | Known bytes | Pass | Search time | key2→iv2 gap | Result |
|---|---|---|---|---|---|---|
20358696.jpg |
24,287 B | 11 (JFIF header) | 1 | 15 s | 4,900 | ✅ SHA-256 identical |
photo_2026-09-23_15-02-25.jpg |
172,871 B | 11 (JFIF header) | 1 | 26 s | 5,380 | ✅ SHA-256 identical |
photo_2026-09-24_12-44-15.jpg |
214,866 B | 11 (JFIF header) | 1 | 30 s | 3,896 | ✅ SHA-256 identical |
QC-STC-T-45-26_Sample_IR_Deliverables_v1.0.docx |
42,333 B | 28 (ZIP header) | 1 | 26 s | 4,225 | ✅ SHA-256 identical |
Screenshot 2026-09-10 152951.png |
26,953 B | 16 (PNG header) | 1 | 28 s | 4,306 | ✅ SHA-256 identical |
Screenshot 2026-09-29 125745.png |
47,803 B | 16 (PNG header) | 1 | 22 s | 4,013 | ✅ SHA-256 identical |
Detection Lab Automated BAS-to-Splunk Detection Engineering Loop.pdf |
595,456 B | 7 (%PDF-1.) |
1 | 22 s, then 2 min | 4,086 | ⚠️ Keys found, middle not decrypted (recovered afterwards) |
rule.yml |
981 B | none | – | – | – | Out of scope |
AnyDesk.exe |
8,375,224 B | – | – | – | – | Not encrypted (.exe excluded) |
The result matches build 1.
- Every file without ChaCha8 chunks came back hash-exact, all on the default pass.
- The timestamp anchor held. The companion value sat 0.9–1.9 ms before each file’s timestamp, and the second pass never ran.
- The large PDF stopped at the same ChaCha8 limit.
Reading the code showed the key generation was the same. This run confirmed what reading code cannot: the chunk layout, the cipher details and the timing all line up.
The two PDFs, recovered afterwards
After both runs we went back to the two PDFs to settle the open question: was the right ChaCha8 key outside the search window, or did the check fail to recognise it? It was the check. Both PDFs’ seeds sat inside the default window, with ordinary gaps of 3,910–4,287 ticks. In the run-6 PDF the whole ChaCha8 region (bytes 65,535–196,605) is one 291 KB compressed page-content stream, and the zeros/text check accepts 0 of 971 genuine 64-byte windows of it.
Once the KCipher-2 chunks are decrypted, most of a PDF’s structure is readable again, which allows two PDF-specific tests (detail in section 18):
| Test that solved it | Pairs searched | Candidates passing | Standalone | Default recover command |
Result | |
|---|---|---|---|---|---|---|
| Run 6 (build 1), 481,630 B | continuing a page-content stream’s deflate | 23,970,816 | 1 | 2,300 s | 8 min 16 s | ✅ SHA-256 = sealed hash |
| Build 2, 595,456 B | 968 object headers from the xref | 24,373,969 | 1 | 89 s | 4 min 52 s | ✅ SHA-256 = sealed hash |
Both tests now run automatically inside recover. With them, 7 of 7 in-scope files on each build are recoverable. We still report the blind runs as 6 of 7, because the tool frozen for those runs did not include the fallback, and changing a score after the fact would undo the point of freezing the rules.
Earlier validation
Before the blind test, the decryptor reproduced byte-exact output for real
.akirafiles from all three modes (Spot, Full and a 3 MB Part file). Keys rebuilt from captured seeds matched the keys inside the sample’s own RSA plaintext blob, and a seed off by a single tick produced garbage. An earlier blind run on another file found its seeds from the timestamp alone in 28 minutes. Current test suites: 8/8 built-in checks, 15/15 on real sample output, 29/29 round trips, 3/3 synthetic PDF stage-2 cases.
07 · When this recovery will and will not work
This is a real weakness, but it is a timing attack. Everything depends on knowing when each file was encrypted. Read these before promising anyone their data back.
Critical Do not reboot the victim
The clock-to-uptime offset must come from the same boot the ransomware ran in: a live, un-rebooted machine, or a memory image taken before shutdown (it holds both clocks). After a reboot, Windows only records the boot time to about half a second; on our VM it was 352 ms off. The first file then needs a search window around a second wide, which takes days with our Python tool.
Critical File timestamps must survive
The .akira file’s last-write time is the anchor. Copy files with their timestamps preserved, or record them before copying. Restoring from backup, re-saving, or timestomping destroys the anchor. The search then falls back to the run’s time span, which is far more expensive.
Required Known bytes at the start of the file
The check in step 4 needs at least 7 bytes of plaintext at offset 0. Most document, image, archive and database formats have a fixed header, but plain text and some proprietary formats do not. Video formats such as MP4 have no rule in the tool yet.
Partial Large files: ChaCha8 middle
Spot files over about 262 KB, database files (Full mode) and files over 2 MB (Part mode) also use ChaCha8, which needs key1 and iv1. The generic check accepts a ChaCha8 key only if the output looks like zeros or text. PDFs now have a fallback that needs either objects inside the ChaCha8 chunks or a page-content stream crossing into them; a PDF with neither (for example a binary image stream at the chunk boundary) still fails. Other large compressed files, such as Office documents, archives and video, can come back partly decrypted.
Cost Seconds per file, if the window is tight
Each file needs its own search: about 20 seconds on a 20-thread host with the compiled engine, or 30 minutes in pure Python. The cost grows with the time window. After a reboot (a window of about a second), building the candidate keys alone takes tens of minutes per file. A GPU port would help there, and we have not built one.
Not guaranteed Timing outliers
One of our four run-5 files had its two seeds 2.67 ms apart instead of about 0.4 ms, so the standard search missed it. A second pass anchored on the companion value, with gaps up to 30,000 ticks, recovered it in 21 minutes. In run 6, none of the seven files was an outlier. Busy or freshly booted machines will produce more such files, and an even longer stall would fall outside that second pass too.
Scope Measured on one setup
Seed spacing and timestamp behaviour were measured on one 2-CPU VM running with a 1 ms system timer. A slower or faster CPU shifts the spacing. Hosts at the default 15.6 ms timer tick, and very large files, need wider windows; neither was tested.
Scope Two builds tested
The method is validated on two builds of the same C++ codebase: 101bd45a… and 98877635…. Other Akira-branded encryptors, such as builds from other codebases or later rewrites, may generate keys differently. Check the key generation in the code before relying on this method for another sample.
Scope Operator key path untested
The decryptor can also open the RSA footer with the operator’s private key, should it ever leak. That path is tested only with our own RSA key pair, since the real key is not available.
08 · Preserve the clock before anything else
- Do not reboot or power off the affected machine. Isolate it from the network instead.
- Capture memory if you can, then record a clock pair with the snippet below. Run it a few times and keep the median.
- Collect, with timestamps intact: the
.akirafiles, everyakira_readme.txt(its creation time marks when a folder started), the operator’sLog-*.txt, and the System event log. - Work on copies, smallest and most important files first.
Add-Type @'
using System; using System.Runtime.InteropServices;
public static class Clk {
[DllImport("kernel32.dll")] public static extern bool QueryPerformanceCounter(out long c);
[DllImport("kernel32.dll")] public static extern bool QueryPerformanceFrequency(out long f);
[DllImport("kernel32.dll")] public static extern void GetSystemTimePreciseAsFileTime(out long ft);
}
'@
$f = 0L; $q0 = 0L; $ft = 0L; $q1 = 0L
[void][Clk]::QueryPerformanceFrequency([ref]$f)
[void][Clk]::QueryPerformanceCounter([ref]$q0)
[Clk]::GetSystemTimePreciseAsFileTime([ref]$ft)
[void][Clk]::QueryPerformanceCounter([ref]$q1)
$q = [long](($q0 + $q1) / 2)
"freq=$f qpc=$q filetime=$ft offset=$($ft - $q)"
With a 10 MHz counter (freq=10000000, the usual value) the offset is simply FILETIME minus the counter, and it goes straight into --qpc-offset. On other frequencies, scale the counter to 100 ns units first. The full collector, collect_clock_and_times.ps1, also records file timestamps, ransom notes and the operator log.
Part II · Technical analysis
Everything below is from our own work on build 1 (101bd45a…) unless marked otherwise. Addresses are static, against Ghidra’s image base 0x140000000; on the VM the image loaded at a different ASLR base on each boot, so live addresses were rebased. Ghidra function names (FUN_…) are kept so you can follow along in your own database.
09 · Static profile
| Property | Value |
|---|---|
| Format | PE32+ x64, Windows GUI subsystem, 7 sections, ASLR / relocatable |
| PE timestamp | 0x658D8B15 = 2023-12-28 14:49:57 UTC |
| Toolchain | MSVC, linker 14.37 (Visual Studio 2022 17.7), C++20 coroutines, RTTI present |
| Debug info | POGO and ILTCG entries only; no PDB path |
| Statically linked | asio (IOCP async file I/O), Nettle (Yarrow-256, RSA), mini-GMP (bignums), an spdlog-style logger, an argh-style CLI parser |
| Packing | None |
| Obfuscation | Three string groups only (the shadow-copy command and two extension lists), stack-built and decoded per string with an affine map mod 127 (section 13) |
| Anti-analysis | None found. IsDebuggerPresent is reached only on CRT fail-fast paths, and the sample ran normally under x64dbg |
| Imports | 49 behaviour-relevant imports, all called directly through the import table; nothing is resolved at runtime except by CRT and STL helpers |
| Network | WSAStartup is asio’s winsock initialiser; no socket is ever created |
Capabilities, all confirmed in code and most also observed live:
- encrypts local, mapped and UNC paths with a multi-threaded IOCP pipeline;
- deletes Volume Shadow Copies through WMI-launched PowerShell;
- force-closes processes holding target files open, using Restart Manager, sparing system processes;
- hybrid crypto: per-file ChaCha8 + KCipher-2 keys, sealed with an embedded RSA-4096 public key;
- three encryption modes (Full, Spot, Part), chosen by file extension and size;
- a ransom note in every traversed folder.
10 · main() and the thread pools
The CRT entry at 0x1400855b8 reaches main at 0x14004ca40 (FUN_14004ca40). main does all one-time set-up, then hands work to three thread pools. Everything after the pools start is C++20 coroutines on an asio I/O completion port.
- Logging (
FUN_14004c6f0). An spdlog-stylefile_loggerwritingLog-%d-%m-%Y-%H-%M-%S.txtin the working directory. The start time is taken withQueryPerformanceCounterat0x14004d505. - Command line (
FUN_140053d70).GetCommandLineWandCommandLineToArgvW, then an argh-style parser (section 11). - Targets, from exactly one source in priority order: the explicit path, then the share file (
FUN_140041fc0), then the drive sweep (FUN_1400766e0). In-lmode the run stops here, after logging the drive list. - Pre-encryption steps, run once: skip-list update (
FUN_14006ecf0), protected-PID snapshot (FUN_140070c30), shadow-copy deletion (FUN_140071d80→FUN_140071620). Section 12. - Crypto init. The RSA-4096 public key is imported from DER at
0x1400ee010(FUN_14007ba90/FUN_140081ac0). The RSA context gets its own Yarrow instance for PKCS#1 padding (FUN_14007bbe0), seeded the same QPC-only way as the file keys. - Thread pools (
FUN_1400737d0), sized fromGetSystemInfo. - Root-folder queue (
FUN_140073950). Each root is queued here;r8holds the root as astd::wstring. This was our dynamic safety gate: a breakpoint here showed exactly which root would be encrypted, before any file was touched. - Folder parsers (
FUN_1400b5fb0,FUN_1400b81d0) apply the exclusions, dropakira_readme.txt(FUN_140042420), set the force-Full and force-Part flags and enqueue each file withFUN_140074480(queue, path, fullFlag, partFlag, ctx, percent, …). The two flags land in task fields+0x49and+0x4a. - Per-file coroutine (
FUN_1400ae840), section 14. mainwaits for the pools, logs the elapsed time (end QPC at0x14004e06f) and exits. No self-deletion and no persistence.
Thread-pool sizing. With N logical CPUs, N is first doubled when it is below 5 (and treated as 2 when it is 1). Then 30% of N become root-folder parsers, 10% (at least one) folder parsers, and the rest encryptors. A 2-vCPU VM therefore gets 1 / 1 / 2, which matches the two encryptor threads we saw interleave in run 4. The log line Number of threads to encrypt = records the result.
The final log line is the run time in milliseconds. In run 4 it read 423038 ms, which is exactly (main-end QPC − main-start QPC) / 10⁴. That is how we know the sample’s own stopwatch is the same counter that seeds its keys.
11 · Command line and targeting
The parser is argh-style with no registered parameters, so a value must be attached with =. -p C:\x (with a space) is read as a bare flag -p followed by a stray positional argument. The path comes out empty and the sample falls back to sweeping every drive. We saw this live in run 1, which escaped its folder and had to be stopped.
| Argument | Effect | Default |
|---|---|---|
--encryption_path=<path> / -p=<path> |
Encrypt only this folder tree | none |
--share_file=<file> / -s=<file> |
Text file, one UNC path or drive per line; replaces the drive sweep | none |
--encryption_percent=<n> / -n=<n> |
Percent used by Spot and Part modes | 50 |
-localonly |
Skip anything classified as a network disk or network path | off |
-l |
List drives to the log and exit without encrypting | off |
Target classification (FUN_140042c50), per root:
X:with a mounted local volume → “This is local disk:”.X:reported asDRIVE_REMOTE, or a no-root-dir mapped share resolved withWNetGetConnectionW(FUN_1400429f0) → “This is network disk:”.- A UNC path (
PathIsNetworkPathW) → network path. - Anything else → “Not allowed disk”, skipped.
The drive sweep only reaches five drives. FUN_1400766e0 walks the buffer from GetLogicalDriveStringsW and filters with GetDriveTypeW, skipping CD-ROM and no-root-dir volumes. Its loop is compiled with a fixed bound, cmp rsi, 0x2C, that ends the walk after the fifth drive letter. A sixth or later volume (H: when C:–G: exist) is never encrypted in sweep mode. We confirmed this in the disassembly only, not in a live run.
Exclusions are exact-match lookups in std::set containers built by static initializers:
| Set | Members |
|---|---|
Folder names skipped (0x1400f5e18) |
tmp, winnt, temp, thumb, $Recycle.Bin, $RECYCLE.BIN, System Volume Information, Boot, Windows, Trend Micro, ProgramData |
Extensions skipped (0x1400f5e08) |
.exe, .dll, .lnk, .sys, .msi, plus .akira and .arika added at runtime |
| Files skipped | akira_readme.txt; any file whose <name>.arika journal already exists |
Protected processes (0x1400f5e28) |
spoolsv.exe, fontdrvhost.exe, cmd.exe, explorer.exe, sihost.exe, SearchUI.exe, lsass.exe, dwm.exe, LogonUI.exe, winlogon.exe, services.exe, csrss.exe, smss.exe, System Idle Process, Secure System, conhost.exe, System, wininit.exe, Registry, Memory Compression |
The set initializers are FUN_1400018a0 (folders) and FUN_140001ac0 (extensions).
12 · Pre-encryption actions
Three steps run once, in this order, before any file is touched. They are skipped in -l mode. Nothing else runs beforehand: no service stops, no event-log clearing, no firewall or boot-configuration changes.
1 · Skip-list update (FUN_14006ecf0). Adds .akira and .arika to the extension skip set, so encrypted and in-progress files are never encrypted twice.
2 · Protected-PID snapshot (FUN_140070c30). WTSEnumerateProcessesW (0x140070c79) lists running processes, and the PIDs whose names match the 20-name protected set are stored. These PIDs are exempt from the Restart Manager kill below.
3 · Shadow-copy deletion (FUN_140071d80 → FUN_140071620).
- A 76-byte stack string is decoded with
((0x39 − b)·0x0B) mod 0x7Fintopowershell.exe -Command "Get-WmiObject Win32_Shadowcopy | Remove-WmiObject". We confirmed the decoded string in x64dbg memory. FUN_140071620runs it through COM/WMI rather thanCreateProcess:CoInitializeEx,CoCreateInstance,CoSetProxyBlanket, thenWin32_Process.CreateinROOT\CIMV2. PowerShell therefore appears as a child ofWmiPrvSE.exe, which breaks naive parent-child detection.- The window is hidden through
Win32_ProcessStartup, straight from the decompile:
bstrString = SysAllocString(L"ROOT\\CIMV2");
bstrString_01 = SysAllocString(L"Win32_Process");
bstrString_02 = SysAllocString(L"Win32_ProcessStartup");
...
local_90.n2.vt = 0x13; local_90._8_4_ = 0; /* VT_UI4 0 */
(**(code **)(*(longlong *)local_100 + 0x28))(local_100.Lo64, L"CreateFlags", 0, &local_90);
local_78.n2.vt = 0x11; local_78._8_1_ = 0; /* VT_UI1 0 = SW_HIDE */
(**(code **)(*(longlong *)local_100 + 0x28))(local_100.Lo64, L"ShowWindow", 0, &local_78);
FUN_140071d80then opens the returnedProcessIdwithSYNCHRONIZE(OpenProcessat0x140071f50) and waits up to 15 s (WaitForSingleObjectat0x140071f66). The PID is cast to a 16-bit value first, so on a host where PowerShell gets a PID above 32,767 the wait silently fails and encryption starts without waiting for the deletion to finish.- In every run we forced
FUN_140071620to return immediately, so the VM’s shadow copies survived.
Per file, only when needed: Restart Manager (FUN_140070e30). The per-file coroutine calls this only when CreateFileW fails with error 0x20 (ERROR_SHARING_VIOLATION) or 0x21 (ERROR_LOCK_VIOLATION):
RmStartSession,RmRegisterResourceson the file, thenRmGetList(twice: size, then data) for the processes holding it.- If any holder is the ransomware itself or a protected PID,
RmEndSessionand give up on that file. - Otherwise
RmShutdown(session, RmForceShutdown)at0x140071024, which kills the holders (database engines, Office, backup agents), and the open is retried. A file that still fails is logged and skipped.
13 · String obfuscation: an affine map mod 127
Only the three most signature-worthy string groups are hidden. Each string (or table) is decoded byte by byte with its own constants, in the form ((±(b − K))·M) mod 0x7F or (a·b + c) mod 127:
| Group | Where | How |
|---|---|---|
| Shadow-copy command | FUN_140071d80 |
76 stack bytes, ((0x39 − b)·0x0B) mod 0x7F |
| VM / disk-image extensions (20) | FUN_140034500 → set 0x1400f5e38 |
Wide stack strings, one key per string |
| Database extensions (153) | FUN_140003680 → set 0x1400f5e60 |
153 tables in .rdata from about 0x1400d3c00, one affine key per table |
The database list took some work. Ghidra’s decompiler timed out on FUN_140003680 (a huge static initializer), and inline Ghidra scripts were disabled in our setup. So we exported the function’s full disassembly (1.65 MB) and a 40,000-byte .rdata dump, and parsed both on the host. Each table’s key was solved by UTF-16 known plaintext (every entry starts with . and ends with a NUL), then checked against the whole table.
Everything else is plaintext: the log strings, the note, the CLI option names and .akira. Those make good detection anchors (section 20).
The 153 database extensions (always Full mode)
.4dd .4dl .accdb .accdc .accde .accdr .accdt .accft .adb .ade .adf .adp .arc .ora .alf .ask .btr .bdf
.cat .cdb .ckp .cma .cpd .dacpac .dad .dadiagrams .daschema .db .db-shm .db-wal .db3 .dbc .dbf .dbs
.dbt .dbv .dbx .dcb .dct .dcx .ddl .dlis .dp1 .dqy .dsk .dsn .dtsx .dxl .eco .ecx .edb .epim .exb .fcd
.fdb .fic .fmp .fmp12 .fmpsl .fol .fp3 .fp4 .fp5 .fp7 .fpt .frm .gdb .grdb .gwi .hdb .his .ib .idb .ihx
.itdb .itw .jet .jtx .kdb .kexi .kexic .kexis .lgc .lwx .maf .maq .mar .mas .mav .mdb .mdf .mpd .mrg
.mud .mwb .myd .ndf .nnt .nrmlib .ns2 .ns3 .ns4 .nsf .nv .nv2 .nwdb .nyf .odb .oqy .orx .owc .p96 .p97
.pan .pdb .pdm .pnz .qry .qvd .rbf .rctd .rod .rodx .rpd .rsd .sas7bdat .sbf .scx .sdb .sdc .sdf .sis
.spq .sql .sqlite .sqlite3 .sqlitedb .te .temx .tmd .tps .trc .trm .udb .udl .usr .v12 .vis .vpd .vvv
.wdb .wmdb .wrk
The 20 VM and disk-image extensions (always Part mode)
.vdi .vhd .vmdk .pvm .vmem .vmsn .vmsd .nvram .vmx .raw .qcow2 .subvol .bin .vsv .avhd .vmrs .vhdx
.avdx .vmcx .iso
14 · The per-file pipeline
Each queued file runs through one coroutine, FUN_1400ae840. We confirmed every step live. The excerpts are from Ghidra’s decompile of that function (artifacts/ghidra/FUN_1400ae840_per_file_coroutine.decompile.c in the case folder), lightly cleaned up for reading: casts dropped, NULL and INVALID_HANDLE_VALUE named, comments added.
Open. Read-only is cleared first (attrs ^ 1), then the file is opened for read, write and delete (0xC0010000), OPEN_EXISTING, overlapped (0x40000080). On a sharing or lock violation it calls the Restart Manager routine and tries again:
DVar18 = GetFileAttributesW(pWVar36);
if ((DVar18 != 0xffffffff) && ((DVar18 & 1) != 0)) {
SetFileAttributesW(pWVar36, DVar18 ^ 1); /* drop FILE_ATTRIBUTE_READONLY */
}
hFile = CreateFileW(pWVar36, 0xc0010000, share, NULL, 3, 0x40000080, NULL);
DVar18 = GetLastError();
if (hFile == INVALID_HANDLE_VALUE) {
if ((cVar14 == '\0') && (DVar18 - 0x20 < 2)) { /* 0x20 / 0x21: file is locked */
uVar21 = FUN_140070e30(pWVar32); /* Restart Manager force shutdown */
if ((char)uVar21 != '\0') {
hFile = CreateFileW(pWVar32, 0xc0000000, 3, NULL, 3, 0x40000080, NULL);
Choose the mode. The two flags set by the folder parser decide first, then the size. Right after this, the coroutine reads the clock (FUN_140078dd4) and calls the key generator FUN_140036740:
if (*(char *)(task + 0x49) == '\0') { /* not a database extension */
if ((*(char *)(task + 0x4a) != '\0') || (2000000 < size)) {
mode = 2; /* Part: VM extension or > 2,000,000 B */
} else {
mode = 1; /* Spot: the first percent% */
}
} else {
mode = 0; /* Full: database extension */
}
...
FUN_140078dd4(); /* QPC read: per-file start */
uVar22 = FUN_140036740(ctx, keys); /* four Yarrow draws + RSA wrap */
(Here the task fields are also renamed: task + 0x49 / + 0x4a are the force-Full / force-Part flags from the folder parser, size is param_1[0xb8].)
The three modes in numbers. Each mode encrypts one or more regions in place, in 0xFFFF-byte chunks. Within every region the first and last chunk use KCipher-2 and the chunks between use ChaCha8.
| Mode | Loop | Region(s) encrypted (S = size, p = percent) | Observed live |
|---|---|---|---|
| 0 Full | FUN_1400b23a0 |
[0, S) | zero_db.sqlite (0x30000 B) → mode byte 0 |
| 1 Spot | FUN_1400b3580 |
[0, p·S/100) | zero_small.txt, other folder files → mode byte 1 |
| 2 Part, p ≥ 50 | FUN_1400b4780 |
4 stripes of B = p·S/500 bytes, starting every (S − 4B)/5 bytes | zero_big.bin (3,000,000 B): reads at 0, 360,000, 720,000, 1,080,000 |
| 2 Part, p < 50 | FUN_1400b4780 |
2 stripes of B = p·S/300 bytes, starting every (S − 2B)/3 bytes | not exercised |
At the default p = 50, Part mode covers only 40% of the file in four 10% stripes; 60% stays plaintext. Spot mode on a small document leaves its second half untouched. Those plaintext regions are what made the DOCX recoverable in runs 5 and 6: its ZIP central directory sits at the end of the file, unencrypted, and it tells us exactly what the first local header looked like.
Journal. Before encrypting, FUN_1400adb00 creates <name.ext>.arika with CreateFileW(GENERIC_READ|GENERIC_WRITE|DELETE, share R/W, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED) at 0x1400adc3f and writes a 532-byte header. Before and after each chunk, bytes 0–8 are overwritten with a progress record.
Encrypt. Reads and writes go through asio’s overlapped ReadFile (0x140040993, FUN_1400407e0) and WriteFile (0x14004075d, FUN_1400405b0) on an I/O completion port. Each chunk is XORed with its keystream and written back at the same offset.
Seal and rename. FUN_1400b5a20 appends the 512-byte RSA footer at offset = original size. Then the open handle is renamed and the journal deleted:
BVar17 = SetFileInformationByHandle((HANDLE)param_1[0x1a], FileRenameInfo, pvVar37, (DWORD)local_ab8);
if (BVar17 == 0) {
DVar18 = GetLastError();
... "file rename failed. System error:" ...
}
...
if (param_1[0xe1] != 0) {
pWVar32 = (LPCWSTR)FUN_14003ac10(param_1 + 0xdf); /* <name.ext>.arika */
DeleteFileW(pWVar32);
}
On-disk structures.
| Structure | Offset | Size | Content |
|---|---|---|---|
.akira footer |
S (original size) | 512 | RSA-4096 ciphertext of the 91-byte blob, 32-bit words byte-swapped |
.arika header |
0 | 1 | 0xFF initially; then 0 (chunk read and encrypted) or 1 (chunk written) |
.arika header |
1 | 8 | Progress offset (end of the last processed chunk) |
.arika header |
9 | 11 | 0x01, mode, percent, original size |
.arika header |
20 | 512 | The same RSA blob as the footer (532 bytes in total) |
An encrypted file is therefore always exactly 512 bytes larger than the original, and the original size is recoverable from the file length alone. Because the journal carries the RSA blob and the progress mark, a file caught mid-encryption by a crash or a kill stays decryptable by the operator, and our decryptor can handle it too (--journal). The folder parser skips any file whose .arika already exists, so a restarted run does not encrypt a file twice.
Other artifacts.
- Ransom note:
akira_readme.txt, written withstd::ofstreaminto every folder the parsers enter (FUN_140042420). The text is plaintext at0x1400ef060. - Operator log:
Log-<dd>-<mm>-<YYYY>-<HH>-<MM>-<SS>.txtin the working directory. Typical lines: “This is local disk:”, “Number of threads to encrypt =”, “file rename failed. System error:”, “Failed to make full encrypt! (“. - Process: a hidden
powershell.exespawned byWmiPrvSE.exe.
The ransom note, verbatim
Hi friends,
Whatever who you are and what your title is if you're reading this it means the internal infrastructure of your company is fully or partially dead, all your backups - virtual, physical - everything that we managed to reach - are completely removed. Moreover, we have taken a great amount of your corporate data prior to encryption.
Well, for now let's keep all the tears and resentment to ourselves and try to build a constructive dialogue. We're fully aware of what damage we caused by locking your internal sources. At the moment, you have to know:
1. Dealing with us you will save A LOT due to we are not interested in ruining your financially. We will study in depth your finance, bank & income statements, your savings, investments etc. and present our reasonable demand to you. If you have an active cyber insurance, let us know and we will guide you how to properly use it. Also, dragging out the negotiation process will lead to failing of a deal.
2. Paying us you save your TIME, MONEY, EFFORTS and be back on track within 24 hours approximately. Our decryptor works properly on any files or systems, so you will be able to check it by requesting a test decryption service from the beginning of our conversation. If you decide to recover on your own, keep in mind that you can permanently lose access to some files or accidently corrupt them - in this case we won't be able to help.
3. The security report or the exclusive first-hand information that you will receive upon reaching an agreement is of a great value, since NO full audit of your network will show you the vulnerabilities that we've managed to detect and used in order to get into, identify backup solutions and upload your data.
4. As for your data, if we fail to agree, we will try to sell personal information/trade secrets/databases/source codes - generally speaking, everything that has a value on the darkmarket - to multiple threat actors at ones. Then all of this will be published in our blog - https://akiral2iz6a7qgd3ayp3l6yub7xx2uep76idk3u2kollpj5z3z636bad.onion.
5. We're more than negotiable and will definitely find the way to settle this quickly and reach an agreement which will satisfy both of us.
If you're indeed interested in our assistance and the services we provide you can reach out to us following simple instructions:
1. Install TOR Browser to get access to our chat room - https://www.torproject.org/download/.
2. Paste this link - https://akiralkzxzq2dsrzsrvbr2xgbbu2wgsmxryd4csgfameg52n7efvr2id.onion.
3. Use this code - 6188-EA-ABYW-YGRG - to log into our chat.
Keep in mind that the faster you will get in touch, the less damage we cause.
15 · Cipher internals
Every primitive below was re-implemented in Python and matched against data captured live from the running sample. Those re-implementations became the decryptor.
| Component | Implementation | Where | How we validated it |
|---|---|---|---|
| RNG | Nettle Yarrow-256: two SHA-256 pools, an AES-256 counter, 1,500 reseed iterations. A new instance per value, seeded only with QPC × 100 as a decimal ASCII string, e.g. "867151895800" |
FUN_14007b1f0, FUN_1400822c0 |
Regenerated all 80 key bytes in a captured RSA blob from its four seeds (run 2), and again for all three run-3 files |
| Per-file secrets | key1 32 B, iv1 16 B, key2 16 B, iv2 16 B, in that order | FUN_140036740 |
Seen at both cipher inits |
| Cipher A | ChaCha, key setup called with kbits = 0x40, which takes the 16-byte branch: constant “expand 16-byte k”, key1[0:16] used twice, nonce iv1[0:8], 64-bit counter from 0. 8 rounds (4 double-rounds). Partial 64-byte blocks are discarded at each 0xFFFF chunk |
FUN_14007b0b0 (init), FUN_14007c570 (key setup), FUN_14007beb0 (block) |
128 B of live keystream matched only the 128-bit-key, 8-round variant; 12 and 20 rounds and the 256-bit key failed. Live state showed counter 0x400 at chunk 3 |
| Cipher B | KCipher-2 (RFC 7008), key2 / iv2, byte-continuous keystream across chunks | FUN_14007b4e0 (init), FUN_14007c690 (key schedule), FUN_14007c9c0 (update), FUN_14007c8a0 (XOR) |
The model built from the binary’s tables reproduces RFC 7008 vector C.1 (f871ebef945b7272…) and matched 64 B of live keystream |
| Key wrap | Nettle rsa_encrypt, PKCS#1 v1.5, embedded RSA-4096 key (e = 65537). Input 91 bytes, output 512 bytes, each 32-bit word byte-swapped before storage |
FUN_14007b8d0, FUN_140081ac0 (DER import), FUN_14007ba90 |
91-byte input and 512-byte footer write both captured |
Two details that cost us time.
- ChaCha’s key size. The key buffer is 32 bytes and the “expand 32-byte k” constant sits right next to “expand 16-byte k” in
.rdata, so a 256-bit ChaCha20 is the obvious guess. It is wrong. Key setup is called withkbits = 0x40, which takes the 16-byte branch, and the round loop runs four double-rounds. Only the live keystream comparison settled it. The upper 16 bytes of key1 are never read, which is whyrecoverdoes not search them and prints them as zeros. - KCipher-2’s tables. The four α-multiplication tables do not all use one polynomial (
amul0fits0x1C3). Our first model assumed a single polynomial and failed the RFC vector. We rebuilt all four tables by GF(2)-linearity from the eight basis entries of each table read from the binary.
The 91-byte RSA plaintext.
| Offset | Size | Content |
|---|---|---|
| 0 | 1 | 0x01 |
| 1 | 1 | mode (0 Full, 1 Spot, 2 Part) |
| 2 | 1 | percent (0 in Full mode) |
| 3 | 8 | original file size, little-endian |
| 11 | 48 | key1 and iv1 as 32-bit words in the order k0 k1 v0 k2 k3 v1 k4 k5 v2 k6 k7 v3, each word byte-swapped |
| 59 | 32 | key2 and iv2 interleaved k0 v0 k1 v1 k2 v2 k3 v3, each word byte-swapped |
The RSA context has its own Yarrow instance for PKCS#1 padding (FUN_14007bbe0), seeded the same QPC-only way. That does not help recovery, because the private key never touches the victim. Recovery has to come from the symmetric side, and it can.
The embedded public key. RSA-4096, e = 65537, DER at VA 0x1400ee010. SHA-256 of the DER RSAPublicKey: a40a55f2600b9199181bc6c0778e64b2e75145c4e6f715da6d23a72407539aaa; the modulus begins c2f8f4dbbe7da09f7c991d69e00b4466. It identifies the operator key pair; build 2 carries a different one. Both PEM and DER copies are in the case artifacts/.
16 · Windows API map
Call sites come from Ghidra cross-references to 49 imports; the key ones were checked against their callers and decompiles.
| Behaviour | APIs | Call site → function |
|---|---|---|
| Read operator arguments | GetCommandLineW, CommandLineToArgvW |
0x14004cb62, 0x14004cb72 → main FUN_14004ca40 |
| Size thread pools | GetSystemInfo |
0x14004d549 → main |
| Drive sweep and classification | GetLogicalDriveStringsW, GetDriveTypeW |
0x140076786, 0x140076827 → FUN_1400766e0; 0x140042d07 → FUN_140042c50 |
| Network share discovery | WNetGetConnectionW, PathIsNetworkPathW |
0x140042a97 → FUN_1400429f0; 0x1400433a9 → FUN_140042c50 |
| Folder walk | FindFirstFileExW, FindNextFileW, GetFileInformationByHandleEx |
std::filesystem helpers FUN_1400789d8, FUN_140078998, FUN_140078a4c, used by FUN_1400b81d0 |
| Protected-process snapshot | WTSEnumerateProcessesW, WTSFreeMemory |
0x140070c79, 0x140070df5 → FUN_140070c30 |
| Kill processes that lock files | RmStartSession, RmRegisterResources, RmGetList ×2, RmShutdown, RmEndSession ×3 |
0x140070eb3–0x1400711c3 → FUN_140070e30; RmShutdown at 0x140071024 |
| Shadow-copy deletion via WMI | CoInitializeEx, CoCreateInstance, CoSetProxyBlanket, SysAllocString ×7, VariantInit / VariantClear ×6 |
0x140071d8d → FUN_140071d80; 0x14007167d, 0x140071704 → FUN_140071620 |
| Wait for the PowerShell child | OpenProcess, WaitForSingleObject |
0x140071f50, 0x140071f66 → FUN_140071d80 (PID cast to short) |
| Key seed (the weakness) | QueryPerformanceCounter, QueryPerformanceFrequency |
0x140078ddd → clock wrapper FUN_140078dd4, called by FUN_14007b1f0 and FUN_14007bbe0; no other entropy source |
| Open target | GetFileAttributesW, SetFileAttributesW, CreateFileW ×2, GetFileSizeEx |
0x1400aeea7–0x1400af41e → FUN_1400ae840 |
| Journal | CreateFileW |
0x1400adc3f → FUN_1400adb00 |
| In-place encryption I/O | ReadFile, WriteFile (overlapped), CreateIoCompletionPort, GetQueuedCompletionStatus, PostQueuedCompletionStatus |
0x140040993 → FUN_1400407e0; 0x14004075d → FUN_1400405b0; IOCP scheduler FUN_1400385d0, FUN_140038e60 |
| Rename and clean up | SetFileInformationByHandle (FileRenameInfo), DeleteFileW |
0x1400b1deb, 0x1400b1f02 → FUN_1400ae840 |
Three imports that look suspicious but are library code:
TerminateThread(0x1400365e0) is in asio’s thread join. It runs only when asio’s terminate flag is set; otherwise asio queues an APC and waits.IsDebuggerPresentappears only on CRT fail-fast paths.WSAStartupis asio’s winsock initialiser; no socket is ever created.
For hunting, the useful API sequence is WTSEnumerateProcessesW → WMI Win32_Process.Create → RmShutdown → overlapped ReadFile/WriteFile → SetFileInformationByHandle rename.
17 · Measuring the seeds without touching them
The whole recovery rests on two numbers: how far apart a file’s four seeds are, and how far the file’s timestamp sits from them. Our first captures used a breakpoint on each seed, which inflated the gaps to 10–500 ms and made them useless. Run 4 measured them natively.
An in-process logger on the sample’s own clock. The sample reads the counter through one tiny wrapper, FUN_140078dd4, 25 bytes long:
sub rsp, 28h
lea rcx, [rsp+30h]
call [QueryPerformanceCounter]
mov rax, [rsp+30h] ; <- tail at 0x140078de3 patched: mov rax, stub / jmp rax
add rsp, 28h
ret
We overwrote its tail with a jump into a small stub in an RWX allocation. The stub reloads the counter value, takes the caller’s return address from [rsp+28h] and the thread ID from gs:[48h], reserves a 32-byte slot with lock xadd, stores (QPC, caller, TID) there and returns the unchanged value. The cost is a few nanoseconds per call and no debug event. To keep the finished process around long enough to read the buffer, the first two bytes of ntdll!RtlExitUserProcess were patched to EB FE (jump to self).
Stub bytes and timer-integrity check
48 8B 44 24 30 4C 8B 44 24 28 65 44 8B 0C 25 48 00 00 00 49 BA <buf> 41 BB 01 00 00 00
F0 45 0F C1 1A 41 81 FB 88 13 00 00 73 13 49 C1 E3 05 4D 01 D3 49 89 43 40 4D 89 43 48
4D 89 4B 50 48 83 C4 28 C3
We use ScyllaHide, which can hook timing functions, so we checked the timer path in a fresh process with the same debugger profile:
- the sample’s IAT entry
0x1400c42f0points to thekernel32!QueryPerformanceCounterstub (jmp [kernel32 IAT]); - that leads to
ntdll!RtlQueryPerformanceCounterwith its original prologue (mov [rsp+8],rbx; push rdi; sub rsp,20; mov r8b,[7FFE03C6]); ntdll!NtQueryPerformanceCounteris the untouched syscall stub (mov r10,rcx; mov eax,31);SharedUserData+0x3C6= 0x83: the QPC bypass is enabled, so the counter is read in user mode from the TSC without a syscall.
None of these is hooked, so the logged values are real time. KUSER_SHARED_DATA.QpcFrequency (0x7FFE0300) was 10,000,000, so seed string = QPC × 100.
The safety gates were armed first (WMI executor forced to return, root folder confirmed), then every breakpoint was deleted. During encryption the debugger log shows thread and DLL events only: no exceptions and no breakpoint hits. Two of the 21 resulting files were decrypted with the logged values × 100 as seeds and came back as their all-zero originals, which proves the hook recorded exactly the seeds the sample used.
What the log showed. 129 counter reads, each caller in its own role:
| Caller (static) | Role | Count |
|---|---|---|
0x14004d505 / 0x14004e06f |
main start / end timer | 1 / 1 |
0x14007bc49 |
RSA-padding Yarrow seed (FUN_14007bbe0) |
1 |
0x1400aff57 |
per-file coroutine start (FUN_1400ae840) |
21 |
0x14007b237 |
per-value Yarrow seed (FUN_14007b1f0) |
84 = 21 × 4 |
0x1400b1c99 |
per-file coroutine end | 21 |
All 84 seeds increase monotonically, with two encryptor threads interleaving. The gaps, in 100 ns ticks:
| Gap | Min | Median | Max |
|---|---|---|---|
| key1 → iv1 | 3,916 | 4,119 | 4,692 |
| iv1 → key2 | 3,887 | 4,047 | 14,397 |
| key2 → iv2 | 3,889 | 4,121 | 27,109 |
| last seed of one file → first of the next, same thread | 15,024 | 16,513 | 32,634 |
The floor of about 3,900 ticks is the time one Yarrow-256 seeding takes on the CPU, so it scales with the victim’s hardware. The rare long gaps come from the two encryptor threads competing for CPU. For key2 → iv2, 18 of 21 gaps were under 5,100 ticks and 20 of 21 under 8,000, which is where recover’s default --gap 3800:8000 comes from.
Timestamps as anchors. Run 4’s 21 files, with their seeds known exactly, let us score every timestamp a responder might have. The live clock offset (precise system time minus QPC, seven pairs agreeing within 12 µs) gave:
| Difference | Min | Median | Max |
|---|---|---|---|
.akira mtime − key1 |
+1.28 ms | +2.34 ms | +6.28 ms |
.akira mtime − key2 |
+0.45 ms | +1.39 ms | +5.36 ms |
.akira mtime − iv2 |
+0.06 ms | +0.98 ms | +3.82 ms |
.akira mtime − coroutine end |
−1.43 ms | −0.53 ms | −0.12 ms |
The footer write sets the last-write time, just before the coroutine returns. The creation time is the original file’s (the sample overwrites in place) and carries no information. Other anchors from the same run:
| Artifact | Relation found | Use |
|---|---|---|
akira_readme.txt mtime |
1.3 ms before the folder’s first key1 seed | lower bound for a folder (--floor) |
| Operator log name | local start time to the second | 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 |
After a reboot the offset has to come from boot records, and those are coarse:
| Offset source | Error vs the true offset | First-file window |
|---|---|---|
| Measured on the live victim, same boot | under 0.1 ms | −8 / +1 ms, about 42 min in pure Python |
WMI LastBootUpTime / Kernel-General 12 StartTime (0.5 s granularity) |
+352 ms | about ±0.6 s, days |
Kernel-General 12 TimeCreated |
+1,497 ms | worse |
EventLog 6005 TimeCreated |
+9,620 ms | worse still |
The error is one constant per boot, though. Once the first file is solved its seeds pin the offset to about ±2.5 ms, and every other file of that boot costs the same as the live case.
18 · Inside recover
akira_decrypt.py is a single Python 3.8+ file using only the standard library (1,601 lines, SHA-256 091701682454a75dfb57324e29a6270977186f68209a7c3a93178ab76fe9fc4b). It never overwrites a file unless told to (--force).
| Command | Key source | Use when |
|---|---|---|
decrypt FILE --keys k1:iv1:k2:iv2 |
Raw keys (32/16/16/16 bytes hex) | Keys came from memory, EDR telemetry or recover |
decrypt FILE --seeds s1,s2,s3,s4 |
The four Yarrow seeds | Seeds came from recover or a capture |
decrypt PATH --private-key key.pem |
Operator RSA key (PEM/DER, PKCS#1 or PKCS#8) | The key ever leaks; reads mode, percent and keys from each footer and walks whole folders |
decrypt FILE --journal FILE.arika … |
Any of the above | An interrupted file; decrypts up to the journal’s progress mark |
recover FILE … |
None: brute-forces the seeds | No key material at all |
info, benchmark, selftest |
n/a | Show the planned layout; size a search; re-run the validation vectors |
Without a footer key the tool infers mode and percent the way the sample does: from the extension lists and the 2,000,000-byte threshold. --percent and --mode override this.
The core is short. Regenerate the four values, rebuild the sample’s chunk plan, and XOR each chunk with the right keystream (condensed from the script):
def keys_from_seeds(seeds): # key1, iv1, key2, iv2
out = []
for s, n in zip(seeds, (32, 16, 16, 16)):
y = Yarrow256() # fresh instance per value
y.seed(s.encode("ascii")) # seed = uptime in ns as text
out.append(y.random(n))
return tuple(out)
def regions(mode, percent, size):
if mode == 0: return [(0, size)] # Full
if mode == 1: return [(0, percent * size // 100)] # Spot
div, blocks = (3, 2) if percent < 50 else (5, 4) # Part
blk = (percent * size // 100) // div
stride = (size - blocks * blk) // div
return [(i * stride, blk) for i in range(blocks)]
def chunk_plan(mode, percent, size): # same order as the sample
for start, length in regions(mode, percent, size):
off = 0
while off < length:
n = min(0xFFFF, length - off, size - (start + off))
cipher = "k2" if off == 0 or length <= off + n else "chacha"
yield start + off, n, cipher
off += n
ChaChaSample.xor starts every chunk on a fresh 64-byte block, which reproduces the sample discarding partial blocks; the KCipher-2 keystream runs on across chunks.
The search. Every value is the first 16 bytes of one fresh Yarrow draw, so one table of candidate seeds serves all four values.
- Stage 1, KCipher-2. For each key2 seed in the window and each candidate gap to the iv2 seed, run KCipher-2 over the first ciphertext bytes and compare with the known header. At least 7–8 bytes are needed; 8 bytes is a 64-bit filter. A stress run over 20,012,001 pairs found the single true pair with no false positives. This alone solves Spot files under about 262 KB.
- Stage 2, ChaCha8. For files with ChaCha8 chunks, walk back from the key2 seed to the iv1 and key1 seeds and test the first ChaCha chunk: known bytes, or the zeros/text heuristic.
- Stage 2 fallback for PDFs. If that finds nothing in a
.pdf, the same window is searched again with two PDF tests (below). - Chaining. Seeds only increase, so one solved file brackets the next and reveals the real seed-to-seed gap on that machine.
Engines. In pure Python the search runs about 150,000 KCipher-2 pair tests per second on our 20-thread host. With numpy and numba installed, akira_fast.py compiles the stage-1 kernel and reaches 40–97 million pairs per second; building the Yarrow table (about 2,000 seeds per second per core) then becomes the bottleneck. Search cost in pure Python, by how well the key2 seed is known:
| key2 uncertainty | Gap 3,800–5,200 (18/21) | Gap 3,800–8,000 (20/21, default) | Gap 3,800–28,000 (21/21) |
|---|---|---|---|
| ±0.5 ms | ~1.6 min | ~4.7 min | ~27 min |
| ±5 ms | ~16 min | ~47 min | ~4.5 h |
| ±50 ms | ~2.6 h | ~7.8 h | ~45 h |
| ±0.5 s | ~26 h | ~78 h | needs a C or GPU port |
The two PDF tests. After stage 1 the KCipher-2 chunks and the unencrypted tail of a PDF are readable, so its structure can be used as known plaintext inside the ChaCha8 chunks:
- xref. The cross-reference tables or streams give the exact offset of every
N G objheader. A header that falls inside a ChaCha chunk is exact known plaintext, and the ChaCha8 keystream block at any offset can be computed directly, so each candidate costs a single 64-byte block. The test is used only if every header in readable bytes sits where the xref says, and a candidate must match at least 90% of the in-chunk headers. - deflate. A compressed page-content stream that starts in readable bytes and runs into the first ChaCha chunk. zlib is primed with the readable part, each candidate’s output continues it, and the inflated text must be valid page-content syntax (numbers, names, strings, operators with the right operand count). A candidate is accepted only if the whole stream then inflates with a valid Adler-32.
recover --stage2 auto (the default) runs the generic check first and the PDF tests only when it finds nothing; --stage2 pdf runs only the PDF tests; --stage2 heuristic keeps the old behaviour. A synthetic test suite (test_pdf_stage2.py) covers an xref-solvable PDF, a deflate-solvable PDF, and a binary-stream PDF where neither applies and the tool correctly reports failure.
Validation of the tool itself.
| Check | Result |
|---|---|
| Real sample output, all 3 modes, decrypted with captured keys and with captured seeds | 6/6 byte-exact |
Real sample output, all 3 modes, all four seeds found by recover from a window |
3/3 byte-exact |
| Negative control: key2 seed off by one tick (100 ns) | 6,116 non-zero bytes out of a zero file: garbage, as it should be |
| Negative control: wrong gap window | no candidate, no false positive |
| Built-in self-test (RFC 7008, captured seeds, blobs, chunks, stripe offsets) | 8/8 |
| Real-output fixtures | 15/15 |
| Round trip with our own RSA-4096 key (footer, PEM/DER/PKCS#8, all modes, journal) | 29/29 |
| Synthetic PDF stage-2 cases | 3/3 |
How we ran the sample
All runs were on an isolated Windows VM under x64dbg, reverted to a clean snapshot afterwards. Two safety gates were used in every run from run 2 onward: the WMI executor FUN_140071620 was forced to return (so shadow copies survived), and a breakpoint on the root-folder queue FUN_140073950 confirmed the target before anything was encrypted. The launch line was always --encryption_path=<folder> -localonly.
| Run | Purpose | Outcome |
|---|---|---|
| 1 | First detonation, with -p <path> |
Escaped the folder because of the argh parsing; stopped early. Taught us the = rule |
| 2 | Cipher, mode and layout validation, on zero-filled test files | ChaCha8 (128-bit), KCipher-2, all three modes, the Part stripe offsets and the footer position confirmed against live data |
| 3 | End-to-end capture: seeds and RSA blobs of three zero-filled files | The three real .akira files became the decryptor’s regression fixtures |
| 4 | Native seed gap, no seed breakpoints (in-process QPC logger) | 84 seeds of 21 files; the gap and timestamp tables above |
| 5 | Blind recovery of four real files | 4/4 hash-exact |
| 6 | Independent blind re-test, rules frozen | 6/7 hash-exact blind; 7/7 with the PDF fallback |
| Build 2 | Static comparison, then one run under the run-6 protocol | 6/7 hash-exact blind with build 1’s tool unchanged; 7/7 with the PDF fallback |
Not covered: --share_file was read statically only, and Part mode with percent below 50 was never exercised.
Function map
| Address | Role |
|---|---|
0x1400855b8 |
PE entry (CRT) |
0x14004ca40 |
main: logging, CLI parse, drive list, pre-encryption steps, crypto init, thread pools, timing |
0x14004c6f0 |
spdlog file_logger setup (Log-….txt) |
0x140053d70 |
argh-style argv parser |
0x1400766e0 |
Drive enumeration (first five drives only) |
0x140042c50 |
Target classifier: local disk / network disk / network path / not allowed |
0x140041fc0 |
Share-file reader |
0x14006ecf0 |
Adds .akira / .arika to the skip set |
0x140070c30 |
Protected-PID snapshot (WTSEnumerateProcessesW) |
0x140071d80, 0x140071620 |
Shadow-copy command decode; WMI Win32_Process.Create |
0x140070e30 |
Restart Manager unlock / force shutdown |
0x1400737d0, 0x140073950 |
Thread-pool creation; root-folder queue |
0x1400b5fb0, 0x1400b81d0 |
Folder-parser coroutines (exclusions, note drop, mode flags, enqueue) |
0x140042420 |
Ransom-note writer |
0x140074480 |
Enqueue a per-file encryption task |
0x1400ae840 |
Per-file coroutine: open, cipher init, journal, mode dispatch, footer, rename |
0x1400adb00 |
.arika journal creation (532-byte header) |
0x1400b23a0 / 0x1400b3580 / 0x1400b4780 |
Full / Spot / Part encryption loops |
0x1400b5a20 |
512-byte footer writer |
0x140036740 |
Per-file key generation and RSA wrap of the 91-byte blob |
0x140078dd4 |
QPC wrapper (the only clock the keys see) |
0x14007b1f0 |
Per-value Yarrow-256 instance seeded from QPC |
0x14007bbe0 |
RSA-padding Yarrow, seeded the same way |
0x14007b0b0, 0x14007c570, 0x14007beb0 |
ChaCha init / key setup / 8-round block function |
0x14007b4e0, 0x14007c690, 0x14007c9c0, 0x14007c8a0 |
KCipher-2 init / key schedule / update / keystream XOR |
0x14007ba90, 0x140081ac0, 0x14007b8d0 |
RSA public-key import (DER) and PKCS#1 v1.5 encrypt |
0x140003680, 0x140034500, 0x1400018a0, 0x140001ac0 |
Static initializers: DB-extension, VM-extension, folder and extension sets |
19 · Indicators of compromise
All indicators come from the binaries or from our controlled runs. The binary has no network, registry, mutex or service indicators: it opens no sockets, writes no registry keys and creates no named mutex.
| Type | Value | Context |
|---|---|---|
| SHA-256 | 101bd45ab673fd6547386dff4339f1649e61b9f5c318260d365f766fbdc2d3c9 | sample |
| MD5 | 427eba9f6ff2cd312c72770f13e279c1 | sample |
| PE timestamp | 0x658D8B15 | 2023-12-28 14:49:57 UTC |
| SHA-256 | 988776358d0e45a4907dc1f4906a916f1b3595a31fa44d8e04e563a32557eb42 | second build, same codebase |
| MD5 | 4aecef9ddc8d07b82a6902b27f051f34 | second build |
| Victim code | 6729-HK-NIZN-WOPQ | chat login in the second build’s note |
| RSA key (DER SHA-256) | a40a55f2600b9199181bc6c0778e64b2e75145c4e6f715da6d23a72407539aaa | embedded RSA-4096 public key; modulus begins c2f8f4dbbe7da09f7c991d69e00b4466 |
| Onion | akiral2iz6a7qgd3ayp3l6yub7xx2uep76idk3u2kollpj5z3z636bad.onion | leak site in the note |
| Onion | akiralkzxzq2dsrzsrvbr2xgbbu2wgsmxryd4csgfameg52n7efvr2id.onion | negotiation chat in the note |
| Victim code | 6188-EA-ABYW-YGRG | chat login in the note |
| Extension | .akira | encrypted file, original size + 512 B, last 512 B high-entropy |
| Extension | .arika | temporary 532-byte per-file journal |
| File name | akira_readme.txt | ransom note, one per folder |
| File name | Log-DD-MM-YYYY-HH-MM-SS.txt | operator log in the working directory |
| Command line | powershell.exe -Command "Get-WmiObject Win32_Shadowcopy | Remove-WmiObject" | hidden, parent WmiPrvSE.exe |
| Arguments | --encryption_path= --share_file= --encryption_percent= -localonly | operator launch syntax |
| Log strings | Number of threads to encrypt = This is network disk: Failed to make part encrypt! ( asio::create_auto_save_file error: | static strings in the binary |
20 · Catch it before the first file
The earliest reliable signal is the shadow-copy deletion: PowerShell started by WmiPrvSE.exe with Win32_Shadowcopy and Remove-WmiObject on its command line. It happens before any file is touched. After that, a burst of .arika creations followed by .akira renames is unmistakable.
| Signal | Source | Why it fires |
|---|---|---|
WmiPrvSE.exe → powershell.exe with Win32_Shadowcopy and Remove-WmiObject |
Process creation (Sysmon 1 / 4688), PowerShell 4104 | Shadow-copy deletion via Win32_Process.Create, hidden window |
Bursts of *.arika creates, then the same paths renamed to *.akira |
File events (Sysmon 11 / EDR rename telemetry) | Per-file journal + FileRenameInfo rename |
akira_readme.txt created in many folders within seconds |
File events | Note dropped per traversed folder |
| Rapid Restart Manager sessions from an unsigned process, then terminations of database or Office processes | Application log RestartManager 10000/10001, process termination | RmShutdown(RmForceShutdown) on file lockers |
Command line containing --encryption_path=, --share_file= or -localonly |
Process creation | Operator launch syntax |
New Log-??-??-????-??-??-??.txt next to an unknown executable |
File events | Built-in operator log |
YARA. The rule uses only strings and constants confirmed present in this binary. It has not been run against a wider corpus.
rule Akira_Win64_Encryptor_101bd45a
{
meta:
description = "Akira-branded x64 encryptor (asio/C++20, ChaCha8 + KCipher-2, RSA-4096)"
sha256 = "101bd45ab673fd6547386dff4339f1649e61b9f5c318260d365f766fbdc2d3c9"
date = "2026-10-11"
strings:
$n1 = "akira_readme.txt" ascii
$n2 = ".arika" ascii
$l1 = "write_encrypt_info error:" ascii
$l2 = "(Full begin) Cipher is not initialize!" ascii
$l3 = "asio::create_auto_save_file error:" ascii
$l4 = "Failed to make spot encrypt! (" ascii
$l5 = "Number of thread to root folder parsers = " ascii
$c1 = "--encryption_percent" wide
$c2 = "--share_file" wide
$c3 = "-localonly" wide
$k2 = { 00 00 00 00 1A 6D 08 B6 34 DA 10 AF 2E B7 18 19 } // KCipher-2 amul0 table
$cc = "expand 32-byte kexpand 16-byte k" ascii
condition:
uint16(0) == 0x5A4D and filesize < 3MB and
( (1 of ($n*) and 4 of ($l*, $c*)) or ($k2 and $cc and 3 of ($l*, $c*)) )
}
| ATT&CK | Technique | In this sample |
|---|---|---|
| T1486 | Data Encrypted for Impact | ChaCha8 + KCipher-2, RSA-4096 footer |
| T1490 | Inhibit System Recovery | Volume Shadow Copies removed |
| T1047 | Windows Management Instrumentation | Win32_Process.Create starts PowerShell |
| T1059.001 | PowerShell | shadow-copy removal |
| T1564.003 | Hidden Window | Win32_ProcessStartup.ShowWindow = 0 |
| T1489 | Service Stop | Restart Manager closes file holders |
| T1057 | Process Discovery | WTSEnumerateProcessesW protected-PID list, RmGetList |
| T1083 | File and Directory Discovery | recursive directory iteration |
| T1135 | Network Share Discovery | mapped-drive and UNC classification; share-list input |
| T1082 | System Information Discovery | GetSystemInfo CPU count sizes the thread pools |
| T1027 | Obfuscated Files or Information | per-string mod-127 obfuscation of three string groups |
21 · Decryptor, scripts and rules
The tools are standard-library Python 3.8+ and PowerShell.
| Artifact | What it is |
|---|---|
akira_decrypt.py |
Decryptor and seed brute-forcer: decrypt, recover, benchmark, selftest. 2026-10-14 version with the PDF stage-2 fallback. |
akira_fast.py |
Optional compiled search engine for recover (needs numpy + numba): about 20 s per file instead of 30 min. |
collect_clock_and_times.ps1 |
Responder collector: clock pairs, file timestamps, ransom notes and the operator log. |
run6_blind_recover.py |
Blind recovery for a whole folder: type-based known bytes (PNG, PDF, ZIP family, OLE2, JFIF, SQLite, RTF), the default pass, then an automatic second pass. |
run5_blind_recover.py |
End-to-end example of the run-5 blind recovery, including the DOCX header rebuild. |
analyze_timestamps.py |
Matches encrypted files to seeds and measures timestamp offsets. |
| PDF stage-2 investigation | How the run-6 PDF was recovered (deflate test); the build-2 xref test is in the build-2 case. |
akira_win64_encryptor_101bd45a.yar |
YARA rule for this build. |
| Sigma rules | Shadow-copy removal by WMI-spawned PowerShell (rule), and the operator launch syntax (rule). |
iocs.csv |
All indicators from this post in one file. |
| Tests | Built-in checks, real-output fixtures, round-trip and PDF stage-2 tests. |
| Case report | The full technical report in one Markdown file, with the per-step notes beside it in the case folder. |
Shayan Ahmed Khan · Lead Threat Detection & Response
All analysis was performed on an isolated lab VM that was reverted after every run. Findings are from our own reverse engineering and controlled detonations of the samples identified above.