Research

We called it the encryptor. It was the decryptor.

We ran three Akira ransomware binaries through our own static analysis platform. Every automatic identification signal agreed on all three. One of them is the tool the operators hand you after you pay. Here is what separated them, why we missed it at first, and where our engine came up short.

In November 2025 CISA, the FBI, Europol, NCSC-NL and the German and French national agencies jointly re-issued their Akira advisory and called it an imminent threat to critical infrastructure: roughly 149 victims in 90 days, about $244M extorted. Encryptors now ship for Windows, Linux, VMware ESXi and Nutanix AHV.

We are building a static file-analysis product, so we took three real Akira binaries and pointed our own tools at them. This is the output, wins and misses.

Nothing here was executed. Our platform has no sandbox and does not detonate anything; it reads bytes. The costs of that are visible further down.

The three samples

All three published on MalwareBazaar, all first seen 2025-06-11.

A: Windows encryptorB: ESXi encryptorC: ESXi 7 decryptor
sha2562727c73f3069457e9ad2197b3cda25aec864a2ab8da3c2790264d06e13d45c3d0ee1d284ed663073872012c7bde7fac5ca1121403f1a5d2d5411317df282796c176493de8de0284f0dc05ee75fb849ba27ee4a246666c508eaf003c2ea83a6e7
size1,029,120 B4,625,664 B3,018,192 B
formatPE32+ x86-64ELF64 DYN/PIE, dynamicELF64 EXEC, static
languageC++ / MSVCRustC++ / GCC 8.5.0
symbols153 imports3,6120
cryptoGMP + ChaCha/Salsa constantrust-crypto, curve25519-dalek, sha3nettle AES-256 + RSA/PKCS#1

Three binaries from one operation: two encryptors and the decryptor that undoes them.

Everything automatic said the same thing

Field by field, straight out of our report.

What our platform emittedA (encryptor)B (encryptor)C (decryptor)
VerdictMaliciousMaliciousMalicious
EvidenceTeam Cymru + MalwareBazaarMalwareBazaar onlyMalwareBazaar only
ClamAVengine was downengine was downengine was down
Team Cymru MHRlistednot listednot listed
YARA (12,388 rules, release 20260802)1 hit0 hits1 hit
Akira .onion addressesboth presentboth presentboth present
Ransom note in binaryyesyesyes
ATT&CK capabilities2 (both noise)nonenone

The decryptor carries the full ransom note, the akira_readme.txt filename, the .akira extension and both Tor negotiation addresses, because it is shipped to the same victim in the same transaction. Every indicator an analyst would pivot on is identical to the Windows encryptor's.

RADECTI analysis-engine table for the Akira Windows encryptor: 6 engines run, 6 checked. Team Cymru lists the hash, MalwareBazaar returns Akira family first seen 2025-06-11, DetectItEasy identifies Microsoft Visual C/C++.
A, the Windows encryptor. Six engines, all answered.
RADECTI analysis-engine table for the Akira ESXi decryptor: 7 engines run, 7 checked. MalwareBazaar returns the same Akira family attribution as the encryptor.
C, the decryptor. Seven engines, the same family verdict.

Those two panels are the problem in one image. The file that encrypts and the file that restores return the same family attribution, from the same source, with the same confidence. Every engine is answering "whose code is this", and the question an incident responder has at 02:00 is "what does it do to my data". Correction, 7 September 2026: the ClamAV row in both is void, see the note at the top.

RADECTI engine table for the Rust ESXi encryptor: ClamAV no signature match, Team Cymru not in registry, CIRCL unknown, MalwareBazaar Akira family, DetectItEasy unavailable.
Per-engine results for the ESXi encryptor. One engine of five carries the verdict; the toolchain identifier failed, and the report says so.
Correction, 7 September 2026: the ClamAV row here is not a result. The daemon was dead and the product printed its failure as "no signature match". Screenshot left as published.
RADECTI verdict-evidence panel: This malicious verdict is based on the following concrete detection. MalwareBazaar (abuse.ch), confirmed sample in public malware corpus, known family Akira.
Every verdict names the detection it rests on. Here, one source.
RADECTI family-match panel: 1 rule matched, DITEKSHEN_MALWARE_Win_Akira, score 75, by ditekSHen, Detects Akira Ransomware Windows, with a link to the rule source.
Family matches carry the rule name, its score, its author and a link to its source.

Family attribution and role attribution are different problems, and almost every static signal answers only the first one. Hash registries and YARA family rules both tell you whose code this is. Neither tells you whether it destroys data or restores it.

The one field that told the truth, and why we couldn't see it

The decryptor announces itself, in a place our own product was throwing away.

