Yen Wang

0x20Writing

Reversing Akira's Rust Experiment: A Breakdown, Part 2

A breakdown and commentary on Check Point Research's analysis of Akira ransomware's Rust rewrite.

Published 20 May 2026 Read 38 min Tags malware, rust, reverse-engineering

Originally published on RE: Exploit (Medium). Read the original ↗

This post is a breakdown and commentary on Ben Herzog’s Check Point Research (December 2024), Inside Akira Ransomware’s Rust Experiment & his talk in 2025. All technical claims are grounded in the linked primary sources below. Combined with some methodologies of Cindy Xiao’s amazing reports/talks in 2024, 2025 (mainly for reconstructing types).

If you haven’t read Part 1, it’s encouraged to read the threat reports at least (see References - Threat Reports below), so the activities of the binary are clear alongside the binary analysis, including the latest Tactics, Techniques, and Procedures (TTPs).

Table of Contents

  • Info of the binary in this analysis
  • LLVM’s picks on scalar moves
  • Section 2: The CLI Framework (seahorse)
  • Section 3: The inlining cascade
  • Section 4: available_parallelism()
  • Section 5: lock and Thread Spawning

Further Reading

    1. SIMD
    1. How aho-corasick uses memchr and SIMD

References

Info of the binary in this analysis

Reversing Akira’s Rust Experiment: A Breakdown Part 1

This post is a breakdown and commentary on Ben Herzog’s Check Point Research (December 2024), Inside Akira Ransomware’s Rust Experiment & his talk in 2025. All technical claims are grounded in the linked primary sources below. Combined with Cindy Xiao’s amazing reports/talks in 2024, 2025 (about reconstructing types and more).

There will be a part 2 to break down the rest. I separate them so this article won’t be _too_long_.

For this analysis, Binary Ninja (v5.3.9434) is used without enabling any plugins. Binary: akirav2.elf , SHA25:60ee1d284ed663073872012c7bde7fac5ca1121403f1a5d2d5411317df282796c.

A note on source references

The commit hash is updated on 10th Jun 2026. 79e9716c… is rustc 1.74.0 stable (released 2023-11-16). Akirav2’s .rodata contains std panic-path strings of the form /rustc//library/....db034cee… is a master/nightly commit from ~1 year later, which I was mistakenly pinned in the article. If you see any links related to db034cee… , please consider it was based on previosu wrong assumption but it works to some degree.

The Rust standard library is under active development and files move between releases. Source links in this post are pinned to commit 79e9716c980570bfd1f666e3b16ac583f0168962 — rustc 1.74.0 stable, the toolchain that compiled this Akira v2 binary. This is recoverable directly from the binary: its .rodata embeds /rustc/79e9716c…/library/... panic paths, and it is corroborated by version-pinned FunctionID analysis resolving the sample to 1.74.0.

This matters because stdlib code is inlined directly into the final binary at compile time: what you see in the disassembly reflects the stdlib at that specific commit, not the current main branch. Following a newer version of the same file risks misidentifying inlined boilerplate or drawing incorrect conclusions about what the malware authors wrote versus what the compiler contributed.

If a pinned link returns a 404, the file has likely been relocated. Navigate to the repository root and search by filename.

LLVM’s picks on scalar moves

SIMD stands for Single Instruction, Multiple Data. It is a CPU execution model where one instruction operates on multiple data elements in parallel, using wide registers.

To understand the compiler’s decision, one must examine the specific arithmetic of the LLVM cost model and processor microarchitecture. The compiler does not merely guess; it evaluates the cost of instructions in bytes, micro-operations (uOps), and execution port pressure.

LLVM’s auto-vectorisation and scalar replacement of aggregates (SROA) pass recognises that zeroing or copying two adjacent 8-byte fields is a 16-byte operation and emits a single XMM store. This is purely a code-generation optimisation, the source code contains no SIMD intrinsics. When you see pxor xmm0, xmm0 followed by movdqu in Rust release builds, it is “likely” (but not absolutely) a struct/array zero-initialisation artefact, not intentional vector computation (more details in the Section 3: Inlining Cascade).

Recognising this code-generation artefact is crucial for reverse engineering, as it cleanly separates genuine mathematical or cryptographic SIMD workloads from routine structural initialisation. At the hardware level, this compiler decision reflects the purest form of systemic leverage: achieving maximal state change with minimal resource expenditure.

Rust and SIMD. Rust’s standard library uses SIMD for things like memchr (which backs aho-corasick internally). When you see a tight loop in a Rust binary using pcmpeqb or vpcmpeqb, it is almost certainly memchr or a SIMD-accelerated pattern search, not custom malware logic. Relevant to your Akira analysis: if you see SIMD near the search routines, that is likely the aho-corasick crate doing pattern matching.

More details can be found in the section Further Readingbelow.

Section 2: The CLI Framework (seahorse)

main function

main function

xmm0 is a 128-bit register in x86-64 assembly used for SSE/AVX SIMD operations, frequently storing floating-point numbers (single/double precision) or packed integers (resource).

What the threat intel establishes

The complete Akira v2 CLI flag set is:

Additionally, --fork controls process forking for the stopvm command, and a --threads flag controls thread count (Check Point Research).

There is also a --logs flag for controlling log verbosity.

Default function in akirav2 in Disassembly Pt 1

Default function in akirav2 in Disassembly Pt 1

Default function in akirav2 in Disassembly Pt 2, you can see another crate <code>Indicatif</code> in the following dsiassembly code

Default function in akirav2 in Disassembly Pt 2, you can see another crate `Indicatif` in the following dsiassembly code

Default function in akirav2 in disassembly in High Level IL

Default function in akirav2 in disassembly in High Level IL

This single HLIL block does three things simultaneously due to inlining: starts a timer, initialises the indicatif spinner, and as a side effect of that initialisation touching adjacent .rodata, exposes the entire operational string table of default_action in one place. Every log message, error string, shell command, and panic message that default_action can emit is visible here.

**1. Timing initialisation (**0x4dc44a)

rax, rdx = std::time::Instant::now::hbcf67d3c6dde57ee()
int64_t var_f8 = rax
int32_t var_2fc = rdx

Records the wall-clock time at the start of default_action. This is used later to calculate elapsed time for the progress spinner display, the {elapsed} or {elapsed_precise} template keys in indicatif. This is pure indicatif boilerplate from yarnish.rs.

**2. Progress spinner initialisation (**0x4dc473)

int32_t var_278[0x4]
indicatif::style::ProgressStyle::with_template::hc10b2fc1f737695f(
    &var_278,
    "{prefix:.bold.dim} {spinner} {wide_msg}akiranew/src/main.rs ..."
    0x27)

