Akira Ransomware Analysis — 101bd45a…c3c9

Oct 11, 2026 · @Shaddy

The sample is a 64-bit C++ ransomware that brands itself Akira. It encrypts files in place with ChaCha8 and KCipher-2, wraps each file’s keys with an embedded RSA-4096 public key, and renames files to .akira. Its per-file keys come from a Yarrow generator seeded only with the machine’s uptime counter, so they can be recovered without the RSA key. Our decryptor did this on the sample’s real output, including a fully independent blind test on files we had not seen, where 6 of 7 in-scope files came back hash-exact. The same decryptor, unchanged, did the same on a second build of this codebase. Both blind runs left one large PDF partly decrypted; a PDF stage-2 fallback added afterwards recovers both hash-exact.

At a glance

A single self-contained PE does everything. There is no network C2, no persistence and no data exfiltration in this binary. The theft claimed in the note is not done by this file.

Property Value
SHA-256 101bd45ab673fd6547386dff4339f1649e61b9f5c318260d365f766fbdc2d3c9
MD5 427eba9f6ff2cd312c72770f13e279c1
Type PE32+ x64, Windows GUI subsystem, 7 sections, ASLR/relocatable
Compiled 2023-12-28 14:49:57 UTC (PE header timestamp)
Toolchain MSVC, linker 14.37 (Visual Studio 2022 17.7), C++20 coroutines, RTTI present
Debug info POGO + ILTCG entries only; no PDB path
Statically linked libraries asio (IOCP async file I/O), Nettle (Yarrow-256, RSA), mini-GMP (bignums), spdlog-style logger, argh-style CLI parser
Packing / obfuscation Not packed. Selected strings (shadow-copy command, VM-disk extension list) are stack-built and decoded with a per-string ((±(b−K))·M) mod 127 cipher
Anti-analysis None found. Only CRT fail-fast paths call IsDebuggerPresent; the sample ran normally under x64dbg
Self-identification Extension .akira, note akira_readme.txt, Akira-branded .onion URLs in the note

Capabilities, all confirmed in code and most also observed live:

Execution flow

The main thread does all set-up once, then hands files to encryptor threads that each run the same pipeline. Everything after step 4 is asynchronous C++20 coroutines on an asio IOCP pool.

Execution flow: 5 set-up steps, 6 per-file steps

In -l mode the run stops after step 2 and only logs the drive list. A file that fails to open even after Restart Manager is logged and skipped.

Command line and targeting

Targets come from exactly one source, in priority order: an explicit path, then a share list, then a drive sweep. The parser is argh-style with no registered parameters. A value must be attached with =; -p C:\x is read as a bare flag, and the sample then falls back to sweeping all drives. We observed this live.

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, per root (FUN_140042c50):

The drive sweep (FUN_1400766e0) uses GetLogicalDriveStringsW + GetDriveTypeW and skips CD-ROM and no-root-dir volumes. Its loop is compiled with a fixed bound (cmp rsi, 0x2C), so only the first five drive letters are ever visited. A sixth or later volume (for example H: when C:–G: exist) is not encrypted in sweep mode.

Exclusions, all 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 progress file 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

Thread pools are sized from the logical CPU count N. N is first doubled when it is below 5 (and treated as 2 when it is 1). Then 30% of N go to root-folder parsers, 10% (at least 1) to folder parsers, and the rest to encryptors. A 2-vCPU VM therefore gets 1 / 1 / 2.

Pre-encryption actions

Three steps run once, in this order, before any file is touched. They are skipped in -l (list-only) mode.

  1. Skip list update (FUN_14006ecf0). Adds .akira and .arika to the extension skip set, so encrypted and in-progress files are never re-encrypted.
  2. Protected-process snapshot (FUN_140070c30). WTSEnumerateProcessesW lists running processes, and the PIDs whose names match the 20-name protected set are stored. These PIDs are later exempt from Restart Manager shutdown.
  3. Shadow-copy deletion (FUN_140071d80 → FUN_140071620).
    • Decodes a 76-byte stack string with ((0x39−b)·0x0B) mod 0x7F into powershell.exe -Command "Get-WmiObject Win32_Shadowcopy | Remove-WmiObject".
    • Launches it through COM/WMI in ROOT\CIMV2: Win32_Process.Create, with Win32_ProcessStartup.ShowWindow = 0 (hidden) and CreateFlags = 0. PowerShell therefore appears as a child of WmiPrvSE.exe, not of the ransomware.
    • Opens the returned PID with SYNCHRONIZE and waits up to 15 s. The PID is read as a 16-bit value, so the wait silently fails for PIDs above 32767.
    • The decoded string was confirmed live in the debugger. We forced the call to return so the VM’s shadow copies survived.

