Yen Wang

0x10Work / Malware reverse engineering

Akira v2, read from the binary

Akira's Rust rewrite, taken apart at byte level: struct layouts from drop glue, the real crypto stack, and the author's code inside 3,274 functions.

When 2026 Role Independent analysis, part of my PhD Tools Binary Ninja, Ghidra, Hex-Rays, rustc 1.74.0
3,274
functions in the binary
~12
written by the malware author
29
library versions pinned as ground truth
32 B
lock-state struct, recovered from drop glue

The sample

Akira v2 for Linux, x86-64, built with rustc 1.74.0 (commit 79e9716c980570bfd1f666e3b16ac583f0168962).

SHA-256 0ee1d284ed663073872012c7bde7fac5ca1121403f1a5d2d5411317df282796c

Most of a Rust binary is library code. Of 3,274 functions, about 12 are the author's. The work is finding those 12 and reading them in Rust's terms, not C's.

Ground truth first

I pinned the exact library versions the binary was built with: 29 crates. With the matching source tree, panic sites act as anchors. Each one ties a block of disassembly to a known file and line, and I checked the mapping by counting operations on both sides.

The crypto stack is curve25519-dalek 4.1.1, rand_chacha 0.3.1 and rust-crypto 0.2.36. There is no rsa crate and no chacha20 crate in the build. This corrects the crypto description in parts of my Medium series.

A struct, recovered from its destructor

Rust emits drop glue to free each type, and that code has to know the type's size. In Arc::drop_slow, the loop advances with add r12, 0x20 and sizes its buffer with shl rsi, 0x5. Both say 32 bytes.

So akira_v2::lock::Attempts is 32 bytes, aligned to 8, with four 8-byte slots: a Box<str> pointer at +0x0, its length at +0x8, and two u64 fields at +0x10 and +0x18.

My first reading said 24 bytes. It had pulled in a Vec<Entry> from std::sync::mpmc::waker and a Vec<PathBuf> drop region that belong to something else. The constructor at 0x4d390e also gave the bare Vec header order for this build: pointer at +0x18, capacity at +0x20, length at +0x28.

A benign twin as a control

To check my readings against something I fully know, I built an inert twin with the same structure, pinned to rustc 1.74.0 with a committed Cargo.lock. Getting it to build was its own problem. curve25519-dalek 4.1.1 pulled in zeroize 1.9.0, which cargo 1.74.0 cannot parse. I wrote a build loop that reads each parse failure, finds the newest release of the offending crate that supports 1.74, and pins it. zeroize landed on 1.8.2.

The twin built cleanly and its akira_v2::lock::lock lifted at 59 basic blocks, against 64 in the sample.

Corrections I made to my own work

First claimCorrected toWhy
Attempts is 24 bytes32 bytesDrop-glue stride and capacity sizing
Keccak is f1600Parametric p1600 with a runtime round countTraced the keccak_p<L> monomorphisation for u64
SSA is 1.7x the size of plain MLIL1.52x by bytes, 1.40x by linesRe-measured on the whole binary
RSA and ChaCha20 in the buildNeither crate is presentVersion lock against the real dependency tree