This calls ProgressStyle::with_template with a template string of length 0x27 = 39 characters. The template itself is {prefix:.bold.dim} {spinner} {wide_msg}, exactly the yarnish.rs spinner template confirmed from the indicatif source. The result lands in var_278, which is the ProgressStyle struct on the stack.

From data_5e74e8, it’s clear a template of Indicatif yarnish.rs

From data_5e74e8, it’s clear a template of Indicatif yarnish.rs

The core insight from Check Point (Indicatif)

The malware authors used indicatif to provide an operator-friendly visual representation of what the malware is doing: progress bars and spinners, and the source code they used was directly inspired by yarnish.rs, the canonical indicatif example.

The puzzle Check Point identified is that default_action begins with dozens of eyebrow-raising basic blocks invoking the C FFI and other low-level functionality, which has nothing to do with encryption. This is entirely explained by indicatif being inlined into default_action by the Rust release compiler.

A truncated overview of the graphs based on <code>default_action</code>

A truncated overview of the graphs based on `default_action`

First lines in <code>default_action</code> before jumping

First lines in `default_action` before jumping

First few functions of <code>default_action</code>

First few functions of `default_action`

This is directly relevant to the disassembly we have been analysing across this session. The std::time::Instant::now() call at prologue (0x4dc44a), the format string {prefix:.bold.dim} {spinner} {wide_msg} at 0x4dc45e, and the CStr::from_bytes_with_nul / File::open_c calls in the early blocks of default_action are all inlined indicatif initialisation code (yarnish.rs), not malware logic. The malware logic only starts later in default_action once the spinner is set up.

pub fn main() {
    let mut rng = rand::rng();
    let started = Instant::now();
    let spinner_style = ProgressStyle::with_template("{prefix:.bold.dim} {spinner} {wide_msg}")
        .unwrap()
        .tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ");

...

Xref — to see the corelation between main and default_action

Xref — to see the corelation between main and default_action

After argument collection, the assembly performs many writes to stack memory before calling default_action. The function name itself is the clue: a web search for it leads directly to seahorse, a Rust CLI framework whose examples contains exactly this pattern:

use seahorse::{App, Context, Command};
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    let app = App::new(env!("CARGO_PKG_NAME"))
        .description(env!("CARGO_PKG_DESCRIPTION"))
        .author(env!("CARGO_PKG_AUTHORS"))
        .version(env!("CARGO_PKG_VERSION"))
        .usage("cli [name]")
        .action(default_action)
        .command(add_command())
        .command(sub_command());

    app.run(args);
}

The string "cli [name]" also appears verbatim in the binary, confirming the match.

The seahorse App struct has ten fields, most of them Option. The stack writes in the assembly are initialising this struct field by field. Once you know the struct layout, each write maps to a named field.

/// Multiple action application entry point
#[derive(Default)]
pub struct App {
    /// Application name
    pub name: String,
    /// Application author
    pub author: Option<String>,
    /// Application description
    pub description: Option<String>,
    /// Application usage
    pub usage: Option<String>,
    /// Application version
    pub version: Option<String>,
    /// Application commands
    pub commands: Option<Vec<Command>>,
    /// Application action
    pub action: Option<Action>,
    /// Alternate application action that returns a Result
    pub action_with_result: Option<ActionWithResult>,
    /// Application flags
    pub flags: Option<Vec<Flag>>,
}

The flags initialised include:

  • --threads : number of encryption threads (1-1000)
  • --stopvm: shut down ESXi VMs before encrypting
  • Others controlling targeting behaviour

movdqu

Move Double Quadword Unaligned. Loads or stores 16 bytes (128 bits) between an XMM register and memory, with no alignment requirement on the memory address. The u suffix distinguishes it from movdqa (aligned, which faults if the address is not 16-byte aligned).

  • movdqu xmm0, [rel data_4908f4[0x7c]] loads a 16-byte string literal; the inline annotation resolves it to /proc/self/cgroupcpu/sys/fs/cgroupcgroup.controllers... as a cgroup path read. This is the ESXi/Linux container-escape or resource-limit probe pattern seen in Akira v2.
  • core::ffi::c_str::CStr::from_bytes_with_nul is called on the result , confirming a C-string literal being wrapped.
  • std::sys::unix::fs::File::open_c opens the resolved path.
  • cmp dword [rbx {var_2f8}], 0x0 / je 0x4dc94c: checks whether File::open_c returned Ok or Err (the Result discriminant is at offset 0 of the Result<File, Error> stack slot).
  • core::ptr::drop_in_place...ror..Error$GT$$GT$ : the error path drops the io::Error on the branch where open failed.
  • The free call at 0x4dc57d via qword [rel free] (PLT slot) is dropping a heap allocation, consistent with an OsString/PathBuf being freed after use.

  • seahorse::context::Context::string_flag is called at 0x4dc8bd, this is the seahorse API call to retrieve a named string flag from the parsed CLI context. The flag name is loaded from data_439f64[8] which the inline annotation shows as a packed log-level string: "logswarn.logSlimMaskbluecyanboldLANG <= ZeroweekhournextWARNINFOcallhashHash => FAILQuit...". That is a seahorse flag-definition table: multiple flag names packed into a single data blob with offsets. The flag being retrieved here is at byte offset 8, which given the prefix "logs" is the --logs flag.
  • At 0x4dc8fb (highlighted in red), lea rdx, [rel data_4418a5[0x19]] loads a string annotated as "debugValid values for --logs: trace, debug, error, info, warn\nakiranew_Root path: Starting path: /vmfs/vo...": this is an error/help string emitted when the --logs value fails validation.
  • _$LT$$u5b$A$u5d$$u20$as$...T$B$GT$$GT$::equal is <[A] as PartialEq<[B]>>::eq: slice equality, comparing the user-supplied flag value against known valid strings ("trace", "debug", "error", "info", "warn"). This is done twice (at 0x4dc8ee and 0x4dc90b) with r12 = 0x4 and r13 = 0x5, i.e. comparing 4-byte and 5-byte slices

Why this matters for this research?

The Check Point finding reframes the analysis problem entirely. The apparent complexity of default_action is not malware sophistication.

Instead, it is release-mode inlining of a third-party progress bar library.

Section 3: Inlining Cascade

Indicatif

The design motivation

The malware authors used indicatif to provide an operator-friendly visual representation of what the malware is doing. In checkpoint report, Ben describe it as "true eye candy for CLI fans", things like progress bars and spinners declared cheerfully as the ransomware destroys backups, generates a symmetric session key, encrypts VMs, and writes ransom notes.