Restart Manager unlocking (FUN_140070e30) runs per file, only when the file cannot be opened:

  1. RmStartSession, then RmRegisterResources on the file, then RmGetList to get the processes holding it.
  2. If any holder is the ransomware itself or a protected PID, it calls RmEndSession and gives up on that file.
  3. Otherwise it calls RmShutdown(session, RmForceShutdown), which kills the holders (database engines, Office, backup agents), and the open is retried.

Nothing else runs beforehand: no service stops, no event-log clearing, no firewall or boot-configuration changes, no persistence.

File selection and encryption modes

Every file that survives the exclusions gets one of three modes. Each mode encrypts some regions in place, in 0xFFFF-byte chunks. The mode is decided in the folder parser (FUN_1400b5fb0) and the per-file coroutine (FUN_1400ae840):

  1. Extension in the database set → mode 0, Full: the whole file.
  2. Otherwise, extension in the VM/disk-image set, or file size above 2,000,000 bytes → mode 2, Part: striped regions.
  3. Otherwise → mode 1, Spot (the binary’s own name): only the first percent % of the file.

Within every encrypted region, the first and last chunk use KCipher-2 and the chunks between use ChaCha8.

Mode Coroutine 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, the sample’s .zip, stage2 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.

Full-mode extensions (153). The list is decoded from obfuscated .rdata tables built by FUN_140003680 into set 0x1400f5e60; each table uses its own affine (a·b + c) mod 127 key:

.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

Forced-Part extensions (20). Built by FUN_140034500 into set 0x1400f5e38 from stack strings, each with its own key:

.vdi .vhd .vmdk .pvm .vmem .vmsn .vmsd .nvram .vmx .raw .qcow2 .subvol .bin .vsv .avhd .vmrs .vhdx .avdx .vmcx .iso

Cryptography

Each file gets four fresh random values, generated by a weakly seeded PRNG. They drive two stream ciphers and are sealed with RSA-4096. Every primitive below was reproduced in Python and matched against data captured live from the running sample.

Component Implementation Where Validation
RNG Nettle Yarrow-256 (two SHA-256 pools + AES-256 counter; 1,500 reseed iterations). A new instance per value, seeded only with QPC × 100 ns as a decimal ASCII string, e.g. "867151895800" FUN_14007b1f0, FUN_1400822c0 Python repro regenerated all 80 key bytes in a captured RSA blob from its four seeds
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 with key setup called as kbits = 0x40, so it takes the 16-byte branch: “expand 16-byte k”, key = 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, FUN_14007c570, FUN_14007beb0 128 B of live keystream matched only the 128-bit-key, 8-round variant (12/20 rounds and 256-bit failed). Live state showed counter 0x400 at chunk 3
Cipher B KCipher-2 (RFC 7008), key2 / iv2, byte-continuous keystream FUN_14007b4e0, FUN_14007c690, FUN_14007c9c0, FUN_14007c8a0 Model built from the binary’s tables reproduces RFC 7008 vector C.1 and matched 64 B of live keystream
Key wrap Nettle rsa_encrypt, PKCS#1 v1.5, embedded RSA-4096 key (e = 65537). Input is a 91-byte blob, output 512 B, 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

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, seeded the same QPC-only way (FUN_14007bbe0). The private key never touches the victim, so recovery cannot come from the RSA side.

Encryption flow. One uptime reading per value is all the randomness a file’s keys get. ChaCha8 and KCipher-2 then share each region’s chunks, and the RSA-sealed blob ends up in both the journal and the footer.

Encryption flow: key generation and the 6-step file pipeline

Encrypted file format and on-disk artifacts

An encrypted file is the original file, partly or fully encrypted in place, followed by a 512-byte RSA footer and renamed to .akira. The file grows by exactly 512 bytes. A per-file .arika journal exists only while encryption is in progress.

