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.
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 claim | Corrected to | Why |
|---|---|---|
Attempts is 24 bytes | 32 bytes | Drop-glue stride and capacity sizing |
Keccak is f1600 | Parametric p1600 with a runtime round count | Traced the keccak_p<L> monomorphisation for u64 |
| SSA is 1.7x the size of plain MLIL | 1.52x by bytes, 1.40x by lines | Re-measured on the whole binary |
| RSA and ChaCha20 in the build | Neither crate is present | Version lock against the real dependency tree |