The boilerplate source is yarnish.rs from the indicatif example directory. As shown in checkpoint report, it’s observed that yarnish.rs was used as a prototype during the writing of this malware, specifically for the lock function, which was moved to a separate module (lock.rs)

This code is part of the example yarnish.rs in Indicatif.

let m = MultiProgress::new();
    let handles: Vec<_> = (0..4u32)
        .map(|i| {
            let count = rng.random_range(30..80);
            let pb = m.add(ProgressBar::new(count));
            pb.set_style(spinner_style.clone());
            pb.set_prefix(format!("[{}/?]", i + 1));
            thread::spawn(move || {
                let mut rng = rand::rng();
                let pkg = PACKAGES.choose(&mut rng).unwrap();
                for _ in 0..count {
                    let cmd = COMMANDS.choose(&mut rng).unwrap();
                    thread::sleep(Duration::from_millis(rng.random_range(25..200)));
                    pb.set_message(format!("{pkg}: {cmd}"));
                    pb.inc(1);
                }
                pb.finish_with_message("waiting...");
            })
        })
        .collect();
    for h in handles {
        let _ = h.join();
    }

Let’s focus on rng.random_range... for now. The breakdown of thread spawning can be found in the section “The Inlining Cascade” below.

pxor xmm0, xmm0

XOR a 128-bit SSE register with itself. The result is always zero regardless of the prior contents, identical in intent to xor eax, eax for 32-bit registers, but operating on 16 bytes in one instruction.

0x4dd16e : thread RNG initialisation and alphabet setup

004dd174   call  core::ptr::drop_in_place...Box<str>...::hb14970c17f15bb5a

Drops a previous Box, cleanup of a prior string allocation (a log message or progress bar message that was displayed).

004dd179   call  rand::rngs::thread::thread_rng::h463d5de9ac748823
004dd17e   mov   r14, rax

In rand 0.9, fn rand::thread_rng() was renamed to rand::rng() and removed from the prelude.

thread_rng() initialises the per-thread CSPRNG from the rand crate. The return value (ThreadRng handle) is stored in r14. This is the first direct evidence of cryptographic randomness being sourced. Concretely it is rand 0.8.5’s ThreadRng, a ReseedingRng<ChaCha12Core, OsRng> built on rand_chacha 0.3.1, which generates the SOSEMANUK session key later wrapped by X25519 (see Part 3).

The mangled symbol in the binary is rand::rngs::thread::thread_rng , this confirms Akira v2 compiled against rand pre-0.9 (where the function was still named thread_rng), not 0.9+.

Randomly-selected characters & Vec

Initialises a Vec at var_278 with discriminant 0x1 (non-empty Some), zeroes the data/len/cap fields via xmm0, then reserves capacity for 10 elements (r12 = 0xa). This is building a Vec that will hold randomly-selected characters.

004dd1b3   lea   rbx, [rel str.2[0x39]]
           {"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/mnt/c/Users/user/.carg..."}

Loads a pointer into the alphanumeric character set ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 , which has 62 characters. The /mnt/c/Users/user/.carg... suffix is the adjacent .rodata (a cargo registry path string), the alphabet itself is just the 62 alphanumeric characters. This is the character pool for random ID generation.

004dd1ba   mov   rax, qword [r14+0x110]  ; load Vec length counter
004dd1c1   cmp   rax, 0x40               ; compare to 64
004dd1c5   jb    0x4dd1dc                ; loop while length < 64

The loop runs until 64 characters have been appended. This is generating a 64-character random alphanumeric string.

**0x4dd1c7–**0x4dd23f : the random character selection inner loop

004dd1c7   mov   rdi, rbp
004dd1ca   mov   rsi, r13
004dd1cd   call  _$LT$rand..rngs..adapter...Core$GT$::generate::hd875faf09d425bd0
004dd1d2   and   qword [r14+0x110], 0x0   ; clear len field temporarily
004dd1da   xor   eax, eax

Calls <rand::rngs::adapter::ReseedingRng>::generate, this is the ThreadRng backend calling the underlying CSPRNG block generator. The result populates a buffer.

004dd1dc   mov   ecx, dword [r14+rax*4+0x10]  ; load random dword from buffer
004dd1e1   mov   edx, ecx
004dd1e3   shr   edx, 0x1b                    ; edx = ecx >> 27
004dd1e6   inc   rax
004dd1e9   mov   qword [r14+0x110], rax
004dd1f0   cmp   edx, 0x1e                    ; compare to 30
004dd1f3   ja    0x4dd1c1                     ; reject if edx > 30 (rejection sampling)