Per-file sequence (FUN_1400ae840), confirmed live:

  1. Open the target; if it is locked, run Restart Manager unlocking and retry.
  2. Create the journal <dir>\<name.ext>.arika with CreateFileW(GENERIC_READ|GENERIC_WRITE|DELETE, share R/W, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED).
  3. Write the journal header at offset 0.
  4. Encrypt the regions for the chosen mode. Before and after each chunk, overwrite journal bytes 0–8 with a progress record.
  5. Append the 512-byte RSA footer at offset = original size (FUN_1400b5a20).
  6. Rename the open handle to <name.ext>.akira with SetFileInformationByHandle(FileRenameInfo), then DeleteFileW the journal.
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 last processed chunk)
.arika header 9 11 0x01, mode, percent, original size
.arika header 20 512 The same RSA blob as the footer (total 532 bytes)

Because the journal carries the RSA blob and progress, files caught mid-encryption by a crash or kill stay decryptable by the operator. The folder parser also skips any file whose .arika already exists, so a restarted run does not double-encrypt it.

Other artifacts:

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.

Windows API calls by behaviour

Every malicious behaviour uses ordinary imports called straight through the import table. Nothing is resolved at runtime: the only GetProcAddress and LoadLibraryExW callers are CRT and STL helpers. 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 iterator helpers FUN_1400789d8, FUN_140078998, FUN_140078a4c, used by folder parser 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 with RmForceShutdown
Shadow-copy deletion via WMI CoInitializeEx, CoCreateInstance, CoSetProxyBlanket, SysAllocString ×7, VariantInit/VariantClear ×6 0x140071d8d → FUN_140071d80; 0x14007167d, 0x140071704 → FUN_140071620. Strings: ROOT\CIMV2, Win32_Process, Create, Win32_ProcessStartup (CreateFlags = 0, ShowWindow = 0), CommandLine, ReturnValue, ProcessId
Wait for the PowerShell child OpenProcess, WaitForSingleObject 0x140071f50, 0x140071f66 → FUN_140071d80; ProcessId is cast to short before the open
Key seed (the weakness) QueryPerformanceCounter, QueryPerformanceFrequency 0x140078ddd → clock wrapper FUN_140078dd4, called by the per-value Yarrow FUN_14007b1f0 and the RSA-padding Yarrow FUN_14007bbe0; no other entropy source
Open target GetFileAttributesW, SetFileAttributesW, CreateFileW ×2, GetFileSizeEx 0x1400aeea7–0x1400af41e → FUN_1400ae840. Clears read-only (attrs ^ 1); opens with GENERIC_READ\|WRITE\|DELETE, overlapped; on error 0x20/0x21 runs Restart Manager and retries
Journal CreateFileW 0x1400adc3f → FUN_1400adb00 (<name>.arika, CREATE_ALWAYS)
In-place encryption I/O ReadFile, WriteFile (overlapped), CreateIoCompletionPort, GetQueuedCompletionStatus, PostQueuedCompletionStatus 0x140040993 → FUN_1400407e0; 0x14004075d → FUN_1400405b0; asio IOCP scheduler FUN_1400385d0, FUN_140038e60
Rename and clean up SetFileInformationByHandle (FileRenameInfo), DeleteFileW 0x1400b1deb, 0x1400b1f02 → FUN_1400ae840

Three imports look suspicious but are library code:

For hunting, the useful API sequence is:

  1. WTSEnumerateProcessesW
  2. WMI Win32_Process.Create
  3. RmShutdown
  4. overlapped ReadFile/WriteFile
  5. SetFileInformationByHandle rename

Weaknesses and recovery outlook

The RSA side is sound, but every symmetric key is a deterministic function of one boot-relative timestamp. Recovery without paying is therefore possible. We verified the weakness and recovered keys from the sample’s real output in a controlled test (next section).

Weakness Verified? Recovery impact
Each of key1, iv1, key2, iv2 comes from a fresh Yarrow-256 seeded only with QPC×100 as decimal text (100 ns steps on a 10 MHz QPC) Yes: four captured seeds regenerated all key bytes Search the seed instead of the key: about 10⁷ candidates per second of uptime uncertainty
The four seeds are taken back-to-back per file (key1 → iv1 → key2 → iv2), microseconds apart in a normal run, and increase from file to file Yes; native gap measured at about 0.4 ms (run 4) Once one seed is found the rest sit in a narrow window; one solved file brackets the others
First chunk of every region is KCipher-2 under key2/iv2, over predictable headers (PK\x03\x04, %PDF, D0 CF 11 E0, “SQLite format 3”) Yes Cheap known-plaintext test for each (key2-seed, iv2-seed) candidate pair
ChaCha uses only key1[0:16] and 8 rounds Yes Does not shrink the seed search, but simplifies verification
Spot and Part leave data unencrypted (default: back half of small files, 60% of large and VM-disk files) Yes, by read offsets Large VM disks, archives and databases over 2 MB can often be partly carved without any key
Drive sweep visits only the first five drive letters Yes, disassembly Later volumes on hosts with many volumes stay untouched
Original size is stored and the footer is a fixed 512 B Yes Tools can strip the footer exactly

