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:
- Encrypts local, mapped and UNC paths using a multi-threaded IOCP pipeline.
- Deletes Volume Shadow Copies through WMI-launched PowerShell.
- Force-closes processes holding target files open, using Restart Manager. System processes are spared.
- Uses hybrid crypto: per-file ChaCha8 + KCipher-2 keys, sealed with an embedded RSA-4096 public key.
- Uses three encryption modes (full, first-N-percent, striped), chosen by file extension and size.
- Drops a ransom note in every traversed folder.
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.

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):
X:with a mounted local volume → local disk.X:reported asDRIVE_REMOTE, or as a no-root-dir mapped share → network disk.- A UNC path (
PathIsNetworkPathW) → network path. - Anything else → “Not allowed disk” and skipped.
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.
- Skip list update (
FUN_14006ecf0). Adds.akiraand.arikato the extension skip set, so encrypted and in-progress files are never re-encrypted. - Protected-process snapshot (
FUN_140070c30).WTSEnumerateProcessesWlists running processes, and the PIDs whose names match the 20-name protected set are stored. These PIDs are later exempt from Restart Manager shutdown. - Shadow-copy deletion (
FUN_140071d80→FUN_140071620).- Decodes a 76-byte stack string with
((0x39−b)·0x0B) mod 0x7Fintopowershell.exe -Command "Get-WmiObject Win32_Shadowcopy | Remove-WmiObject". - Launches it through COM/WMI in
ROOT\CIMV2:Win32_Process.Create, withWin32_ProcessStartup.ShowWindow = 0(hidden) andCreateFlags = 0. PowerShell therefore appears as a child ofWmiPrvSE.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.
- Decodes a 76-byte stack string with
Restart Manager unlocking (FUN_140070e30) runs per file, only when the file cannot be opened:
RmStartSession, thenRmRegisterResourceson the file, thenRmGetListto get the processes holding it.- If any holder is the ransomware itself or a protected PID, it calls
RmEndSessionand gives up on that file. - 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):
- Extension in the database set → mode 0, Full: the whole file.
- Otherwise, extension in the VM/disk-image set, or file size above 2,000,000 bytes → mode 2, Part: striped regions.
- 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.

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:
- Open the target; if it is locked, run Restart Manager unlocking and retry.
- Create the journal
<dir>\<name.ext>.arikawithCreateFileW(GENERIC_READ|GENERIC_WRITE|DELETE, share R/W, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED). - Write the journal header at offset 0.
- Encrypt the regions for the chosen mode. Before and after each chunk, overwrite journal bytes 0–8 with a progress record.
- Append the 512-byte RSA footer at offset = original size (
FUN_1400b5a20). - Rename the open handle to
<name.ext>.akirawithSetFileInformationByHandle(FileRenameInfo), thenDeleteFileWthe 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:
akira_readme.txtwritten withstd::ofstreaminto every folder the parsers enter (FUN_140042420). The text is plaintext at0x1400ef060. - Log file:
Log-<dd>-<mm>-<YYYY>-<HH>-<MM>-<SS>.txtin the process’s working directory, written by an spdlog-stylefile_logger. Lines include “This is local disk:”, “Number of threads to encrypt =”, “file rename failed. System error:”, and per-file failures such as “Failed to make full encrypt! (“. - Process: a hidden
powershell.exespawned byWmiPrvSE.exefor shadow-copy removal.
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:
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 SetFileInformationByHandlerename
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:
- Estimate the QPC value at encryption time from the boot time (kernel boot events,
LastBootUpTime) and each.akirafile’s timestamps. - Brute-force the key2 seed, then the iv2 seed a small delta later, testing the first-chunk header.
- Walk backwards from the key2 seed to the iv1 and key1 seeds.
- 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.
- 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.
- 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.
- 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.
- 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. - 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.

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.
- 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.
- 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.
- 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:
- The operator-key path is tested only with our own RSA key.
- The native seed gap was measured on one 2-vCPU VM. A faster or slower victim CPU shifts it, so widen –gap toward 3800:30000 if a search finds nothing.
- Seed recovery needs a predictable header for stage 1, and plaintext-looking content in the first ChaCha chunk (or known bytes there) for stage 2. In run 6 and on build 2 that check found nothing on real PDFs. The PDF stage-2 fallback now recovers both; other large compressed files (Office documents, archives, video) may still come back only partly decrypted.
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:
- its
.akiracopy and last-write time; - the QPC offset measured on the same boot after the attack;
- known plaintext from the file type: the PNG header,
%PDF-1.(7 bytes), or the DOCX’s first ZIP header rebuilt from its unencrypted central directory.
| 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:
- xref: the cross-reference tables or streams give the exact offset of every
N G objheader. Headers inside a ChaCha chunk are exact known plaintext, and the ChaCha8 keystream block at any offset can be computed directly, so each candidate costs one 64-byte block. The test is used only if every header in readable bytes sits where the xref says, and a candidate needs 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 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.
| 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.
- Run 1 used
-p <path>. It escaped the folder because of the argh parsing behaviour above and was stopped early. - Run 2 used
--encryption_path=<folder> -localonly. A breakpoint on the root-folder queue checked the target before any encryption. The shadow-copy call was forced to return. - Test files: zero-filled
zero_small.txt(0x3000 B),zero_db.sqlite(0x30000 B) andzero_big.bin(3,000,000 B), so ciphertext equals keystream. -
Not covered: share-file input was read statically only; no live run used
--share_file. Part mode with percent < 50 was not exercised. - Run 3 encrypted zero-filled
e2e_small.txt,e2e_db.sqliteande2e_big.binwhile their seeds and RSA blobs were captured. Those real.akirafiles are the regression fixtures for the decryptor and the seed brute-forcer. - Run 4 measured the native seed gap with no seed breakpoints.
- An in-process logger on the sample’s
QueryPerformanceCounterwrapper recorded every reading, and an exit trap kept the finished process alive for reading. - Re-attaching stalled on a ScyllaHide dialog, so the encryption ran attached with no breakpoints; the only debug events were thread and DLL events, with no exceptions.
- The timer path was confirmed unhooked.
- An in-process logger on the sample’s
- Run 5 encrypted four analyst-provided real files with no seed capture, after the same two safety gates; all four were recovered blind, hash-exact (the fourth by an iv2-anchored second pass). Read-only PowerShell probes, launched from x64dbg, collected file times, clock pairs and boot events on the VM for the timestamp study.
- Run 6 was the independent re-test: after a full revert, eight analyst-chosen files were encrypted with no seed capture, under rules frozen beforehand. Six of seven in-scope files were recovered blind and hash-exact. The PDF’s ChaCha chunks were not recovered in the blind run (the PDF stage-2 fallback recovered them afterwards), and the MP4 had no known-plaintext rule. Probes were run with x64dbg’s
initcommand, becauseLoadBinarydrops command-line arguments. - Build 2 (
988776358d0e…, case2026-10-13_988776358d0e_akira-build2): a static comparison with this build, then one run under the run-6 protocol, decrypted with this build’s decryptor unchanged. 6 of 7 in-scope files were hash-exact; the PDF’s ChaCha chunks were not recovered in the blind run, and the PDF stage-2 fallback recovered them afterwards.
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.