The pre-0.9 version that matches Akira v2’s compile-time rand version (the binary uses thread_rng, which was renamed to rng() only in 0.9

This is rejection sampling to avoid modulo bias. The random dword is right-shifted by 27 bits, leaving 5 bits. If those 5 bits gives 0–31; rejecting > 30 keeps 0–30, which is 31 values. But 62 = 2 × 31, so this is selecting a value in [0, 30] and then using a further bit to select the upper or lower half of the alphabet. The next block confirms this:

004dd1f5   shr   ecx, 0x1a               ; ecx >> 26 (6 bits)
004dd1f8   mov   r15b, byte [rcx+rbx]    ; index into alphabet[ecx]
004dd1fc   test  r15b, r15b
004dd1ff   js    0x4dd241               ; sign bit set → handle non-ASCII (encode_utf8 path)

After rejection, ecx >> 26 gives a 6-bit index into the 62-character alphabet. Since 62 < 64, the maximum 6-bit value (63) can still exceed the alphabet, the js branch at 0x4dd1ff handles the edge case via core::char::methods::encode_utf8_raw seen later. For ASCII characters (the common case), r15b is directly the selected character byte.

004dd201   mov   rsi, qword [rsp+0x220 {var_268}]   ; Vec len
004dd209   cmp   rsi, qword [rsp+0x218 {var_278[2].q}]  ; compare len to cap
004dd211   jne   0x4dd228                             ; if len < cap, fast path
004dd213   lea   rdi, [rsp+0x210 {var_278}]
004dd21b   call  alloc::raw_vec::RawVec<...>::reserve_for_push  ; grow if needed

Standard Vec::push capacity check, grow if full, otherwise fast-path the byte append.

004dd228   mov   rax, qword [rsp+0x210 {var_278[0].q}]  ; Vec data ptr
004dd230   mov   byte [rax+rsi], r15b   ; append the character
004dd234   inc   rsi
004dd237   mov   qword [rsp+0x220 {var_268}], rsi  ; update len
004dd23f   jmp   0x4dd26d               ; back to outer loop

Appends the selected character to the Vec and increments length.

0x4dd327: time-0.3.30 struct initialisation and Display formatting

004dd327   __builtin_memcpy(dest: &rax_43[6],
               src: "\x05\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x"
                    "00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00"
                    "00\x05\x00\x00\x00\x00\x00\x00\x00\x0b",
               count: 0x31)

This is initialising the field layout of a time::PrimitiveDateTime or time::OffsetDateTime struct from the time-0.3.30 crate. The byte values correspond to date/time fields: month=5 (May), day=1, hour=1, minute=0, second=4, etc. This is constructing a fixed or computed datetime value for use in the session record or ransom note timestamp.

004dd371   rax_43[0x10] = "/mnt/c/Users/user/.cargo/registry/src/index.crates."
                          "io-6f17d22bba15001f/time-0.3.30/src/date.rsjulian_day"
004dd385   rax_43[0x13] = "/mnt/c/Users/user/.cargo/registry/src/index.crates."
                          "io-6f17d22bba15001f/time-0.3.30/src/date.rsjulian_day"

These are panic location strings from time-0.3.30/src/date.rs, leaked into the binary as Rust panic metadata. The developer's full filesystem path /mnt/c/Users/user/.cargo/registry/... is embedded. The /mnt/c/Users/ prefix confirms the developer compiled on WSL2 (Windows Subsystem for Linux): C:\Users... mounted as /mnt/c/Users/....

004dd3f1   var_278[0].q = &data_5e7570
004dd3f4   var_278[2].q = 2

Sets up a &str fat pointer: data pointer + length 2, likely a two-character string used as a separator or suffix in the session ID string being assembled.

004dd404   int64_t (* var_2f0_2)(int64_t* arg1) =
               _$LT$alloc..string..Stri....Display$GT$::fmt::h9352ad71406cfecb

Stores the ::fmt vtable function pointer into var_2f0_2. This is setting up a trait object for fmt::Display, the session ID String is about to be formatted for output (to the progress bar, log, or ransom note).

What this block does in total

(Likely) This is Akira v2’s session key / encryption ID generation:

// Reconstructed source
let mut rng = thread_rng();
let charset: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let session_id: String = (0..64)
    .map(|_| {
        // rejection-sampled uniform selection from charset
        charset[rng.gen_range(0..62)] as char
    })
    .collect();

// then: get current datetime via time-0.3.30
// then: format session_id for display / note writing

The 64-character alphanumeric random string is the per-execution session key identifier, used to name the ransom note, tag encrypted files, or derive the symmetric encryption key depending on where it flows next.

The WSL2 developer path (/mnt/c/Users/user/.cargo/) is a OPSEC leak. The developer built this on a Windows machine running WSL2, using the default username user. This is consistent across all three panic strings visible in this session.

How is Malware Operatoring Indicatif

The operator is shown the elapsed time of the current operation. In yarnish.rs, the relevant indicatif template string uses {elapsed} or {elapsed_precise} . SPARKLE is the static variable (emoji) used in yarnish.rs .

static LOOKING_GLASS: Emoji<'_, '_> = Emoji("🔍  ", "");
static TRUCK: Emoji<'_, '_> = Emoji("🚚  ", "");
static CLIP: Emoji<'_, '_> = Emoji("🔗  ", "");
static PAPER: Emoji<'_, '_> = Emoji("📃  ", "");
static SPARKLE: Emoji<'_, '_> = Emoji("✨ ", ":-)");

The root cause

The upper half of default_action contains dozens of confusing basic blocks that appear to call from_bytes_with_nul, std::fs::buffer_capacity_required, and other unrelated functions. One basic block runs for over 200 instructions. None of this is directly authored by the malware writers.

The Akira authors wanted their thread count to be intelligent. Rather than hardcoding a number, they wrote something equivalent to. The operator-supplied --threads flag has a default: the number of logical CPU cores. This is implemented as:

let threads: u32 = match threads_flag_value {
    Some(n) => n,
    None => available_parallelism().unwrap() as u32,
};

available_parallelism() is a deceptively simple call. Its Release-mode expansion is not. available_parallelism() may sound trivial, but on Linux, it is not.

What the Runtime Actually Has to Do

To return a meaningful CPU count on a Linux system, the Rust standard library cannot simply call sysconf(_SC_NPROCESSORS_ONLN) and be done with it. That syscall returns the host's physical CPU count, which is meaningless inside a container or a VM with a cgroup CPU quota applied. A process limited to 2 CPU-equivalents by cgroup policy on a 64-core host should not spin up 64 threads.

Section 4: available_parallelism()

Quick Recap

Before diving deeper, Let’s recap what we had found so far:

  • Seahorse: CLI Framework, relevant function default_action
  • Indicatif: Progress in CLI command, relevant file is yarnish.rs

What we are going to discover:

  • The structure of the threading code in default_action matches yarnish.rs's pattern: spawning N threads, collecting handles into a Vec, joining them
  • yarnish.rs : multithreaded source code, relevant function call available_parallelism

Now jump to yarnish.rs — Multithreading part

As we saw earlier, the operator-facing UI is built using indicatif, a Rust library for terminal progress bars and spinners. The malware displays a progress box with emojis labelled things like “destroying backups”, “generating symmetric session key”, “encrypting VM”, “writing ransom note”.

The source inspiration yarnish.rs, has an example in the indicatif repository. That example has a multithreaded outline:

use std::thread;
...

let handles: Vec<_> = (0..4u32)
    .map(|i| thread::spawn(move || { ... }))
    .collect();

It took me a bit time to realise how to connect yarnish.rs with available_parallelism() . Basically, the function available_parallelism() is the standard function of the Rust stdlib, thread.rs.

The link from yarnish.rs to available_parallelism is not direct. The chain is:

  • yarnish.rs itself does not call available_parallelism().
  • You can confirm it hardcodes 0..4u32 with no CPU query at all. The authors added available_parallelism() themselves as a modification.
yarnish.rs hardcodes 0..4u32
    ↓ authors modify it to be dynamic
    ↓ CLI flag --threads added via seahorse
    ↓ help text "Default: number of logical CPU cores" visible in binary
    ↓ None branch must query CPU count
    ↓ idiomatic stdlib call = available_parallelism()

Binary Confirmation: Inlined Call Tree Artefacts

For the CPU_COUNT_S bitmask, that lives in the libc crate, not the rust stdlib. You need the correct libc crate commit that corresponds to what db034cee was depending on. Check the lockfile, the version is listed below.

Because of Release + LTO inlining, available_parallelism() leaves these verifiable traces inside default_action:

Each of these is individually verifiable by reading the stdlib source and tracing what gets inlined. The bitmask constants are the strongest binary fingerprint because they are unique to the software popcnt expansion of CPU_COUNT_S operating on a cpu_set_t.

How to verify bitmask

Verify the bitmask with correct libc version:

[[package]]
name = "libc"
version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"

The libc crate uses git tags matching the version string exactly. The correct pinned URL for CPU_COUNT_S in the Linux GNU module is potentially in the link:

https://github.com/rust-lang/libc/blob/0.2.159/src/unix/linux_like/linux/gnu/mod.rs

However, I couldn’t find CPU_COUNT_S in the link, so I tried another method to find under the same version of libc :

https://github.com/rust-lang/libc/search?q=CPU_COUNT&type=code&ref=0.2.159

The most likely files for this inspection, but which one? Or none of them?

The most likely files for this inspection, but which one? Or none of them?

  • teeos is a completely separate top-level target in the libc crate. It is gated by #[cfg(target_os = "teeos")]. The Akira binary is an ELF compiled for x86_64-unknown-linux-gnu, which means target_os = "linux". The teeos module is never compiled for a Linux target; the cfg gates are mutually exclusive.
  • linux_l4re_shared.rs by contrast is named and structured as a shared file between Linux and L4Re targets, sitting inside src/unix/linux_like/. It is included via the Linux module hierarchy, which is the path taken for x86_64-unknown-linux-gnu.

But I cannot verify which one is relevant, though their codes did the same thing basically…

https://github.com/rust-lang/libc/blob/0.2.159/src/unix/linux_like/linux/mod.rs

The source code of mod.rs:

pub fn CPU_COUNT_S(size: usize, cpuset: &cpu_set_t) -> ::c_int {
        let mut s: u32 = 0;
        let size_of_mask = ::mem::size_of_val(&cpuset.bits[0]);
        for i in cpuset.bits[..(size / size_of_mask)].iter() {
            s += i.count_ones();
        };
        s as ::c_int
    }

    pub fn CPU_COUNT(cpuset: &cpu_set_t) -> ::c_int {
        CPU_COUNT_S(::mem::size_of::<cpu_set_t>(), cpuset)
    }

CPU_COUNT and CPU_COUNT_S are defined directly in src/unix/linux_like/linux/mod.rs, inside the f! macro. Not in linux_l4re_shared.rs, orteeos/mod.rs.

linux_l4re_shared.rs contains a duplicate copy of the same functions, but for the x86_64-unknown-linux-gnu target used by the Akirav2 binary, the code that actually compiles in is from this file.

Back to the disassembly…

The malware replaces the hardcoded 4u32 with the dynamic thread count from available_parallelism(). That single substitution is what triggers the entire inlining cascade described above.

An immediate puzzl in <code>default_action</code>, linear mode in disassembly

An immediate puzzl in `default_action`, linear mode in disassembly

An immediate puzzl in <code>default_action</code>, graph mode in disassembly

An immediate puzzl in `default_action`, graph mode in disassembly

The original yarnish.rs hardcodes 4 threads. The binary does not hardcode 4. Instead, default_action begins with a large block of low-level calls.

The malware authors “probably” have replaced 0..4u32 with a runtime value. That runtime value requires querying the CPU count. The standard Rust idiom for CPU count is available_parallelism().

This is abductive reasoning (inference to best explanation), not direct identification. The argument is:

  1. Template originally had 0..4u32
  2. Binary has a variable thread count
  3. CLI flag --threads exists with explicit Default: number of logical CPU cores help text (this string survives in the binary as a literal)
  4. Therefore the None branch of the match must call something that returns core count
  5. The idiomatic stdlib call for that is available_parallelism()

The compiler inlined the following call tree, five levels deep, directly into default_action:

available_parallelism()
  -> quota()                     [reads /proc/self/cgroup]
      -> quota_v1(path)           [cgroup v1 CPU quota]
      -> quota_v2(path)           [cgroup v2 CPU quota]
  -> libc::CPU_COUNT(&set)
      -> CPU_COUNT_S(size, set)
          -> count_ones()         [LLVM popcnt intrinsic]
              -> BitPopulationCount  [expanded as magic-constant popcount]
  -> libc::sched_getaffinity(...)
  -> libc::sysconf(_SC_NPROCESSORS_ONLN)

LLVM Instrinsic popcnt

From the source code of mod.rs(src/unix/linux_like/linux/mod.rs):

pub fn CPU_COUNT_S(size: usize, cpuset: &cpu_set_t) -> ::c_int {
    let mut s: u32 = 0;
    let size_of_mask = ::mem::size_of_val(&cpuset.bits[0]);
    for i in cpuset.bits[..(size / size_of_mask)].iter() {
        s += i.count_ones();
    };
    s as ::c_int
}

On x86–64, cpu_set_t.bits is [u64; 16] . Each iteration calls u64::count_ones().

Proving the chain: count_ones() to llvm.ctpop.i64

A brief summary:

  • Step 1. u64::count_ones() calls intrinsics::ctpop(self). Source-confirmed, from pasted uint_macros.rs at commit db034cee.
  • Step 2. Rust commit db034cee uses LLVM 19.1.3, submodule pinned to b35599b. Source-confirmed, from your screenshot of github.com/rust-lang/rust/tree/db034cee.../src.
  • Step 3. LLVM 19.1.3 registers ISD::CTPOP / MVT::i64 as Custom when Subtarget.hasPOPCNT() is false. Source-confirmed, from pasted X86ISelLowering.cpp at llvmorg-19.1.3.
  • Step 4. The custom handler LowerCTPOP returns SDValue() for a full 64-bit operand with no statically known zero bits, signalling fallback to generic expansion. Source-confirmed, from pasted LowerCTPOP at llvmorg-19.1.3.
  • Step 5. The generic expansion is TargetLowering::expandCTPOP in llvm/lib/CodeGen/SelectionDAG/TargetLowering.cpp. It constructs Mask55, Mask33, and Mask0F via APInt::getSplat(64, APInt(8, 0x55/0x33/0x0F)), producing 0x5555555555555555, 0x3333333333333333, and 0x0f0f0f0f0f0f0f0f. The code cites its own source: Sean Anderson's "Bit Twiddling Hacks". Source-confirmed, from pasted TargetLowering.cpp at llvmorg-19.1.3.
  • Step 6. The same algorithm, same constants, appears in the Flang runtime header at LLVM commit 521c996. Source-confirmed, from pasted bit-population-count.h. This confirms the constants are a property of LLVM's target-independent infrastructure, not of the source language.

Now let’s breakdown step by step.

Layer 1: count_ones() to core::intrinsics::ctpop

At commit 79e9716c980570bfd1f666e3b16ac583f0168962, library/core/src/num/uint_macros.rs defines count_ones as:

#[stable(feature = "rust1", since = "1.0.0")]
#[rustc_const_stable(feature = "const_math", since = "1.32.0")]
#[doc(alias = "popcount")]
#[doc(alias = "popcnt")]
#[must_use = "this returns the result of the operation, \
              without modifying the original"]
#[inline(always)]
pub const fn count_ones(self) -> u32 {
    return intrinsics::ctpop(self);
}

For a u64 receiver, intrinsics::ctpop(self) compiles to the LLVM IR call llvm.ctpop.i64.

Two details from the attributes are significant.

  • #[inline(always)] means the call is always inlined at the call site, so no call instruction is ever emitted for it.
  • #[doc(alias = "popcnt")] is a documentation alias that the Rust compiler team added precisely because this function is the stable surface of the hardware popcount concept.

The body delegates unconditionally to intrinsics::ctpop, a Rust compiler intrinsic with no Rust-level implementation: it is a direct instruction to the compiler backend to emit the LLVM IR node ISD::CTPOP, which for a u64 operand produces llvm.ctpop.i64.

Layer 2: llvm.ctpop.i64 to machine instruction, conditional on**+popcnt**

It is very likely commit 79e9716 using rustc 1.74.0, LLVM 17.x, pinned to rust-lang/llvm-project at commit fef3d7b14ede45d051dc688aae0bb8c8b02a0566.

The LLVM x86 instruction selection file X86ISelLowering.cpp contains the following logic (llvmorg-19.1.3/llvm/lib/Target/X86/X86ISelLowering.cpp):

if (Subtarget.hasPOPCNT()) {
    setOperationPromotedToType(ISD::CTPOP, MVT::i8, MVT::i32);
    // popcntw is longer to encode than popcntl and also has a false dependency
    // on the dest that popcntl hasn't had since Cannon Lake.
    setOperationPromotedToType(ISD::CTPOP, MVT::i16, MVT::i32);
  } else {
    setOperationAction(ISD::CTPOP          , MVT::i8   , Custom);
    setOperationAction(ISD::CTPOP          , MVT::i16  , Custom);
    setOperationAction(ISD::CTPOP          , MVT::i32  , Custom);
    setOperationAction(ISD::CTPOP          , MVT::i64  , Custom);
  }

This is the registration of how LLVM 19.1.3 will handle ISD::CTPOP for each integer width on x86. When Subtarget.hasPOPCNT() is false, MVT::i64 is marked Custom, meaning LLVM will call a custom lowering function rather than emitting a single popcnt instruction. This is the decision point that determines whether the bitmask sequence appears.

Where the constants come from

They come from TargetLowering::expandCTPOP in llvm/lib/CodeGen/SelectionDAG/TargetLowering.cpp. This is the target-independent generic expansion function in LLVM's code generation infrastructure.

The call chain when +popcnt is absent is:

LowerCTPOP (X86ISelLowering.cpp)
    -> returns SDValue() for full 64-bit operands
        -> LegalizeDAG calls TargetLowering::expandCTPOP (TargetLowering.cpp)
            -> emits the bitmask sequence

The constants appear in expandCTPOP as the Hamming weight algorithm. The Flang header you pasted is a human-readable C++ expression of exactly the same algorithm. The constants are identical because both implement the same mathematical identity: to count set bits, partition the word into 2-bit fields, count each, then sum progressively wider groups.

The algorithm works by a divide-and-conquer strategy: count bits in groups of 2, then merge groups of 4, then 8, and so on until the whole word is summed. Each step uses a bitmask to isolate the relevant bits.

SDValue TargetLowering::expandCTPOP(SDNode *Node, SelectionDAG &DAG) const {
  ...

  // This is the "best" algorithm from
  // http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel
  SDValue Mask55 =
      DAG.getConstant(APInt::getSplat(Len, APInt(8, 0x55)), dl, VT);
  SDValue Mask33 =
      DAG.getConstant(APInt::getSplat(Len, APInt(8, 0x33)), dl, VT);
  SDValue Mask0F =
      DAG.getConstant(APInt::getSplat(Len, APInt(8, 0x0F)), dl, VT);

...

What the Akira disassembly tells you

The report observes those exact bitmask constants in the Akira assembly. This confirms that akirav2.elf was compiled without the +popcnt target feature, so llvm.ctpop.i64 expanded to the software path rather than a single popcnt instruction.

u64::count_ones() at commit db034cee unconditionally delegates to core::intrinsics::ctpop, which maps to llvm.ctpop.i64. Whether that IR node produces a single popcnt rax, rdi or a multi-instruction software bitmask sequence depends solely on whether +popcnt is present in the compile-time target feature set. In akirav2.elf, the software path was taken, evidenced by the bitmask constants visible in the disassembly.

The popcount expansion is what produces the 200-instruction basic block. The standard software-popcount algorithm uses a sequence of bitmasks (0x5555555555555555, 0x3333333333333333, 0x0f0f0f0f0f0f0f0f) that are recognisable once you know what you are looking at:

x = (x & 0x5555555555555555) + ((x >> 1) & 0x5555555555555555);
x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333);
// ...

The appearance of these constants in an otherwise opaque basic block is the forensic fingerprint that tells you the entire block is an inlined CPU_COUNT.

Note: in Release-mode Rust binaries, a single innocuous standard library call can expand into hundreds of instructions through aggressive inlining. The correct analysis strategy is to identify the innermost inlined functions by their constants and patterns first, then work outward to reconstruct the call tree.

Section 5: VM Shutdown (--stopvm)

I have discovered this in the previous article Akirav2 Analysis Part 1.

Some interesting strings found in the section of <code>.rodata</code>

Some interesting strings found in the section of `.rodata`

If --stopvm is passed, default_action forks a child process and executes:

vim-cmd vmsvc/getallvms | tail -n +2 | awk '{system("vim-cmd vmsvc/power.off " $1)}'

This uses ESXi’s built-in vim-cmd management utility to enumerate all registered VMs and power them off sequentially. The ransomware must run with sufficient ESXi privileges for this to succeed. This is a standard pre-encryption step in ESXi-targeting ransomware: powered-on VMs hold file locks that block encryption.

Looking at the strings section and it contains multiple critical findings:

ESXi targeting confirmed:

/vmfs/volumes
vim-cmd vmsvc/getallvms | tail -n +2 | awk '{system("vim-cmd vmsvc/power.off " $1)}'

This is the exact shell command to enumerate and power off all VMs on an ESXi host, confirming the Check Point analysis.

ESXi is VMware’s bare-metal hypervisor. It runs directly on server hardware (no host OS underneath) and hosts multiple virtual machines. In enterprise environments it is extremely common: one physical server running ESXi might host dozens of VMs for databases, domain controllers, file servers, etc.

Why ransomware authors love targeting it?

  • One infection, massive blast radius. Encrypt the ESXi host and every VM it manages goes down simultaneously, rather than having to compromise each machine individually.
  • Often poorly monitored. EDR agents are rarely deployed on hypervisor hosts the way they are on Windows endpoints.
  • Critical infrastructure. Organisations are under extreme pressure to pay because their entire virtualised environment is offline.
  • NFS/VMFS datastores. VM disk images (.vmdk files) live on shared storage volumes that ESXi mounts directly, so the ransomware can enumerate and encrypt them efficiently without touching individual guest OSes.

The --path /vmfs/volumes default you saw in the Check Point article is the standard ESXi datastore mount point. That single directory contains the disk images for every hosted VM.

RE Methodology Takeaways

Check Point’s report is as much about method as it is about this specific binary. The principles that made it possible:

  1. Do not start by reading assembly top-down. Identify framework code first (seahorse, indicatif) by searching for function names and strings that appear in the binary.
  2. Recognise inlining by constants, not call structure. Magic constants like popcount masks, cgroup path strings (/proc/self/cgroup), and well-known errno values are fingerprints of inlined library functions even when no call instruction is present.
  3. Know the type layouts. Understanding that IntoIter is a 32-byte header with a 24-byte-strided heap buffer makes the argument-parsing assembly readable. Without that, it is just pointer arithmetic.
  4. Follow the PAL boundary. Standard library types like Args and OsString have OS-specific implementations. The assembly reflects the PAL target, not the public API.
  5. Release vs Debug mode. The same source code can produce thousands of additional instructions in Release mode due to inlining, loop unrolling, and LLVM intrinsic expansion.

Further Reading

1. SIMD

SIMD stands for Single Instruction, Multiple Data. It is a CPU execution model where one instruction operates on multiple data elements in parallel, using wide registers.

What SIMD means in hardware

Instead of adding two 32-bit integers, a SIMD instruction can add eight 32-bit integers simultaneously, all in one clock cycle, using a 256-bit register (like ymm0).

Register families you will encounter:

  • MMX (64-bit, obsolete)
  • SSE / SSE2 / SSE4 (xmm0–xmm15, 128-bit)
  • AVX / AVX2 (ymm0–ymm15, 256-bit)
  • AVX-512 (zmm0–zmm31, 512-bit)

Why it matters in RE

  • Compiler auto-vectorisation. Compilers (LLVM, GCC, MSVC) aggressively auto-vectorise loops. A simple memcpy, memcmp, or string scan in source becomes SIMD instructions in the binary. This makes hot loops look unfamiliar if you are not expecting it.
  • Obfuscation and packing. Packers and obfuscators sometimes use SIMD intentionally to complicate static taint analysis, because most taint engines either do not track xmm/ymm registers at all or do so poorly.
  • Rust and SIMD. Rust’s standard library uses SIMD for things like memchr (which backs aho-corasick internally). When you see a tight loop in a Rust binary using pcmpeqb or vpcmpeqb, it is almost certainly memchr or a SIMD-accelerated pattern search, not custom malware logic. Relevant to your Akira analysis: if you see SIMD near the search routines, that is likely the aho-corasick crate doing pattern matching.
  • Cryptography. AES-NI (aesenc, aesdec) is technically a SIMD-adjacent extension. Much ransomware uses AES, usually via AES-NI (aesenc and friends), so spotting aesenc is normally a strong sign AES is present. This Akira v2 sample instead uses the SOSEMANUK stream cipher, implemented in software with no AES-NI, and indeed no aesenc, aesdec or related opcodes appear anywhere in the 428k-line disassembly. The asymmetry matters: absence of aesenc only rules out AES-NI-accelerated AES (software AES or any stream cipher would also lack it), so it is a useful negative filter, not a positive identification. The positive call rests on the SOSEMANUK constants (the 0x54655307 FSM multiplier with rotate-left-7, and the Serpent key-schedule constant 0x9E3779B9).

Common SIMD instructions worth recognising

vzeroupper in particular is a useful marker: it almost always appears at the end of a function that used AVX, so it helps you identify function boundaries in stripped binaries.

2. How aho-corasick uses memchr and SIMD

The dependency chain

aho-corasick  →  memchr  →  SIMD intrinsics (platform-dependent)

aho-corasick uses memchr as an accelerator for its "teddy" algorithm, which is its SIMD-accelerated literal search backend. The relevant source locations:

aho-corasick calling memchr: https://github.com/BurntSushi/aho-corasick/blob/master/src/packed/teddy/generic.rs

The teddy backend uses 128-bit or 256-bit shuffles to match candidate bytes in parallel before running the full automaton. Look specifically at the find_at methods and the Teddy struct.

memchr SIMD implementation: https://github.com/BurntSushi/memchr/blob/master/src/arch/x86_64/sse2/memchr.rs https://github.com/BurntSushi/memchr/blob/master/src/arch/x86_64/avx2/memchr.rs

The SSE2 path uses _mm_cmpeq_epi8 (which compiles to pcmpeqb) and _mm_movemask_epi8 (which compiles to pmovmskb). The Advanced Vector Extensions (AVX2) path uses the ymm equivalents.

Advanced Vector Extensions, AVX, is Intel’s extension to the x86 ISA that widens SIMD registers from 128-bit (SSE) to 256-bit, and later 512-bit (AVX-512).

What this looks like in a stripped binary

In Binary Ninja, when you find the aho-corasick search loop (recoverable from your panic strings since you already confirmed crate aho-corasick 1.1.2), expect to see a structure like this:

; Outer loop: advance through the haystack in 16 or 32-byte chunks
vmovdqu  ymm0, [rsi + rax]       ; load 32 bytes of haystack
vpshufb  ymm1, ymm2, ymm0        ; teddy shuffle: match low nibbles
vpshufb  ymm3, ymm4, ymm0        ; teddy shuffle: match high nibbles
vpand    ymm1, ymm1, ymm3        ; AND the masks together
vptest   ymm1, ymm1              ; test if any candidates found
jz       .next_chunk             ; no match, advance
; ... fallthrough to verification logic

vpshufb (SIMD byte shuffle) is the fingerprint of the teddy algorithm specifically. If you see vpshufb in a tight loop over a memory region, that is almost certainly teddy, not custom logic.

What version 1.1.2 specifically uses

The recovered crate version is aho-corasick 1.1.2. At that version:

Check src/packed/teddy/generic.rs at that tag for the exact loop structure: https://github.com/BurntSushi/aho-corasick/blob/1.1.2/src/packed/teddy/generic.rs

How to confirm in the Akira binary

  1. In Binary Ninja, search for vpshufb or pcmpeqb instructions in the disassembly
  2. If found in a loop iterating over a buffer, cross-reference against the panic string offsets you already recovered for aho-corasick
  3. The function containing this loop is the teddy search kernel; it will be called from the automaton search dispatch

Though I cannot tell you the exact virtual address or function offset because that requires your specific binary. The panic string Relative Virtual Addresses (RVAs) that have already been recovered are the anchor points for finding the call sites.

Thanks for reading

Thanks for reading

References

Source Code & API Documentation

Rust-lang (no date) core::ffi::c_str — CStr implementation. Available at: https://github.com/rust-lang/rust/blob/main/library/core/src/ffi/c_str.rs

Rust-lang (no date) std::ffi::c_str — re-export layer. Available at: https://github.com/rust-lang/rust/blob/main/library/std/src/ffi/c_str.rs

Rust-lang (2023) std::sys::unix::fs — historical unix filesystem implementation including open_c. Available at: https://github.com/rust-lang/rust/blob/5a7834050f3a0ebcd117b4ddf0bc1e8459594309/library/std/src/sys/unix/fs.rs

Rust-lang (no date) std::sys::fs::unix — current unix filesystem implementation. Available at: https://github.com/rust-lang/rust/blob/main/library/std/src/sys/fs/unix.rs

Rust-lang (no date) core::ptr::mod — drop_in_place definition. Available at: https://github.com/rust-lang/rust/blob/main/library/core/src/ptr/mod.rs

Rust-lang (no date) std::fs — public File and OpenOptions API. Available at: https://github.com/rust-lang/rust/blob/main/library/std/src/fs.rs

Rust-lang (no date) core::ptr::drop_in_place — stable API documentation. Available at: https://doc.rust-lang.org/beta/core/ptr/fn.drop_in_place.html

ksk001100 (no date) seahorse::app — App struct and run_with_result implementation. Available at: https://github.com/ksk001100/seahorse/blob/master/src/app.rs

console-rs (no date) indicatif — progress bar and spinner library. Available at: https://github.com/console-rs/indicatif/blob/0c037edc86449d84aa457d7d5db80b4166c18d6b/examples/yarnish.rs

Threat Intelligence & Research Reports

Check Point Research (2024) Inside Akira Ransomware’s Rust Experiment. Available at: https://research.checkpoint.com/2024/inside-akira-ransomwares-rust-experiment/

CISA, FBI, EC3 and NCSC-NL (2024) #StopRansomware: Akira Ransomware [Joint Cybersecurity Advisory AA24–109A]. Available at: https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-109a

CISA, FBI, EC3 and NCSC-NL (2024) #StopRansomware: Akira Ransomware [PDF version]. Available at: https://www.ic3.gov/CSA/2024/240418.pdf

Cisco Talos (2024) Akira Ransomware Continues to Evolve. Available at: https://blog.talosintelligence.com/akira-ransomware-continues-to-evolve/

Palo Alto Unit 42 (2024) Threat Assessment: Howling Scorpius (Akira Ransomware). Available at: https://unit42.paloaltonetworks.com/threat-assessment-howling-scorpius-akira-ransomware/

MITRE ATT&CK (no date) Akira_v2, Software S1194. Available at: https://attack.mitre.org/software/S1194/

Security Online (2024) Akira v2 Emerges: Rust-Based Ransomware Raises the Stakes. Available at: https://securityonline.info/akira-v2-emerges-rust-based-ransomware-raises-the-stakes/

aho-corasick / Teddy

Burntsushi (no date) aho-corasick 1.1.2 — packed::teddy::builder. Available at: https://docs.rs/aho-corasick/1.1.2/src/aho_corasick/packed/teddy/builder.rs.html

Intel ISA and SIMD architecture

SSE / AVX register sets

Compiler auto-vectorisation

Rust memchr and SIMD

AES-NI

vzeroupper behaviour

AVX

popcnt

The Rust Project Developers (n.d.) core/num/uint_macros.rs source, Rust Standard Library Source. Available at: https://doc.rust-lang.org/src/core/num/uint_macros.rs.html

LLVM Project (2024) llvm/lib/Target/X86/X86ISelLowering.cpp at tag llvmorg-19.1.3. Available at: https://raw.githubusercontent.com/llvm/llvm-project/refs/tags/llvmorg-19.1.3/llvm/lib/Target/X86/X86ISelLowering.cpp

LLVM Project (2024) llvm/lib/CodeGen/SelectionDAG/TargetLowering.cpp at tag llvmorg-19.1.3, function expandCTPOP. Available at: https://raw.githubusercontent.com/llvm/llvm-project/refs/tags/llvmorg-19.1.3/llvm/lib/CodeGen/SelectionDAG/TargetLowering.cpp

LLVM Project (2024) flang/include/flang/Common/bit-population-count.h at commit 521c996. Available at: https://github.com/llvm/llvm-project/blob/521c99627690e09ba25383c83232f94ff457f00c/flang/include/flang/Common/bit-population-count.h

Anderson, S. (n.d.) ‘Bit Twiddling Hacks: Count bits set in parallel’. Available at: http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel

lock function reconstruction

The Rust Project Developers (2024) std::thread::available_parallelism. The Rust Standard Library. Available at: https://doc.rust-lang.org/std/thread/fn.available_parallelism.html

Tompkins, W. et al. (2024) Akira ransomware continues to evolve. Cisco Talos Intelligence. Available at: https://blog.talosintelligence.com/akira-ransomware-continues-to-evolve/

console-rs (2024) indicatif yarnish.rs example. GitHub. Available at: https://github.com/console-rs/indicatif/blob/0c037edc86449d84aa457d7d5db80b4166c18d6b/examples/yarnish.rs

ksk001100 (2024) seahorse CLI framework. GitHub. Available at: https://github.com/ksk001100/seahorse