A practical recovery attempt would run in four steps:

  1. Estimate the QPC value at encryption time from the boot time (kernel boot events, LastBootUpTime) and each .akira file’s timestamps.
  2. Brute-force the key2 seed, then the iv2 seed a small delta later, testing the first-chunk header.
  3. Walk backwards from the key2 seed to the iv1 and key1 seeds.
  4. Decrypt with the models in this report.

The cost grows linearly with the uptime window and the gap span. Measured speeds and a cost table are in the next section.

Open question: QPC frequency differs on some hardware. The code handles any frequency, but non-10 MHz hosts give seeds that are not multiples of 100, which changes the search space.

Decryptor and key recovery

We built a decryptor that restores files encrypted by this sample, and a seed brute-forcer that recovers per-file keys without the operator’s RSA key. Both work on the sample’s real output: three files it encrypted in our VM (one per mode) were recovered from a seed window and decrypted byte-exact.

Decryption in plain English. The ransomware’s weak spot is how it picks its keys. It doesn’t use real randomness: each key is built from the computer’s uptime, counted in 100-nanosecond steps. Guess the uptime at the moment a file was encrypted and you can rebuild that file’s keys. Each file gets two keys, each with a companion value; most files only need the first key.

  1. 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 tells you its uptime when encrypted, to within a few milliseconds.
  2. Guess the key. Try every uptime value in that few-millisecond window, about 90,000 candidates. Each one rebuilds the key exactly as the ransomware did.
  3. 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: roughly 380 million pairs in total.
  4. Check each guess. Every PDF starts with %PDF-1., and every PNG starts with the same 16 bytes. Decrypt the file’s first few bytes with each pair; the pair that produces the right header is the correct one.
  5. Decrypt the file. With the right pair, decrypt the whole file, strip the 512 bytes the ransomware appended, and the original is back byte for byte.

Steps 2–4 are the slow part: about 30 minutes per file on our 20-thread analysis host in pure Python, or about 20 seconds with the optional numba engine (akira_fast.py, 40–45M seed pairs per second). All four run-5 files re-ran hash-exact in 16–26 s each. The ransomware’s master RSA key is never needed.

The tool is akira_decrypt.py: single file, Python 3.8+, standard library only, at D:\MalwareAnalysisVMs\AI-MalwareAnalysis\cases\2026-10-11_101bd45ab673_akira-ransomware\06-decryptor. It never overwrites unless told to (--force). Run it on copies of the encrypted files.

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 --uptime A:B --gap G1:G2 None: brute-forces the seeds No key material at all; needs a known header and an uptime estimate
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.

Decryption flow. Whatever the key source, the tool ends up with the same four values. It then replays the sample’s own chunk plan with both keystreams, so a decrypted file matches the original byte for byte.

Decryption flow: 4 key sources, 5 shared steps

How recover works. Every value the sample uses is the first 16 bytes of one fresh Yarrow draw, seeded only by uptime in nanoseconds. One table of candidate seeds therefore serves all four values.

  1. Stage 1. For each key2 seed in the uptime window and each candidate gap to the iv2 seed, run KCipher-2 over the first ciphertext bytes and compare with the known file header (at least 8 bytes, a 64-bit filter). This alone is enough for Spot files.
  2. Stage 2. For Full and Part files, walk back from the key2 seed to the iv1 and key1 seeds. The test is the first ChaCha chunk: known bytes, or a zeros/text heuristic. For a PDF, if that finds nothing, the same window is searched again with two PDF tests (object headers from the xref, or continuing a page-content stream’s deflate); see Stage-2 fallback for PDFs.
  3. Chaining. Seeds only increase, so one solved file brackets the next. It also reveals the real seed-to-seed gap, which narrows every later search.
