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 encryptor | B: ESXi encryptor | C: ESXi 7 decryptor | |
|---|---|---|---|
| sha256 | 2727c73f3069457e9ad2197b3cda25aec864a2ab8da3c2790264d06e13d45c3d | 0ee1d284ed663073872012c7bde7fac5ca1121403f1a5d2d5411317df282796c | 176493de8de0284f0dc05ee75fb849ba27ee4a246666c508eaf003c2ea83a6e7 |
| size | 1,029,120 B | 4,625,664 B | 3,018,192 B |
| format | PE32+ x86-64 | ELF64 DYN/PIE, dynamic | ELF64 EXEC, static |
| language | C++ / MSVC | Rust | C++ / GCC 8.5.0 |
| symbols | 153 imports | 3,612 | 0 |
| crypto | GMP + ChaCha/Salsa constant | rust-crypto, curve25519-dalek, sha3 | nettle 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 emitted | A (encryptor) | B (encryptor) | C (decryptor) |
|---|---|---|---|
| Verdict | Malicious | Malicious | Malicious |
| Evidence | Team Cymru + MalwareBazaar | MalwareBazaar only | MalwareBazaar only |
| ClamAV | engine was down | engine was down | engine was down |
| Team Cymru MHR | listed | not listed | not listed |
| YARA (12,388 rules, release 20260802) | 1 hit | 0 hits | 1 hit |
| Akira .onion addresses | both present | both present | both present |
| Ransom note in binary | yes | yes | yes |
| ATT&CK capabilities | 2 (both noise) | none | none |
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.
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.
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.
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:
| flag | type | description, verbatim from the binary |
|---|---|---|
--path | String | Start path. Default value: /vmfs/volumes |
--id | String | Build ID |
--stopvm | Bool | Stop VMs |
--vmonly | Bool | Crypt only .vmdk, .vmem, .vmx, .log, .vswp, .vmsd, .vmsn files |
--threads | Int | Number of threads (1-1000). Default: number of logical CPU cores |
--ep | Int | Percent of crypt. Default - 15% |
(4 chars) | Bool | Work in background |
--logs | String | Print logs. Valid values: trace, debug, error, info, warn. Default: off |
--exclude | String | Skip 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.
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.
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:
| rule | A (Win) | B (ESXi Rust) | C (ESXi C++ decryptor) |
|---|---|---|---|
DITEKSHEN_MALWARE_Win_Akira | hit | – | – |
DITEKSHEN_MALWARE_Linux_Akira | – | no hit | hit |
MALPEDIA_Win_Akira_Auto | no hit | – | – |
SIGNATURE_BASE_MAL_WIN_Akira_Apr25 | no hit | – | – |
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
| sample | whole-binary analysis | result |
|---|---|---|
| A: 1.03 MB PE | 4.3 – 4.9 s | 1,466 functions; partition sane |
| C: 3.02 MB stripped static ELF | 10.9 – 13.3 s | 8,911 functions; top 300 claim 15× the entire code segment |
| B: 4.63 MB unstripped Rust PIE | 45.2 s, three times running | timed 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), pluslocal.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-dalekandsha3. 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.shon 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.shexecuted with an unexpected parent binary.vim-cmd vmsvc/power.offinvoked in a loop driven byawk, especially followingvim-cmd vmsvc/getallvms.- On Windows: WMI
Win32_Process.Createspawningpowershell.exewith aWin32_Shadowcopyremoval command. Do not rely on matching the string in the file; the executed copy is assembled at runtime. - Restart Manager API sequence (
RmStartSession→RmRegisterResources→RmGetList→RmShutdown) from an unsigned binary, paired withWTSEnumerateProcessesW. - Files named
akiranew.txtor with anakiranew_prefix, from the Rust ESXi line, distinct from the olderakira_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.