/mnt/z/manco_work/gittedprojects/esxi7_decrypt/argh.h
/mnt/z/manco_work/gittedprojects/esxi7_decrypt/cryptolib/nettle_rsa/aes256/aes256-encrypt.c
/mnt/z/manco_work/gittedprojects/esxi7_decrypt/./asio/include/asio/detail/posix_event.hpp

--decryption_path
Number of threads to decrypt =
(Full begin) Decrypt exception:
Read encrypt info from file failed!          <- it READS the header an encryptor WRITES

Sixteen occurrences of that build path, all compiler-emitted __FILE__ expansions that nobody types by hand. With the inverted verb on every log string, the role is unambiguous.

We did not see any of it on the first pass, and the reason was ours. Our analyst-facing string view is capped at 3,000 runs, and it selected them in file order. On a 3 MB statically-linked stripped binary the first 3,000 printable runs are all still inside .text, with a mean length of 7.9 characters and about 44% of them x86 register-save sequences like AWAVAUATSH. The word akira appeared zero times in the list an analyst reads. The IOC extractor, which scans all 5,200 runs, had found both onion addresses the whole time.

We changed the selection to rank by informativeness rather than file position, re-scanned the same bytes, and the conclusion changed from "encryptor" to "decryptor". Same file, same engine, one heuristic.

The evidence was in the file. Our engine read it, discarded it at the display layer, and produced a confident, wrong role attribution. The fix shipped before this post did.

Sample B: the Rust ESXi encryptor

Structurally the opposite of the decryptor, and by far the most readable Akira artefact we have seen.

45.2%

of the binary is DWARF debug information

3,612

symbols; it was never stripped

0

YARA rules in our corpus matched it

8 .debug_* sections totalling 2,088,943 bytes of 4,625,664. Add the symbol and string tables and 53.1% of the shipped payload is analyst-facing metadata. This is a cargo build that never had strip run on it, and it means assumptions built on the older, stripped C++ ESXi line are stale.

Its own --help, without running it

The binary is a seahorse CLI app. Its main function builds the flag table with nine consecutive Flag::new(ptr, len) / Flag::description(ptr, len) pairs. The pointers are contiguous, so the table reassembles arithmetically against the string blob across eleven consecutive boundaries, no slack:

flagtypedescription, verbatim from the binary
--pathStringStart path. Default value: /vmfs/volumes
--idStringBuild ID
--stopvmBoolStop VMs
--vmonlyBoolCrypt only .vmdk, .vmem, .vmx, .log, .vswp, .vmsd, .vmsn files
--threadsIntNumber of threads (1-1000). Default: number of logical CPU cores
--epIntPercent of crypt. Default - 15%
(4 chars)BoolWork in background
--logsStringPrint logs. Valid values: trace, debug, error, info, warn. Default: off
--excludeStringSkip files by "regular" extension

Application identity akira_v2, version string 2024.1.30. The version is not in the strings at all. It lives in a compiler immediate and is only recoverable through the decompiler.

What --stopvm actually does

Fully established without execution. The decompiled function reads the flag, then:

buf = alloc(0x54);                        // 84 bytes
memcpy(buf, 0x4197a, 0x54);
std::fs::write(0x38de0, 0x10, buf, 0x54);   // 16-byte path
pid = fork();
    execlp("sh", "sh", "/tmp/stop_vms.sh", 0);
    unlink(0x38de0, 0x10);                // deletes its own script
    expect(r, 0x41a30, 0x13);             // 19-char message

Three length identities settle it, and anyone can check them:

len("/tmp/stop_vms.sh")                                  == 16 == 0x10
len("vim-cmd vmsvc/getallvms | tail -n +2 | awk
     '{system(\"vim-cmd vmsvc/power.off \" $1)}'")        == 84 == 0x54
len("Failed to stop vms:")                               == 19 == 0x13

So: write the vim-cmd one-liner to /tmp/stop_vms.sh, fork, execlp it through sh, then unlink the script. It powers off every VM the host knows about, then encrypts the datastore underneath them.

Targeting

Default root /vmfs/volumes. Skips /proc /sbin /lib64 /locker /productLocker /bootbank /altbootbank /tardisks /vmfs/devices and the boot volumes. Skips files with a .dontdel extension, skips vSphere Cluster Services agent VMs (vCLS file skipped:), and handles VMFS locks explicitly (VMFS lock on file removed:). Ships an indicatif progress bar with an ETA counter and emoji status markers, a friendlier console UI than most commercial CLI tools.

Its ransom note differs from the C++ line: akiranew.txt, encrypted-file prefix akiranew_, opening "Hi friends,". Samples A and C both use akira_readme.txt and .akira.

Sample A: the shadow copies are not where you'd grep for them

The Windows encryptor's most useful detail is a detection-engineering one.

The command that deletes volume shadow copies is present in .rdata in plaintext. A strings-based rule finds it. But that is not the copy that runs. The executed copy is built on the stack from 76 constants and transformed at runtime:

do {
    iVar1 = (int32_t)((0x39 - enc[i]) * 0xb) % 0x7f + 0x7f;
    plain[i] = iVar1 % 0x7f;
} while (i < 0x4c);

We lifted the constants out of the pseudocode and ran the transform:

plain = bytes(((((0x39 - b) * 11) % 127) + 127) % 127 for b in enc)
→ b'powershell.exe -Command "Get-WmiObject Win32_Shadowcopy | Remove-WmiObject"\x00'

75 characters plus NUL, exactly the 0x4c the loop counts. It is then handed to a WMI client (ROOT\CIMV2, Win32_Process, Create, CoSetProxyBlanket) and the process waits up to 15 seconds for the deletion to finish. Hunt the WMI process-creation behaviour, not the string.

Unlocking files without killing the box

All five RstrtMgr.DLL imports resolve to a single function. It registers the target file with the Windows Restart Manager, probes RmGetList against ERROR_MORE_DATA, and calls RmShutdown on every process holding the file, except its own PID and a protected set built from WTSEnumerateProcessesW against a hardcoded list of twenty Windows core processes (lsass.exe, csrss.exe, winlogon.exe, …). It unlocks the file without pulling the OS out from under itself.

Worth noting what is absent: WS2_32.dll contributes only WSAStartup and WSACleanup. No socket, connect, send, recv, no WinINet, no WinHTTP. There is no command-and-control capability in this binary's import table. The only network artefacts are the two Tor addresses in the note, which are for victim negotiation.

Where our engine came up short

Three places our own tooling failed on these samples.

RADECTI coverage banner: Analysis incomplete. 2 parts of this file were not fully read. Findings below are limited by that, not a clean result.
Every scan opens with what it could not read. An empty findings list under this banner means we ran out of reach, not that the file is clean.

Our capability engine found "checks the system language"

On a live ransomware encryptor, our ATT&CK capability analysis returned exactly two techniques: System Language Discovery and Debugger Evasion, the second of which our own UI labels as close to background noise. No T1486. No Impact technique at all.

RADECTI capability panel on the Akira Windows encryptor, showing only System Language Discovery and Debugger Evasion, both low confidence.
The full ATT&CK output for a working ransomware encryptor.

The rule behaved correctly. It requires an imported encryption API (CryptEncrypt, BCryptEncrypt and friends), and Akira statically links its own cipher and bignum library, importing zero Windows crypto APIs. An import-table shape matcher looks for encryption, finds none, and declines to fire. That is the ceiling on import-table capability analysis.

Our YARA corpus matches the decryptor and misses the current payload

We match against YARA-Forge release 20260802, 12,388 curated rules. It holds four Akira-named rules. Against our three samples:

ruleA (Win)B (ESXi Rust)C (ESXi C++ decryptor)
DITEKSHEN_MALWARE_Win_Akirahit
DITEKSHEN_MALWARE_Linux_Akirano hithit
MALPEDIA_Win_Akira_Autono hit
SIGNATURE_BASE_MAL_WIN_Akira_Apr25no hit
RADECTI panel reading: Known malware families. No rule in our family corpus matched this file.
B, the current ESXi payload. Nothing matched.
RADECTI panel showing a YARA family match on DITEKSHEN_MALWARE_Linux_Akira with author and source link.
C, the decryptor. Matched, named, attributed.

The scan ran in 384 ms, all 12,388 rules, zero matches on the Rust encryptor. Family rules are written against a codebase, and the Rust line is a different codebase, so this says nothing about evasion by the operators or about the quality of ditekSHen's rule. The operational consequence: if your Linux Akira coverage is that rule, you are covered for the recovery tool and not for the current ESXi payload. Check yours against all four names.

Our disassembler times out on the easy binary and lies about the hard one

samplewhole-binary analysisresult
A: 1.03 MB PE4.3 – 4.9 s1,466 functions; partition sane
C: 3.02 MB stripped static ELF10.9 – 13.3 s8,911 functions; top 300 claim 15× the entire code segment
B: 4.63 MB unstripped Rust PIE45.2 s, three times runningtimed out

We expected the timeout on the stripped binary. It happened on the one with 3,612 symbols: monomorphised Rust produces more graph than the analysis finishes in 45 seconds. And on the stripped ELF the analysis completes while being wrong: function boundaries overlap so badly that the top 300 sum to fifteen times the executable segment. A completed analysis and a correct one are not the same thing.

Everything in the Rust section above came from per-function decompilation, which runs without a whole-binary pass and was immune to both failure modes. That part of the design held up.

Three smaller ones, for completeness

  • Our ELF parser reported the Rust compiler version as rustc-demangle-0. It had matched a crate directory name, not a compiler. The real identifier, a rustc commit hash, was in the same file and we did not extract it.
  • The third-party toolchain identifier failed outright on the one sample where it would have mattered most. We rendered the failure rather than swallowing it, which is the right behaviour, but it still failed.
  • Our IOC extractor classified 98 Rust source filenames as domains (mod.rs, lib.rs, sosemanuk.rs), plus local.tgz.ve, a slice of an ESXi exclusion path that happens to end in a live ccTLD. One click from being published as an Akira indicator.

What we cannot tell you

The limits are the product, so we publish them.

  • Nothing was executed. We have no sandbox. We can tell you what is in these files; we cannot tell you what they did on a victim network.
  • Which cipher encrypts file bytes, in any of the three. Sample A has a Salsa/ChaCha constant string and links GMP. Sample B links four crypto crates including rust-crypto, curve25519-dalek and sha3. Sample C vendors nettle. None of that is the same as reading the cipher core, which we did not do, so we are not naming a cipher suite here.
  • Key handling, key wrapping, or whether anything is decryptable. Static analysis cannot answer this.
  • Sample A's command-line semantics. The flags are present as strings; per-function decompilation of its argument parser returned the wrong function.
  • Whether the C++ and Rust lines share authorship. They differ in language, concurrency model, crypto stack, note filename and build host. That is a difference between codebases, not evidence about people.
  • We publish no detection ratio. We run local ClamAV and hash-only reputation lookups. A "2 of 3" would be arithmetic, not information.

For the record, ClamAV produced no signature match on any of the three. The Malicious verdicts came from hash registries, and on two of the three from a single registry.

Void, 7 September 2026. The ClamAV sentence above is wrong: the daemon was not running on the day these samples were analysed, and its failure was rendered as a clean result. See the correction at the top. The second sentence stands: the verdicts did come from hash registries.

Detections you can use today

All derived from artefacts above. Nothing here helps an operator.

  • File creation of /tmp/stop_vms.sh on an ESXi host, any content, any process. The encryptor deletes it immediately afterwards, so a create-then-unlink pair is the stronger signal.
  • sh /tmp/stop_vms.sh executed with an unexpected parent binary.
  • vim-cmd vmsvc/power.off invoked in a loop driven by awk, especially following vim-cmd vmsvc/getallvms.
  • On Windows: WMI Win32_Process.Create spawning powershell.exe with a Win32_Shadowcopy removal command. Do not rely on matching the string in the file; the executed copy is assembled at runtime.
  • Restart Manager API sequence (RmStartSessionRmRegisterResourcesRmGetListRmShutdown) from an unsigned binary, paired with WTSEnumerateProcessesW.
  • Files named akiranew.txt or with an akiranew_ prefix, from the Rust ESXi line, distinct from the older akira_readme.txt / .akira.

Reproduce this

Every claim above is checkable against public samples.

Samples (MalwareBazaar, all first seen 2025-06-11):
  A  2727c73f3069457e9ad2197b3cda25aec864a2ab8da3c2790264d06e13d45c3d
  B  0ee1d284ed663073872012c7bde7fac5ca1121403f1a5d2d5411317df282796c
  C  176493de8de0284f0dc05ee75fb849ba27ee4a246666c508eaf003c2ea83a6e7

YARA corpus:  YARA-Forge release 20260802, packages/full — 12,388 rules
Rules fired:  DITEKSHEN_MALWARE_Win_Akira, DITEKSHEN_MALWARE_Linux_Akira
              (ditekSHen, github.com/ditekshen/detection, yara/malware.yar)
Did not fire: MALPEDIA_Win_Akira_Auto, SIGNATURE_BASE_MAL_WIN_Akira_Apr25

Sample B identity:  akira_v2 2024.1.30, crates akira_v2 / akira_utils
Sample B rustc:     commit 79e9716c980570bfd1f666e3b16ac583f0168962
Sample C build:     GCC 8.5.0, RHEL/CentOS 8 build host

How we handled them: each sample stayed inside its password-protected archive on disk and was unpacked in memory only. Nothing was ever written to disk as an executable, nothing was given an execute bit, and nothing was uploaded to any third-party sandbox. The only thing that left our infrastructure was a hash. Everything ran on EU-owned, EU-hosted infrastructure.

The takeaway we would want a defender to leave with: a family name is not a verdict about behaviour. Three binaries from one operation, one of which restores your files, were indistinguishable to every automated identification signal we produce. What separated them was a build path in a string table, and we only saw it after fixing a bug in our own display layer. Check what your tools are actually telling you, and check what they are quietly leaving out.