Validation Result
Real sample output, all 3 modes, decrypted via --keys and via --seeds 6/6 byte-exact
Real sample output, all 3 modes, seeds recovered by recover (±100–300 ticks per dimension, 16 zero bytes of known plaintext, heuristic for ChaCha) All 4 seeds found per file, 3/3 decrypted byte-exact
Stress: 20,012,001 candidate pairs, 8-byte filter Single true pair found, no false positives, 69 s
Self-test vectors (RFC 7008, captured seeds, blobs and chunks, observed stripe offsets) 8/8 pass
Round trip with our own RSA-4096 key (footer, PEM/DER/PKCS#8, all modes, journal) 29/29 pass

Native seed gap (measured). The search cost depends on how far apart a file’s seeds are. Run 4 measured this without any seed breakpoints: a small logger patched into the sample’s own QueryPerformanceCounter wrapper recorded all 84 seeds of 21 files on 2 encryptor threads. Decrypting two of those files with the logged values proved they were the real seeds, and the timer path was checked free of debugger hooks.

Gap (100 ns ticks) 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

Consecutive seeds are about 0.4 ms apart. The floor 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, so recover now defaults to --gap 3800:8000.

The search runs about 150,000 KCipher-2 pair tests per second on the 20-thread analysis host in pure Python. The pair count is the key2 window in ticks times the gap span, so the uptime estimate is now what drives the cost.

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

On the two native-run files, a ±0.25 ms key2 window and the default gap window found the true pair among 21 million candidates in 70 s and 85 s.

Finding the window from victim timestamps. An .akira file’s last-write time lands 0.45–5.36 ms after its key2 seed (median 1.39 ms, all 21 run-4 files). The sample’s own operator log confirms it times itself with QPC: its final 423038 ms line is exactly main-end minus main-start QPC. So key2 ≈ (mtime − FILETIME of QPC 0) × 100, and recover --qpc-offset … --mtime … builds the window from that. How well the offset is known decides the cost:

Offset source Error vs true QPC 0 First-file window
Measured on the live victim (system time minus QPC, same boot) under 0.1 ms −8/+1 ms, ~42 min
WMI LastBootUpTime / Kernel-General 12 StartTime (0.5 s granularity) +352 ms ~±0.6 s, days
Kernel-General 12 TimeCreated +1,497 ms worse

One solved file then pins the offset for every other file of that boot. The ransom note’s creation time bounds a folder’s keys from below (--floor). Measured in 04-dynamic/05-timestamp-seed-anchors.md; a blind test found the gap_00 seeds from mtime and offset alone in 28 min.

Limits:

Does it work? Yes, on the sample’s own output. The three .akira files the live sample produced in run 3 started as zero-filled files, so a correct decryption must give back exactly that many zero bytes. The demo was re-run on 2026-10-11 from the case folder, and its log is artifacts/decryptor-demo-2026-10-11.txt.

Run Input Result
decrypt --seeds e2e_small.txt.akira (Spot, 12,288 B) 12,288 bytes, all zero; SHA-256 equals that of 12,288 zero bytes
decrypt --seeds e2e_db.sqlite.akira (Full, 196,899 B, partial last chunk) 196,899 bytes, all zero
decrypt --seeds e2e_big.bin.akira (Part, 3,000,000 B, 4 stripes) 3,000,000 bytes, all zero
Negative control key2 seed off by one tick (100 ns) 6,116 non-zero bytes: garbage, as it should be
Negative control recover with a wrong gap window No candidate found, no false positive
recover, no keys or seeds e2e_small, ±100-tick window, 16 known zero bytes key2 and iv2 seeds found in 0.3 s; file decrypted, all zero
recover, both stages e2e_db, ±100-tick windows All four seeds found in 0.8 s, including the ChaCha seeds from the zeros heuristic; file decrypted, all zero
Test suites selftest, real outputs, round trip 8/8, 9/9, 29/29 pass

One thing it cannot show: decryption through the operator’s RSA private key works only with our own test key, because the real one is not available.

Blind recovery of real files (run 5). On a freshly reverted VM, the live sample encrypted a PDF, a DOCX and two PNG screenshots. Only the shadow-copy and target-folder gates were in place, with no seed capture. Each file was then attacked with only what a responder has:

File Search time Result
1.pdf 31 min decrypted SHA-256 identical to the original
QC-STC-T-45-26_Sample_IR_Deliverables_v1.0.docx 28 min decrypted SHA-256 identical to the original
Screenshot 2026-10-05 102304.png 30 min decrypted SHA-256 identical to the original
Screenshot 2026-09-29 125745.png 81 min missed, then 21 min (iv2-anchored second pass) decrypted SHA-256 identical to the original (key2→iv2 gap 26,718 ticks)

The fourth file was a seed-gap outlier: its key2 and iv2 seeds were 26,718 ticks (2.67 ms) apart, close to the 27,109-tick outlier from run 4. The default pass (gaps ≤ 8,000 over the whole note-bounded window) missed it. A second pass with recover --anchor iv2 found it in 1,240 s, decrypting it hash-exact. That pass bounds iv2 instead of key2, at 2.5 ms before the mtime to 0.1 ms after, and tries gaps of 8,001–30,000 (572 M pairs). On all four files iv2 sat 0.8–1.8 ms before the mtime, so this second pass is the recommended follow-up whenever the default misses. Details: 04-dynamic/06-run5-blind-real-files.md.

Independent blind test (run 6). Six of seven in-scope files were recovered hash-exact, all by the default pass in about 25 s each. Before encryption we froze the driver (run6_blind_recover.py, SHA-256 ae0df7df…), the decryptor (870c6db9…) and the rules: pass 1 with the defaults, an automatic --anchor iv2 pass 2 on a miss, and fixed known bytes per file type. After a full VM revert the analyst placed eight files we had not seen. Their original hashes were sealed and read only after recovery. The same two safety gates were applied, then every breakpoint was removed. The post-run QPC offset agreed within 69 ticks across seven pairs.

File Size (B) Known bytes Pass Search key2→iv2 gap (ticks) Outcome
20358696.jpg 24,287 JFIF header, 11 1 14 s 4,173 Hash-exact
photo_2026-09-23_15-02-25.jpg 172,871 JFIF header, 11 1 26 s 4,072 Hash-exact
photo_2026-09-24_12-44-15.jpg 214,866 JFIF header, 11 1 26 s 4,139 Hash-exact
QC-STC-T-45-26_Sample_IR_Deliverables_v1.0.docx 42,333 ZIP header, 28 1 26 s 3,933 Hash-exact
Screenshot 2026-09-10 152951.png 26,953 PNG header, 16 1 26 s 3,993 Hash-exact
Screenshot 2026-09-29 125745.png 47,803 PNG header, 16 1 24 s 5,570 Hash-exact
Autopsy_DFIR_Validation_Playbook_Extended.pdf 481,630 %PDF-1., 7 1 22 s (KCipher-2), 154 s total 4,141 Keys found, output differs
Screen Recording 2026-09-24 104237.mp4 6,799,834 none – – – Out of scope

The PDF is over 262,140 bytes, so its encrypted half also holds ChaCha8 chunks. Its KCipher-2 keys were found, but stage 2 (iv1/key1 gaps 3,105–8,000, 24 M pairs) found no key that its zeros/text check accepts. This risk was written into the protocol before the run. The cause was settled afterwards: the check rejects compressed PDF data, and the seeds were inside the window (Stage-2 fallback for PDFs, below). No file needed pass 2, and iv2 sat 0.55–1.86 ms before the mtime. Two inputs repeat run 5, the DOCX and the 125745 PNG; they were encrypted with new keys. Details: 04-dynamic/07-run6-independent-blind.md.

Second build (build 2, run 1). Build 1’s decryptor, unchanged (870c6db9…), recovered 6 of 7 in-scope files from a second build hash-exact. Every file was found by the default pass in 15–30 s. The build is SHA-256 988776358d0e45a4907dc1f4906a916f1b3595a31fa44d8e04e563a32557eb42 (MD5 4aecef9ddc8d07b82a6902b27f051f34).

The static comparison showed the same key generation. There is no CSPRNG import. The per-value seed function FUN_140082c90 turns QPC×100 into a decimal string and seeds a new Yarrow with it. FUN_140036730 makes the same four draws (key1 32 B, iv1, key2, iv2) and builds the same 91-byte blob, which goes into the 512-byte RSA footer. The ChaCha constants, the 2,000,000-byte Part threshold, the CLI and the note are the same. Three things changed: the string-obfuscation constants (the shadow-copy command decodes with ((0x32 − b)·0x2A) mod 0x7F), the chat code 6729-HK-NIZN-WOPQ, and the operator RSA key. The gates were the WMI executor FUN_140078890, which was forced to return, and the root queue FUN_14007ac00, where r8 was checked.

The run followed the run-6 protocol. The driver (fc5d69fd…) differs from run 6’s only in the tool path, and it was frozen before detonation. After a revert, the analyst placed the files. A sealed pre-listing was taken, and the Ghidra copy imported before encryption matches it in all 13 hashes. No breakpoints were set during encryption. The operator log shows 2 threads and 36,814 ms. The QPC offset agreed within 111 ticks across 7 pairs.

File Size (B) Known bytes Pass Search key2→iv2 gap (ticks) Outcome
20358696.jpg 24,287 JFIF header, 11 1 15 s 4,900 Hash-exact
photo_2026-09-23_15-02-25.jpg 172,871 JFIF header, 11 1 26 s 5,380 Hash-exact
photo_2026-09-24_12-44-15.jpg 214,866 JFIF header, 11 1 30 s 3,896 Hash-exact
QC-STC-T-45-26_Sample_IR_Deliverables_v1.0.docx 42,333 ZIP header, 28 1 26 s 4,225 Hash-exact
Screenshot 2026-09-10 152951.png 26,953 PNG header, 16 1 28 s 4,306 Hash-exact
Screenshot 2026-09-29 125745.png 47,803 PNG header, 16 1 22 s 4,013 Hash-exact
Detection Lab Automated BAS-to-Splunk Detection Engineering Loop.pdf 595,456 %PDF-1., 7 1 22 s (KCipher-2), 155 s total 4,086 Keys found, output differs
rule.yml 981 none – – – Out of scope
AnyDesk.exe 8,375,224 – – – – Not encrypted (.exe excluded)

The PDF stopped at the same ChaCha8 stage-2 limit as run 6’s PDF, and was recovered afterwards (below). iv2 sat 0.91–1.91 ms before the mtime, and pass 2 never ran. The dynamic run therefore confirms what the static comparison could not: the mode and chunk layout, the cipher details and the timing. Details: case 2026-10-13_988776358d0e_akira-build2, 04-dynamic/01-b2run1-protocol.md.

Stage-2 fallback for PDFs (added 2026-10-14). Both PDFs are now recovered hash-exact. Their ChaCha8 seeds were inside the default window; the zeros/text check rejected the right key because the first ChaCha chunk starts in compressed data. In the run-6 PDF the whole ChaCha region (bytes 65,535–196,605) is one 291 KB page-content stream, and the check accepts 0 of 971 genuine 64-byte windows of it. After stage 1, the KCipher-2 chunks and the unencrypted tail are readable, which allows two PDF tests:

PDF Test Pairs searched Candidates Search time Result
Run 6 (build 1), 481,630 B deflate + page-content syntax 23,970,816 1 2,300 s standalone; 352 s in the decryptor SHA-256 = sealed hash
Build 2, 595,456 B 968 object headers from the xref 24,373,969 1 89 s standalone; 115 s in the decryptor SHA-256 = sealed hash

Seeds: run 6 566543569000, 566543980000, 566544406300, 566544820400; build 2 1219626222000, 1219626650700, 1219627041700, 1219627450300. All gaps are 3,910–4,287 ticks, ordinary for this sample.

Both tests are in akira_decrypt.py as a fallback. recover --stage2 auto (the default) runs the zeros/text check first and, for a .pdf, the PDF tests when it finds nothing; --stage2 pdf runs only them; --stage2 heuristic keeps the old behaviour. The default command line recovered both files hash-exact (8 min 16 s and 4 min 52 s), and the deflate test alone also solves the build-2 PDF. Tests: selftest 8/8, real outputs 15/15, round trip 29/29, and the new synthetic test_pdf_stage2.py 3/3. Neither test fits every PDF (no objects inside the ChaCha chunks plus a binary stream at the boundary), and other large compressed types have no stage-2 test yet. The blind results stay 6 of 7, because the frozen decryptor lacked the fallback; with it, 7 of 7 in-scope files on each build are recoverable. Details: 06-decryptor/stage2-investigation/ in both case folders.

The decryption routine. The full script is attached below as akira_decrypt.py (also in the case folder under 06-decryptor), 1,601 lines (version of 2026-10-14, with the PDF stage-2 fallback), SHA-256 091701682454a75dfb57324e29a6270977186f68209a7c3a93178ab76fe9fc4b. It is stored as .txt because the doc store does not accept .py; rename it after downloading. The core is short: regenerate the four values, rebuild the sample’s chunk plan, and XOR each chunk with the right keystream.

[image: akira_decrypt.py (attached as .txt)]

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

def decrypt_stream(fin, fout, size, mode, percent, keys):
    key1, iv1, key2, iv2 = keys
    k2 = KCipher2(key2, iv2)                    # RFC 7008
    cc = ChaChaSample(key1, iv1)                # 8 rounds, key1[:16] twice, iv1[:8]
    copy_first(fin, fout, size)                 # body without the 512-byte footer
    for off, n, cipher in chunk_plan(mode, percent, size):
        fin.seek(off)
        plain = (k2 if cipher == "k2" else cc).xor(fin.read(n))
        fout.seek(off); fout.write(plain)
    fout.truncate(size)

This excerpt is condensed for reading. In the script, copy_first is an inline loop, and journal handling, logging and argument checks are left out here. ChaChaSample.xor starts every chunk on a fresh 64-byte block, which reproduces the sample discarding partial blocks. recover prints key1 with its upper 16 bytes as zeros: ChaCha never reads them, so they are not searched.

Indicators of compromise

All indicators below come from this binary or were observed in the controlled run; none are taken from external reporting. The binary has no network, registry, mutex or service indicators: it opens no sockets, writes no registry keys and creates no named mutex.

Type Indicator Context
SHA-256 101bd45ab673fd6547386dff4339f1649e61b9f5c318260d365f766fbdc2d3c9 Sample
MD5 427eba9f6ff2cd312c72770f13e279c1 Sample
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
PE timestamp 0x658D8B15 (2023-12-28 14:49:57 UTC) Sample header
RSA public key SHA-256 of DER RSAPublicKey a40a55f2600b9199181bc6c0778e64b2e75145c4e6f715da6d23a72407539aaa; modulus begins c2f8f4dbbe7da09f7c991d69e00b4466 Embedded at file offset of VA 0x1400ee010; identifies the operator key pair
Onion (leak blog) akiral2iz6a7qgd3ayp3l6yub7xx2uep76idk3u2kollpj5z3z636bad.onion Ransom note
Onion (chat) akiralkzxzq2dsrzsrvbr2xgbbu2wgsmxryd4csgfameg52n7efvr2id.onion Ransom note
Victim / campaign code 6188-EA-ABYW-YGRG Ransom note chat login
File extension .akira Encrypted files
File extension .arika Temporary per-file journal (532-byte header)
File name akira_readme.txt Note in every traversed folder
File name pattern 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" Spawned hidden by WmiPrvSE.exe
CLI strings --encryption_path, --share_file, --encryption_percent, -localonly Operator launch arguments
File trait Encrypted file = original size + 512 bytes; last 512 bytes high-entropy Triage of .akira files
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

Detection

The most reliable early signal is the WMI-spawned PowerShell shadow-copy deletion, which happens before any file is encrypted. After that, mass .arika creation followed by .akira renames is unmistakable. The YARA 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*)) )
}

Behavioural detections:

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

MITRE ATT&CK mapping:

Technique ID Evidence
Data Encrypted for Impact T1486 ChaCha8 + KCipher-2 with RSA-4096 key wrap, .akira
Inhibit System Recovery T1490 Win32_Shadowcopy removal
Windows Management Instrumentation T1047 Win32_Process.Create in ROOT\CIMV2
PowerShell T1059.001 Shadow-copy command
Hidden Window T1564.003 Win32_ProcessStartup.ShowWindow = 0
Service Stop T1489 Restart Manager force-shutdown of processes holding files
Process Discovery T1057 WTSEnumerateProcessesW protected-PID list, RmGetList
File and Directory Discovery T1083 Recursive directory iteration
Network Share Discovery T1135 Mapped-drive and UNC classification; share list input
System Information Discovery T1082 GetSystemInfo CPU count sizes thread pools
Obfuscated Files or Information T1027 Stack-built strings with per-string mod-127 decoding

Analysis notes

Findings come from static analysis in Ghidra 12.1.2 and six controlled runs of this build, plus one of a second build (988776358d0e…), under x64dbg in an isolated Windows VM, followed by snapshot reverts. Crypto was checked by re-implementing each primitive in Python and comparing against live captures.

Function map (image base 0x140000000):

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 and 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
0x14007b1f0 Per-value Yarrow-256 instance seeded from QPC
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

The Python re-implementations used for validation became the decryptor and seed brute-forcer described in Decryptor and key recovery.