0APT Ransomware: Deep Dive into a Rust-Based Windows Encryptor

Disclaimer: This analysis was conducted for educational and defensive security research purposes only. All findings are intended to help security professionals understand and defend against this threat.

Executive Summary

0APT is a Rust-compiled Windows ransomware that employs a hybrid AES-256 + RSA-4096 OAEP encryption scheme with hardware-accelerated AES-NI autodetection and software fallback. The ransomware uses the Rayon parallel computing framework for multi-threaded file encryption with RAM-aware throttling to avoid destabilizing the host system during encryption. It reads its configuration from an external text file, executes a two-pass encryption cycle, changes the desktop wallpaper to a dropped PNG image, and drops ransom notes throughout the filesystem.

Key findings:

  • Hybrid encryption: per-file AES-256-CTR key + RSA-4096 OAEP key wrapping with SHA-256/MGF1-SHA-256 (CSPRNG-generated via ProcessPrng/BCryptGenRandom)
  • The ransom note falsely claims RSA-2048 — the actual embedded public key is RSA-4096
  • Multi-threaded encryption via Rayon work-stealing thread pool with real-time memory pressure monitoring via sysinfo + GlobalMemoryStatusEx/K32GetPerformanceInfo
  • External configuration file (config2.txt) with 8 operator-tunable parameters including RAM thresholds, parallelism, file size limits, and start delay
  • AES-NI hardware detection via CPUID ECX bit 25 with transparent software fallback (fixslice32 constant-time bitsliced implementation)
  • Two-pass encryption: Runs 2 complete directory traversal cycles with 2-second inter-cycle sleep for deferred file retry
  • Encrypted file format: [4-byte BE RSA blob length][RSA-OAEP wrapped AES key][16-byte IV][AES-256-CTR ciphertext]
  • Hardcoded fallback RSA-4096 public key embedded at 0x68CD2C (used if public_key.pem missing)
  • Embedded 800x600 PNG wallpaper (24,886 bytes) dropped to %TEMP%\embedded_wallpaper.png
  • Process termination via taskkill.exe /PID <pid> /F with hidden console window (CREATE_NO_WINDOW)
  • Tor-based negotiation portal with 24/48-hour deadline escalation
  • No shadow copy deletion, no persistence mechanisms, no C2 communication
  • OPSEC leak: Hindi-language debug strings and Kali Linux build environment (/home/kali/)
  • No anti-analysis techniques detected — no anti-debug, anti-VM, or anti-sandbox checks
  • Decryption without the attacker’s RSA-4096 private key is NOT feasible

Sample Overview

Property Value
SHA-256 b2f915cbf1a2b6879d278f613b13d790de9a460759142f30457c3238e598e077
File Type PE32 executable (console), Intel 80386, for MS Windows
Architecture x86 (32-bit)
Compiler Rust (MinGW/GCC toolchain)
Rust Compiler rustc commit ded5c06cf21d2b93bffd5d884aa6e96934ee4234
Source File src/bin/encrypt.rs
Build Environment Kali Linux (/home/kali/.cargo/...)
Binary Size ~3.6 MB
Functions 13,050
Encrypted Extension .0apt
Ransom Note README0apt.txt

PE Sections

Section Size Permissions Entropy Notes
.text 2.5 MB RX 6.39 Code, 13,050 functions
.rdata 468 KB R 6.93 Read-only data, strings, constants
.eh_fram 448 KB R 4.92 Exception handling frames (Rust/GCC)
.data 4 KB RW 1.62 Global variables
.bss 4 KB RW 0.00 Uninitialized data
.idata ~12 KB R Various Import tables
.tls 4 KB RW 0.54 Thread-local storage

The .eh_fram section (truncated from .eh_frame) is characteristic of MinGW-compiled Rust binaries that use DWARF-based exception handling on Windows. The .tls section supports three TLS callbacks used by the Rust runtime for thread-local storage destruction.

Entry Points

Type Address Name
Main Entry 0x401400 _mainCRTStartup
TLS Callback 0 0x5f0f60 std::sys::thread_local::guard::windows::tls_callback
TLS Callback 1 0x67f6f0 TlsCallback_1
TLS Callback 2 0x67f6b0 TlsCallback_2

Execution Flow Overview

The ransomware follows a straightforward execution pipeline:

_mainCRTStartup (0x401400)
  └─> __tmainCRTStartup (0x401010)
       └─> main (0x406370)
            └─> encrypt::main (0x4040a0)  [7,356 bytes — the orchestrator]
                 ├── 1. Read config from "config2.txt"
                 ├── 2. Parse configuration key:value pairs
                 ├── 3. Load RSA public key from "public_key.pem"
                 ├── 4. Initialize Rayon thread pool
                 ├── 5. Read target paths from "allpath.txt"
                 ├── 6. Initialize sysinfo::System for RAM monitoring
                 ├── 7. Walk directories using walkdir crate
                 ├── 8. Filter: skip system dirs & already-encrypted files
                 ├── 9. Parallel encrypt files (process_file_with_logic)
                 ├── 10. Drop ransom notes (README0apt.txt)
                 ├── 11. Change wallpaper (run_wallpaper_module)
                 └── 12. Process termination via taskkill.exe

The encrypt::main function at 0x4040a0 is the central orchestrator at 7,356 bytes — one of the largest functions in the binary. It handles configuration parsing, RSA key loading, thread pool setup, directory walking, and post-encryption cleanup in a single monolithic function.

Two-Pass Execution Model

A distinctive design choice: the main encryption loop runs exactly 2 complete cycles:

Cycle 0: Walk all target directories → encrypt qualifying files
         Print "Cycle 0 finished. Waiting 2 seconds..."
         Sleep 2 seconds
Cycle 1: Walk all target directories again → retry any files that were
         skipped due to RAM pressure or file locks
         Print "Process finished after 2 cycles. Exiting..."

The two-pass approach serves as a simple retry mechanism — files that were deferred during the first pass (due to RAM pressure throttling, file locks, or transient errors) get a second chance during the second pass. Between cycles, the 2-second sleep allows pending I/O to complete and memory pressure to subside.


Configuration Parsing

The ransomware reads its configuration from an external file named config2.txt. This file uses a simple colon-separated key:value format, with one parameter per line. Lines starting with # are treated as comments and ignored.

Configuration Fields

Field Type Default Description
extension String list 18 hardcoded extensions File extensions to skip (e.g., .exe, .dll)
filename String list 4 hardcoded filenames Filenames to skip (own artifacts)
folder String list 20+ hardcoded paths Folder name substrings to skip
max_size_gb Integer 1 GB (0x40000000 bytes) Maximum file size to encrypt
max_parallel Integer CPU core count Maximum parallel encryption threads
min_free_ram_mb Integer 500 MB Minimum free RAM threshold before throttling
ram_refresh_ms Integer 100 ms Interval for refreshing RAM usage statistics
ab_start_min Integer 1 Delay before starting encryption (in minutes)

The configuration parser in encrypt::main (0x4040a0) processes each line by:

  1. Trimming whitespace using core::str::trim_matches
  2. Splitting on : using CharSearcher::next_match
  3. Converting the key to lowercase for case-insensitive matching
  4. Dispatching based on key string length (switch on length 6, 8, 9, 11, 12, 14, 15) then comparing key bytes

The max_parallel parameter defaults to rayon_core::current_num_threads() (the number of logical CPU cores) if not specified. The max_size_gb value is stored internally as bytes (shifted left by 2 bits). The Config struct (visible in drop_in_place<encrypt::Config> at 0x401950) contains three dynamically allocated vectors for extension, filename, and folder exclusion lists.

Default skip filenames (the ransomware’s own artifacts):

config2.txt    README0apt.txt    public_key.pem    company.txt

Built-in Exclusion Lists

Even without a configuration file, the ransomware embeds hardcoded exclusion lists:

Skipped Extensions (16 total):

.tmp .temp .log .cache .lnk .ini .bak .old .thumb .db .exe .dll .sys .msi .bat .com .vbs

Skipped Directories (identified from encrypt::is_skipped_dir at 0x401f50):

Figure 8: mw_is_skipped_dir switch statement — directory exclusion by name length with magic constant comparisons

The function uses a switch statement on directory name length with byte-level comparisons and SSE2 (_mm_cmpeq_epi8) for longer names:

Length Directories
3 lib, sys, dev, tmp, bin
4 boot, proc, temp
5 cache
6 system
7 windows, appdata, library
11 programdata
12 $recycle.bin
13 program files
16 application data
19 program files (x86)

Skipped Paths (Unix-style paths also excluded — cross-platform heritage):

/temp /tmp /cache /google /chrome /mozila /firefox /$recycle.bin
/appdata /local/temp /windows /program data
/bin /sbin /proc /dev /sys /etc /lib /boot

Encryption Engine

Hybrid Encryption Scheme

0APT implements a hybrid encryption architecture combining symmetric and asymmetric cryptography:

For each file:
  1. Generate random 32-byte AES-256 key     (OsRng → ProcessPrng/BCryptGenRandom)
  2. Generate random 16-byte IV/nonce         (OsRng → ProcessPrng/BCryptGenRandom)
  3. Encrypt AES key with RSA-4096 OAEP       (rsa::oaep::Oaep::encrypt)
  4. Encrypt file contents with AES-256-CTR   (cipher::stream::AsyncStreamCipher)
  5. Write: [4B RSA blob len | RSA(AES_key) | IV | AES-CTR ciphertext]

Key discrepancy: The ransom note claims “RSA-2048” but the actual hardcoded public key is RSA-4096 (MIICIjAN... = 4096-bit PKCS#8 SubjectPublicKeyInfo format). This is either intentional misdirection or an error in the ransom note template.

RSA Key Loading

The RSA public key is loaded from public_key.pem at startup via:

  • spki::traits::DecodePublicKey::from_public_key_pem (0x40ebb0, 946 bytes)
  • Validates PEM label is exactly "PUBLIC KEY" (PKCS#8 / RFC 5280 SubjectPublicKeyInfo format)
  • Parses the DER-encoded SPKI structure via SubjectPublicKeyInfo::try_from
  • Extracts RSA components (n, e) via rsa::encoding::TryFrom<SubjectPublicKeyInfo>::try_from
  • The key is stored in an Arc<RsaPublicKey> for thread-safe sharing across the Rayon thread pool

Hardcoded fallback key: If public_key.pem cannot be read, the ransomware falls back to a hardcoded RSA-4096 PEM public key embedded at address 0x68CD2C (800 bytes). This ensures encryption proceeds even if the operator forgets to deploy the key file.

Figure 4: Embedded RSA-4096 public key in .rdata — full PEM at 0x68CD2C with cross-references to mw_encrypt_main and mw_process_file_with_logic

Embedded RSA-4096 Public Key (click to expand) ``` -----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxd8bb37MwlfEXkUYV5Hl HpHZAZy7McqWjHBqrA6KHJxPtOnoFDCjBrjG5CHZ6z8Mp6ioW1ffDCQUvsF8CCf4 iZZzACW5l3ZdO30mVBd4oiiXySXaIlJj5WV0NuLEd88SpCHLtaFJesMMiwk1cmnq No0D0g0VFlOL/hG4nV3gEMG+6Fz5CWZsEQ91DD1RfoldCqKINCELyLrugcbhLDQJ 5QPup4hn/4CZ93sZMbL+bb8Bt42yYPvuZmnivnLg/HPLJh9fsU53BU0qSUiBDMSo SzuZOyO77L3RW27esbGpio7WVFwtx/EzLLtXwGiTOe6H3MGYJakrRYhdHv3ediqO DEbbzMbWkWeBHTY2d89jGCajf4nfGQ8VviKGy9xEJzb6xM1eNtV+dawhm8rtqcIN hkxsZ176atYYnXT+d9RrWu2pqkLR9FpAUAU3MgRyqgibRH2783VNLf+To2H5JI7w 1nYMw0/MCqNHUPKaJA8kh9hnvvq7AlDijzYzQ3yRCmbvPFFM/UhTmtlqevslDXQp 40OYtf3X7MuDaf7/OZRaMD1RFCjEnc6owDOvyDFml0ft402fzOebfIivJc5kQVAy 9VDJKZI42VZbDr7cr4dQHzGQenbeGijbel3a1KtWE7W9y5UKqZw3t85rlRo/Yhb/ pd3BHU36MXciTF88XGni4WcCAwEAAQ== -----END PUBLIC KEY----- ``` **Key details**: 4096-bit RSA, e=65537 (`0x10001`), modulus starts with `0x00:c5:df:1b:6f:7e:cc:c2:57...`

Per-File Key Generation (CSPRNG)

Each file gets a unique AES-256 key and IV generated using the OS cryptographic random number generator:

<rand_core::os::OsRng as rand_core::RngCore>::fill_bytes → pbBuffer (32 bytes for AES key)
<rand_core::os::OsRng as rand_core::RngCore>::fill_bytes → v165    (16 bytes for IV)

On Windows, OsRng uses BCryptGenRandom and ProcessPrng (via bcryptprimitives.dll), which are cryptographically secure PRNGs.

RSA-OAEP Key Wrapping

Figure 1: mw_oaep_new_sha256 pseudocode — OAEP padding initialization with SHA-256 IV constants (0x6A09E667, 0xBB67AE85, etc.)

The per-file AES key is encrypted using RSA-OAEP:

// process_file_with_logic @ 0x4021e0
rsa::oaep::Oaep::new(Buf1);                          // 0x40d090
<rsa::oaep::Oaep as rsa::traits::padding::PaddingScheme>::encrypt(
    Src,          // output
    Buf1,         // OAEP padding scheme
    v158,         // RNG
    a4,           // RSA public key
    pbBuffer,     // 32-byte AES key
    0x20u         // key length
);

OAEP Padding Construction (RFC 8017 Section 7.1.1)

The Oaep::new function at 0x40d090 (324 bytes) constructs the OAEP padding scheme with precisely verified parameters:

Parameter Value Verification
Hash Algorithm SHA-256 Confirmed by IV constants: 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 (FIPS 180-4)
MGF Function MGF1-SHA-256 Same SHA-256 hasher, separate instance
Label Empty (default) Flag 0x80000000 at offset 0 indicates no custom label
Hash Output Size 32 bytes DynDigest::output_size() returns 32

Two separate SHA-256 hasher instances (112 bytes each) are allocated — one for computing the label hash (lHash), one for the MGF1 mask generation function.

The PaddingScheme::encrypt function at 0x40d4b0 (3,259 bytes) implements the full OAEP-EME encoding:

1. Compute k (RSA modulus byte length) via BitScanReverse on modulus limbs
2. Verify: mLen + 2*hLen + 2 <= k  (for SHA-256: max plaintext = k - 66 bytes)
3. Construct EM buffer (k bytes):
   EM = 0x00 || seed (32 random bytes via OsRng) || DB
   DB = lHash (SHA-256 of empty label) || PS (zero padding) || 0x01 || M (AES key)
4. MGF1 mask generation (two passes):
   maskedDB = DB ⊕ MGF1(seed, DB_len)         // mask DB with seed
   maskedSeed = seed ⊕ MGF1(maskedDB, hLen)    // mask seed with maskedDB
5. RSA raw encryption:
   Convert EM to BigUint (SSE2 SIMD byte-reversal for performance)
   c = m^e mod n  (BigUint::modpow)
   Convert back to big-endian, zero-padded to k bytes
6. Zeroize all intermediate BigUint values and buffers

The MGF1 implementation at 0x546c40 uses:

  • SHA-256 hashing of seed || counter (4-byte big-endian counter)
  • SSE2 _mm_xor_ps for 32-byte-at-a-time XOR acceleration
  • Counter increments with carry propagation

Validation checks before encryption:

  • RSA exponent must be odd (*v22 & 1)
  • RSA modulus must be odd
  • Exponent must be less than modulus (cmp_slice)
  • Modulus bit length >= 2
  • Message fits within OAEP overhead

AES-256 Encryption with Hardware Autodetection

CPUID-Based AES-NI Detection

The AES-NI detection function at 0x5e2c20 (aes::autodetect::aes_intrinsics::init_inner) performs:

CPUID(EAX=1)    check ECX bit 25 (AES-NI feature flag)
Result stored in:  aes::autodetect::aes_intrinsics::STORAGE  (global boolean)

All subsequent AES operations dispatch based on this flag:

// cipher::stream::AsyncStreamCipher::encrypt_inout @ 0x412c40
v6 = *aes::autodetect::aes_intrinsics::STORAGE;
if ( v6 == 1 )
{
    // Hardware path: AES-NI instructions
    <aes::autodetect::Aes256 as cipher::block::BlockEncrypt>::encrypt_with_backend::inner();
}
else
{
    // Software fallback: fixslice implementation
    aes::soft::fixslice::aes256_encrypt(v19, a1, &v20);
}

AES-256 Key Schedule (Hardware Path)

The key expansion function Aes256Enc::new at 0x5e22c0 (1,333 bytes) generates 15 round keys (240 bytes) from the 256-bit key using the AESKEYGENASSIST hardware instruction:

Input: 256-bit key as two 128-bit halves (K0, K1)
Output: 15 round keys (RK0..RK14) stored in 240-byte buffer

For each round:
  Even rounds: shuffle = _mm_shuffle_epi32(AESKEYGENASSIST(prev), 0xFF)  // broadcast highest word
  Odd rounds:  shuffle = _mm_shuffle_epi32(AESKEYGENASSIST(prev), 0xAA)  // broadcast third word
  RK[i] = prev ⊕ shift_left(prev, 32) ⊕ shift_left(prev, 64) ⊕ shift_left(prev, 96) ⊕ shuffle

The inverse key expansion (inv_expanded_keys at 0x401550, 204 bytes) transforms encryption round keys into decryption round keys using the AESIMC (Inverse MixColumns) instruction for rounds 1–13, with rounds 0 and 14 copied unchanged.

AES-256 Block Encryption (14 Rounds Confirmed)

Figure 2: AES-NI single block encryption — 13x AESENC + AESENCLAST confirming 14 AES-256 rounds (disassembly and decompiler side-by-side)

Three inner encryption functions confirm 14 AES rounds (AES-256):

Address Size Function Pattern
0x412e60 125 B Single block encrypt XOR(RK0) → 13× AESENCAESENCLAST(RK14)
0x412ee0 144 B XOR-then-encrypt XOR(block1, block2)store → 14-round encrypt
0x412f70 437 B Block-loop CTR mode Pre-loads all 15 RKs into XMM registers, processes blocks in a loop with counter feedback via _mm_shuffle_epi32

The block-loop function at 0x412f70 pre-loads all 15 round keys into local stack variables and XMM registers for maximum throughput. After each block encryption, the ciphertext is decomposed via _mm_shuffle_epi32 with selectors 0x55, 0xEE, 0xFF to extract individual 32-bit lanes for the next iteration’s counter/XOR state — this is the CTR mode counter processing.

Software Fallback (Fixslice32)

  • Implementation: aes::soft::fixslice from aes-0.8.4/src/soft/fixslice32.rs
  • Technique: Bitsliced constant-time implementation resistant to timing side-channel attacks
  • Functions: Located at 0x5dc0900x5e16f0 (multiple helper functions)
  • Performance: Slower than AES-NI but provides identical security guarantees

Figure 5: AES-256-CTR encryption dispatch — AES-NI hardware path vs fixslice32 software fallback based on CPUID detection

The cipher operates in AES-256-CTR stream cipher mode via AsyncStreamCipher, processing data in 16-byte blocks. The counter state is maintained in a1[30] (480 bytes offset into the cipher state).

Cryptographic Parameters Summary

Parameter Value
Symmetric Algorithm AES-256-CTR (stream cipher mode)
AES Key Size 256 bits (32 bytes)
AES IV Size 128 bits (16 bytes)
AES Rounds 14 (confirmed in disassembly: 13× AESENC + 1× AESENCLAST)
AES-NI Detection CPUID EAX=1, ECX bit 25
AES Software Fallback fixslice32 constant-time bitsliced (timing side-channel resistant)
Asymmetric Algorithm RSA-4096 (actual) / RSA-2048 (claimed in ransom note)
RSA Padding OAEP (RFC 8017 Section 7.1.1)
OAEP Hash SHA-256 (confirmed by IV constants 0x6A09E6670x5BE0CD19)
OAEP MGF MGF1-SHA-256 (SSE2-accelerated XOR)
OAEP Label Empty (default)
RSA Key Format PKCS#8 SubjectPublicKeyInfo PEM (-----BEGIN PUBLIC KEY-----)
RSA Implementation rsa crate 0.9.9 with num-bigint-dig 0.8.6 for modpow
RNG OsRngProcessPrng (primary) / BCryptGenRandom (fallback)
Key Material Cleanup zeroize 1.8.2 (Vec<u8>, BigUint, MaybeUninit<Z>)

RAM-Aware Throttling

One of 0APT’s most distinctive features is a sophisticated memory-pressure-aware encryption throttling system. Since the ransomware reads entire files into memory for encryption (no partial/chunked encryption), it must carefully manage memory to avoid OOM crashes.

Memory Monitoring Infrastructure

The sysinfo crate’s refresh_memory_specifics function at 0x43cca0 uses two Windows APIs:

API DLL Purpose
GlobalMemoryStatusEx kernel32 Total/available physical memory, virtual memory stats
K32GetPerformanceInfo psapi Commit totals, page size, handle/process/thread counts

The function writes memory statistics into the SystemInner struct:

  • Offset +112: Total/Available physical memory (from MEMORYSTATUSEX)
  • Offset +128-132: Used memory = (TotalPhys - AvailPhys) * PageSize
  • Offset +136: Available RAM (the value checked by the throttler)

Throttle Decision Flow

// Global atomic counter tracking total RAM consumed by active encryption threads
encrypt::ACTIVE_THREADS_RAM_BYTES  // atomic i64

// Step 1: Compute projected memory requirement
projected = file_size + (config.max_size_gb_in_mb << 20);  // file + overhead buffer

// Step 2: Acquire mutex on sysinfo System and timer state
lock(Arc<Mutex<System>>);
lock(Arc<Mutex<TimerState>>);

// Step 3: Conditionally refresh RAM stats (based on ram_refresh_ms interval)
if (elapsed_ms >= config.ram_refresh_ms) {
    sysinfo::common::System::refresh_memory(system);
    timer = Instant::now();  // reset refresh timer
}

// Step 4: Read available RAM from sysinfo struct (offset +136/+140)
available_ram = system.available_memory;

// Step 5: First check — can we proceed?
if (ACTIVE_THREADS_RAM_BYTES + projected < available_ram) {
    goto ENCRYPT;  // proceed immediately
}

// Step 6: Over budget — backoff and retry
std::thread::sleep(Duration::from_secs(3));  // 3-second backoff
// Re-acquire mutexes, refresh memory again
if (STILL over budget after second check) {
    return;  // skip this file entirely (will retry in cycle 2)
}

ENCRYPT:
// Step 7: Atomically register our memory usage
do {
    old = ACTIVE_THREADS_RAM_BYTES;
} while (CAS(&ACTIVE_THREADS_RAM_BYTES, old, old + file_size) != old);

// ... encrypt file ...

// Step 8: Atomically deregister (ALWAYS runs, even on error)
do {
    old = ACTIVE_THREADS_RAM_BYTES;
} while (CAS(&ACTIVE_THREADS_RAM_BYTES, old, old - file_size) != old);

Design Analysis

This design prevents the ransomware from consuming all available RAM when encrypting many large files in parallel, which would cause:

  • System instability alerting the victim prematurely
  • OOM kills terminating the ransomware process
  • System hangs preventing encryption completion

The atomic CAS (Compare-and-Swap) loop ensures lock-free thread coordination without mutex contention on the global counter. The 3-second backoff provides time for other threads to complete and free memory. The guaranteed deregistration on both success and error paths prevents memory accounting leaks that would gradually starve subsequent files.


File Processing Pipeline

File Selection Logic

Figure 3: mw_process_file_with_logic decompiled pseudocode — the 914-line per-file encryption pipeline with .0apt extension check and config struct access

The encrypt::process_file_with_logic function (0x4021e0, 7,297 bytes) implements a multi-layered exclusion system. All path comparisons are case-insensitive (via to_lowercase):

 1. Get file metadata (std::sys::fs::metadata)
 2. Convert full path to lowercase
 3. Check if already encrypted (.0apt extension) → SKIP
 4. Check path against folder exclusion list (substring match) → SKIP
 5. Check file extension against extension exclusion list → SKIP
 6. Check filename against filename exclusion list → SKIP
 7. Check file size against max_size_gb threshold → SKIP if too large
 8. RAM availability check with throttling (may defer to cycle 2)
 9. Atomically register file size in ACTIVE_THREADS_RAM_BYTES
10. Open file read-only, read ENTIRE contents into memory (read_to_end)
11. Generate per-file AES-256 key (32 bytes via OsRng)
12. Generate per-file IV (16 bytes via OsRng)
13. RSA-OAEP encrypt the AES key with RSA-4096 public key
14. AES-256-CTR encrypt the file contents in-place
15. Write encrypted file (overwrite original with new format)
16. Flush to disk (sync_all)
17. Drop ransom note in the same directory
18. Rename file with .0apt extension appended
19. Atomically deregister file size from ACTIVE_THREADS_RAM_BYTES
20. Print "LOCKED: {path}" on success, or "Error for {path}: {error}" on failure

Figure 9: .0apt extension check disassembly — XOR comparison against 0x7061302E (".0ap") with config struct field access

The .0apt extension check at step 3 uses an optimized 5-byte comparison:

// Check last 5 bytes of lowercase path
!(*(_DWORD *)(ptr + len - 5) ^ 0x7061302E     // ".0ap"
 | *(unsigned __int8 *)(ptr + len - 1) ^ 0x74) // "t"

The folder exclusion at step 4 uses Boyer-Moore-like substring matching (StrSearcher::new) — each configured folder pattern is searched as a substring of the full file path. This means a folder entry like /google will match any path containing that substring, not just exact directory names.

No partial encryption: Files are read entirely into memory via std::io::Read::read_to_end. There is no chunked or header-only encryption for large files. The max_size_gb limit (default 1 GB) caps which files are eligible, and the RAM throttling system prevents memory exhaustion.

Encrypted File Format

The original file is overwritten in-place with a new structure, then renamed:

┌──────────────────────────────────────────────────────────┐
│  Offset  │  Size      │  Content                         │
├──────────┼────────────┼──────────────────────────────────┤
│  0       │  4 bytes   │  RSA blob length (big-endian)    │
│  4       │  N bytes   │  RSA-OAEP encrypted AES-256 key  │
│          │            │  (N = 512 for RSA-4096)          │
│  4+N     │  16 bytes  │  AES IV (plaintext)              │
│  4+N+16  │  variable  │  AES-256-CTR encrypted file data │
└──────────────────────────────────────────────────────────┘

Written sequentially via std::io::Write::write_all:

  1. 4 bytes: _byteswap_ulong(rsa_blob_size) — big-endian RSA blob length
  2. N bytes: RSA-OAEP encrypted AES key (typically 512 bytes for RSA-4096)
  3. 16 bytes: AES IV (stored in plaintext for decryption)
  4. Variable bytes: AES-256-CTR encrypted original file contents
  5. std::fs::File::sync_all() — flush to disk to ensure data integrity

Output file opened with: {read: false, write: true, create: true, truncate: true}

File Renaming

After writing the encrypted content:

  • Files with extension: document.docxdocument.docx.0apt
  • Files without extension: READMEREADME.0apt

The renaming uses std::path::Path::file_stem and the original extension to construct the new name.

Ransom Note Deployment

After each successful file encryption:

  1. Extract the parent directory of the encrypted file via std::path::Path::parent()
  2. Join with "README0apt.txt" (14 bytes at 0x68C2AF)
  3. Write the ransom note via std::fs::write() — the 1,876-byte constant at 0x68C3EC
  4. The note is dropped into every directory that contains at least one encrypted file

Additionally, company.txt and public_key.pem are dropped alongside the ransom note for victim-specific personalization and key backup.


Multi-Threaded Architecture

Rayon Thread Pool

0APT leverages the Rayon parallel computing library for concurrent file encryption:

encrypt::main
  └─> rayon::iter::ParallelIterator::for_each (0x408480)
       └─> walkdir entries → filter_map → par_bridge → for_each
            └─> encrypt::process_file_with_logic (per file)

The pipeline:

  1. walkdir::WalkDir recursively enumerates directories
  2. FilterEntry applies encrypt::is_skipped_dir to skip system directories
  3. filter_map filters and transforms entries
  4. par_bridge converts the sequential iterator into a parallel iterator
  5. for_each dispatches process_file_with_logic across the thread pool

The thread count defaults to rayon_core::current_num_threads() (CPU core count) but can be overridden via:

  • max_parallel in config2.txt
  • RAYON_NUM_THREADS environment variable (Rayon built-in)

Path Collection

The ransomware supports two path enumeration modes:

Mode 1 — Operator-specified paths (allpath.txt):

  • If allpath.txt exists, it is parsed line-by-line into path entries
  • Files are processed directly from this list
  • The path list is split in half, and both halves are processed in parallel via rayon_core::registry::in_worker

Mode 2 — Automatic disk enumeration (fallback):

  • If allpath.txt does not exist, the ransomware enumerates all mounted disks via sysinfo::common::Disks::new_with_refreshed_list()
  • For each disk, uses WalkDir-style recursive iteration
  • Entries are piped through par_bridge for parallel processing via Rayon’s work-stealing scheduler

A dedicated background thread is spawned (std::thread::Builder::spawn_unchecked) before the main encryption loop, related to the parallel processing setup (passes max_parallel thread count parameters).


Error Handling & Resilience

The ransomware is designed for maximum encryption coverage with graceful error handling:

Mechanism Implementation
Non-fatal errors Every I/O operation returns Result. On failure, errors are caught via anyhow::Error::from() and logged — the thread continues to the next file
Success logging "LOCKED: {path}" printed to stdout on successful encryption
Error logging "FAILED: {path} \| Error: {error}" printed on failure
RAM counter cleanup ACTIVE_THREADS_RAM_BYTES is always decremented on both success and error paths, preventing memory accounting leaks
Buffer deallocation Read buffer is freed on both success and error paths
RSA failure handling If RSA-OAEP encryption fails, the error is propagated via anyhow and the file is skipped
Mutex poisoning Explicit check of std::panicking::panic_count::GLOBAL_PANIC_COUNT — marks mutex guards as poisoned if a panic is in progress
Two-pass retry Files that fail or are deferred in cycle 0 get a second attempt in cycle 1
No retry per-file Individual files are not retried within a single cycle — only across the 2-cycle boundary

Post-Encryption Activities

Wallpaper Change

Figure 6: Wallpaper module disassembly — embedded PNG drop to %TEMP%, SystemParametersInfoW call for desktop change

The encrypt::change::run_wallpaper_module function (0x40f0c0, 687 bytes) performs:

  1. Gets the Windows temp directory via std::env::temp_dir()
  2. Creates a file at %TEMP%\embedded_wallpaper.png
  3. Writes 24,886 bytes (0x6136) of embedded PNG data from address 0x68D855
  4. Converts the file path to UTF-16 wide string
  5. Calls SystemParametersInfoW(0x14, 0, pvParam, 3):
    • 0x14 = SPI_SETDESKWALLPAPER
    • 3 = SPIF_UPDATEINIFILE | SPIF_SENDCHANGE (persists the change and notifies all windows)

Embedded PNG image details:

Property Value
Location in binary 0x68D855
Size 24,886 bytes (0x6136)
Header 89 50 4E 47 0D 0A 1A 0A (valid PNG signature)
Dimensions 800 x 600 pixels
Color depth 8-bit RGB (color type 2)
Drop path %TEMP%\embedded_wallpaper.png

Debug strings reveal Hindi-language messages:

"Wallpaper set ho raha hai: "     // "Wallpaper is being set: " (Hindi/Urdu)
"Wallpaper change"
"Wallpaper successfully change "
"Embedded image temp \n"           // Debug trace for temp file creation

Process Termination

The ransomware uses the sysinfo crate to enumerate running processes and taskkill.exe to terminate them:

// sysinfo::windows::process::ProcessInner::kill_with @ 0x432D20
if (signal == 9) {  // SIGKILL equivalent
    Command::new("taskkill.exe")
        .arg("/PID").arg(pid_string)
        .arg("/F")                          // Force kill
        .creation_flags(0x8000000)          // CREATE_NO_WINDOW — hide console
        .spawn();
}

The process enumeration infrastructure uses NtQuerySystemInformation with SYSTEM_PROCESS_INFORMATION (via sysinfo::windows::process::refresh_processes_specifics at 0x43D160). The CREATE_NO_WINDOW flag (0x8000000) ensures the taskkill.exe console window is hidden from the user.

Command Execution Infrastructure

The standard Rust std::process::Command shell invocation at 0x6E58A4:

cmd.exe /e:ON /v:OFF /d /c "
  • /e:ON — Enable command extensions
  • /v:OFF — Disable delayed environment variable expansion
  • /d — Disable AutoRun registry commands
  • /c — Execute command and terminate

Additional reference: \\.\NUL at 0x6E5F3F (null device for output redirection — suppressing command output).

What 0APT Does NOT Do

Unlike more mature ransomware families, 0APT is notably missing several common destructive techniques:

Technique Status Notes
Shadow copy deletion (vssadmin) Not present No strings or code for vssadmin delete shadows
Recovery disabling (bcdedit) Not present No boot configuration manipulation
Persistence (scheduled tasks, registry Run keys) Not present One-shot execution only
C2 communication / beaconing Not present Only passive .onion URL in ransom note
Data exfiltration Not present No evidence of file upload or network callbacks
Service stopping (SQL, Exchange, backup software) Not present No named service/process kill lists
Event log clearing Not present No wevtutil or log manipulation
Safe mode boot Not present No bcdedit /set safeboot

This limited post-encryption behavior suggests the ransomware is designed as a focused encryption tool, with the operator expected to handle pre-encryption reconnaissance, lateral movement, and data exfiltration separately.


Ransom Note Analysis

Figure 10: Ransom note embedded in .rdata — "::: 0APT LOCKER :::" header, Tor onion URL, and victim ID visible at 0x68C3EC

The full ransom note (1,876 bytes, embedded at 0x68c3ec):

::: 0APT LOCKER :::

!!! ALL YOUR FILES ARE ENCRYPTED !!!

Hello,

If you are reading this message, it means your company's network has been breached
and all your data has been encrypted by "0apt" group.

WHAT HAPPENED?
We have exploited vulnerabilities in your network infrastructure. All your servers,
databases, and backups have been locked with military-grade encryption algorithms
(AES-256 & RSA-2048). You cannot recover your files without our private key.

DATA LEAK WARNING:
Before encryption, we downloaded your confidential data. If you refuse to pay or
do not contact us, this data will be published on our Tor blog for your competitors
and regulators to see.

HOW TO GET YOUR FILES BACK?
We are not interested in destroying your business, we only want payment.
You must purchase a unique decryption tool from us.

>>> LEGAL & REPUTATION NOTICE (IMPORTANT):
We have analyzed your files. If you do not pay:
1. We will send copies of this incriminating data directly to your GOVERNMENT
   agencies and regulators to trigger an investigation against you.
2. We will email your clients, business partners, and everyone in your CONTACT
   LIST to inform them that you lost their data.

INSTRUCTIONS:
1. Download and install Tor Browser: https://www.torproject.org/
2. Open Tor Browser and navigate to our chat portal:
   http://oaptxiyisljt2kv3we2we34kuudmqda7f2geffoylzpeo7ourhtz4dad.onion/login.php
3. Enter your Personal ID to start the negotiation.

Your Personal ID:
6BE2-1B07-2D14-0APT-KEY

DEADLINE:
You have 24 hours to contact us. After this, the price will double.
If we do not hear from you within 48 hours, your data will be leaked permanently.

ATTENTION:
- Do not rename encrypted files.
- Do not try to decrypt using third-party software (you may lose data forever).
- Do not call the police or FBI (we will leak data immediately).

-- 0apt Team --

Notable characteristics:

  • Classic double-extortion model (encryption + data leak threat)
  • Tor-based negotiation portal with .onion address
  • Escalating deadline pressure (24h price double, 48h permanent leak)
  • Hardcoded victim ID (6BE2-1B07-2D14-0APT-KEY) — likely a placeholder or per-build ID
  • Legal/regulatory threats as additional pressure
  • Anti-recovery warnings to discourage third-party decryption attempts

Anti-Analysis Assessment

No deliberate anti-analysis techniques were detected:

Category Findings
Anti-Debugging None — no IsDebuggerPresent, timing checks, or debug register manipulation
Anti-VM None — no CPUID checks, registry VM artifacts, or MAC address checks
Anti-Sandbox None — no sleep acceleration detection, user interaction checks, or environment fingerprinting
Anti-Disassembly 9 functions with high basic block density (false positives from Rust/DER parsing code)

The absence of anti-analysis is notable for modern ransomware. This suggests either:

  1. The developers prioritized encryption speed over evasion
  2. The ransomware is deployed post-exploitation (after initial access is already achieved)
  3. The operation is less mature than established RaaS groups

Build Environment & Crate Analysis

Developer Environment

Source paths embedded in the binary reveal the build environment:

/home/kali/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/...

Key observations:

  • Build OS: Kali Linux — a penetration testing distribution
  • Username: kali (default Kali Linux user)
  • Cross-compilation: Built on Linux targeting Windows i686 (32-bit) via MinGW/GCC
  • Rust source: src/bin/encrypt.rs — the main ransomware source file
  • Rustc commit: ded5c06cf21d2b93bffd5d884aa6e96934ee4234

OPSEC Leak: Hindi Debug Strings

The wallpaper module contains debug messages in Hindi/Urdu:

"Wallpaper set ho raha hai: "   // "Wallpaper is being set: "

The phrase “ho raha hai” (हो रहा है) is Hindi/Urdu for “is happening/being done.” This is a significant OPSEC leak suggesting the developer’s primary language is Hindi or Urdu, pointing to a South Asian origin for at least one developer in the 0APT group.

Figure 7: Hindi debug strings in .rdata — "Wallpaper set ho raha hai" and other OPSEC-leaking developer messages

Rust Crate Dependencies

Category Crate Version Purpose
Crypto rsa 0.9.9 RSA-4096 OAEP public key encryption
  aes 0.8.4 AES-256 encryption with AES-NI autodetection
  sha2 0.10.9 SHA-256 for OAEP hash and MGF1
  digest Cryptographic hash trait interface (DynDigest)
  cipher Stream cipher trait (AsyncStreamCipher)
  generic_array Fixed-size arrays for crypto block operations
Key Encoding der 0.7.10 ASN.1 DER encoding/decoding
  spki SubjectPublicKeyInfo parsing
  pkcs8 Private key format support
  pem-rfc7468 0.7.0 PEM file format parsing
  base64ct 1.8.1 Constant-time Base64 encoding/decoding
  const-oid 0.9.6 OID constants for crypto algorithms
Math num-bigint-dig 0.8.6 Big integer arithmetic for RSA modpow
  num-integer 0.1.46 Integer traits
  libm 0.2.15 Math library functions
RNG rand 0.8.5 Random number generation interface
  rand_core 0.6.4 OS-level CSPRNG (OsRng)
  rand_chacha 0.3.1 ChaCha20-based CSPRNG (seeded from OsRng)
  getrandom Platform CSPRNG seeding (ProcessPrng, BCryptGenRandom)
Security zeroize 1.8.2 Secure memory zeroing (Vec<u8>, BigUint, MaybeUninit)
Parallelism rayon 1.11.0 Parallel iterator framework
  rayon-core 1.13.0 Work-stealing thread pool
  crossbeam-deque 0.8.6 Work-stealing deques for Rayon
  crossbeam-epoch 0.9.18 Epoch-based lock-free memory reclamation
System sysinfo 0.30.13 RAM, CPU, processes, disks monitoring
  windows 0.52.0 Windows API bindings (WMI, COM, shell)
  windows-core 0.52.0 Core Windows API types
  winapi-util 0.1.11 Windows API utilities
I/O walkdir 2.5.0 Recursive directory traversal
  anyhow 1.0.100 Error handling with context
Utility smallvec 1.15.1 Stack-allocated small vectors
  once_cell 1.21.3 Lazy static initialization
  spin 0.9.8 Spinlock primitives
  gimli DWARF debugging support

The use of zeroize for key material cleanup (Vec<u8>, BigUint, MaybeUninit<Z>) demonstrates awareness of cryptographic best practices. The rsa and aes crates from the RustCrypto project are well-audited, production-quality implementations.

CSPRNG Chain

The random number generation pipeline:

OsRng (rand_core)
  └─> getrandom crate
       ├─> ProcessPrng (bcryptprimitives.dll) — primary, modern Windows
       ├─> BCryptGenRandom (bcrypt.dll) — fallback
       ├─> RtlGenRandom — legacy fallback
       └─> RDRAND — hardware random (last resort)

WMI Usage

The binary includes extensive WMI (Windows Management Instrumentation) bindings from the windows 0.52.0 crate, used exclusively by the sysinfo crate for system enumeration (not lateral movement or persistence):

WMI Query Purpose
\Processor(_Total)\% Idle Time CPU utilization monitoring via PDH performance counters
root\WMI namespace System information queries
SOFTWARE\Microsoft\Windows NT\CurrentVersion OS version detection (CurrentBuildNumber, CurrentMajorVersionNumber)

The WMI interfaces (ISWbemObject, ISWbemLocator, ISWbemObjectPath, etc.) are part of the sysinfo crate’s Windows system enumeration infrastructure. The COM initialization (CoCreateInstance, CoInitializeEx, CoSetProxyBlanket) is required for WMI queries.


Import Analysis

The binary imports from 20 DLL modules with 136 total imports:

Cryptographic Operations

DLL API Purpose
bcrypt BCryptGenRandom CSPRNG for key generation
bcryptprimitives ProcessPrng Alternative CSPRNG path

System Information

DLL API Purpose
iphlpapi GetAdaptersAddresses, GetIfTable2 Network adapter enumeration
psapi GetProcessMemoryInfo Process memory statistics
powrprof CallNtPowerInformation Power/system state queries
pdh PdhAddEnglishCounterW, PdhCollectQueryData Performance counter monitoring (CPU usage)

User & Session Enumeration

DLL API Purpose
Secur32 LsaEnumerateLogonSessions, LsaGetLogonSessionData Enumerate logged-in users
netapi32 NetUserEnum, NetGroupEnum, NetUserGetInfo Network user/group enumeration
ADVAPI32 LookupAccountSidW, ConvertSidToStringSidW SID resolution

COM/WMI

DLL API Purpose
ole32 CoCreateInstance, CoInitializeEx, CoSetProxyBlanket COM initialization for WMI queries
oleaut32 SysAllocString, VariantClear BSTR/VARIANT handling for WMI

Desktop Manipulation

DLL API Purpose
user32 SystemParametersInfoW Desktop wallpaper change

Networking

DLL API Purpose
ws2_32 Full socket API (26 functions) Network communications

The extensive ws2_32 imports (including connect, send, recv, accept, listen, bind) suggest either network communication capabilities (C2, data exfiltration) or that the sysinfo and windows crates pulled in comprehensive networking type definitions.


Decryption Feasibility

Decryption without the attacker’s RSA-4096 private key is NOT feasible.

Factor Analysis
Key Generation Per-file AES-256 keys generated via ProcessPrng/BCryptGenRandom (CSPRNG) — no weakness
Key Wrapping RSA-4096 OAEP with SHA-256/MGF1-SHA-256, empty label — standard implementation, no flaws
OAEP Padding Full RFC 8017 compliance with proper seed generation via OsRng — no deterministic padding
AES Implementation RustCrypto aes 0.8.4 crate with AES-NI/fixslice32 autodetection — well-audited
AES Mode CTR mode (stream cipher) — proper nonce usage with per-file random IV
Key Reuse Each file gets a unique 32-byte key AND 16-byte IV — no key reuse vulnerability
Memory Zeroing zeroize crate aggressively wipes Vec<u8>, BigUint, and MaybeUninit buffers
RNG Quality OS-provided CSPRNG (ProcessPrng primary, BCryptGenRandom fallback) — no weak seed
RSA Validation Proper checks: odd exponent, odd modulus, e < n, modulus bits >= 2
File Format RSA blob length stored in big-endian header — clean parsing for decryptor with private key

Potential weak points (minor):

  • The hardcoded fallback RSA key at 0x68CD2C means that if an operator deploys the ransomware without public_key.pem, all victims encrypted with the same binary share the same RSA key pair. If law enforcement seizes the corresponding private key, all files encrypted with the fallback key can be decrypted.
  • The two-pass cycle means some files may fail to encrypt (RAM pressure, file locks) — these files remain unencrypted and recoverable.

The only viable paths to decryption:

  1. Obtaining the attacker’s RSA-4096 private key (law enforcement infrastructure seizure)
  2. If the fallback RSA key was used, compromising the corresponding private key
  3. Recovering files that failed encryption during both passes (file lock protection)
  4. Memory forensics during active encryption (before zeroize wipes keys — very narrow window)

Indicators of Compromise

File Indicators

Type Value
SHA-256 b2f915cbf1a2b6879d278f613b13d790de9a460759142f30457c3238e598e077
Encrypted Extension .0apt
Ransom Note README0apt.txt
Config File config2.txt
Public Key File public_key.pem
Company File company.txt
Path List allpath.txt
Wallpaper %TEMP%\embedded_wallpaper.png

Network Indicators

Type Value
Tor Portal http://oaptxiyisljt2kv3we2we34kuudmqda7f2geffoylzpeo7ourhtz4dad.onion/login.php
Tor Download https://www.torproject.org/

Host-Based Indicators

Type Value
Victim ID 6BE2-1B07-2D14-0APT-KEY
Process Kill taskkill.exe /PID <pid> /F (hidden console, CREATE_NO_WINDOW)
Command Shell cmd.exe /e:ON /v:OFF /d /c "
Wallpaper API SystemParametersInfoW(0x14, 0, path, 3)SPI_SETDESKWALLPAPER
Wallpaper File %TEMP%\embedded_wallpaper.png (800x600, 24,886 bytes)
Atomic Counter encrypt::ACTIVE_THREADS_RAM_BYTES (global memory tracking)
Embedded RSA Key 800-byte PEM at binary offset 0x68CD2C (fallback RSA-4096 key)
Embedded PNG 24,886 bytes at binary offset 0x68D855
Ransom Note 1,876 bytes at binary offset 0x68C3EC

Strings of Interest

"Wallpaper set ho raha hai: "              // Hindi debug string (OPSEC leak)
"Wallpaper change"
"Wallpaper successfully change "
"Embedded image temp \n"
"Cycle 0 finished. Waiting 2 seconds..."   // Two-pass execution
"Process finished after 2 cycles. Exiting..."
"LOCKED: "                                 // Successful encryption log
"FAILED: " / " | Error: "                  // Error logging format
"Key Error"                                // RSA key parsing failure
"failed to spawn thread"                   // Thread creation failure
"config2.txt"
"allpath.txt"
"embedded_wallpaper.png"
"company.txt"
"/home/kali/.cargo/registry/src/"          // Build environment leak
"src/bin/encrypt.rs"                       // Source file name leak

Encrypted File Format Signature

First 4 bytes: Big-endian uint32 (RSA blob length, typically 0x00000200 = 512 for RSA-4096)
Bytes 4-515:   RSA-OAEP encrypted AES-256 key
Bytes 516-531: 16-byte AES IV (plaintext)
Bytes 532+:    AES-256-CTR encrypted original file data

MITRE ATT&CK Mapping

Technique ID Technique Evidence
T1486 Data Encrypted for Impact AES-256-CTR + RSA-4096 OAEP hybrid file encryption
T1491.001 Defacement: Internal Defacement Desktop wallpaper changed to embedded 800x600 PNG
T1489 Service Stop taskkill.exe /PID <pid> /F with CREATE_NO_WINDOW
T1059.003 Command and Scripting Interpreter: Windows Command Shell cmd.exe /e:ON /v:OFF /d /c
T1083 File and Directory Discovery walkdir recursive directory enumeration, disk enumeration via sysinfo::Disks
T1082 System Information Discovery sysinfo crate: RAM (GlobalMemoryStatusEx, K32GetPerformanceInfo), CPU, processes, disks; OS version via registry
T1057 Process Discovery NtQuerySystemInformation(SYSTEM_PROCESS_INFORMATION), GetProcessMemoryInfo
T1016 System Network Configuration Discovery GetAdaptersAddresses, GetIfTable2
T1033 System Owner/User Discovery LsaEnumerateLogonSessions, NetUserEnum, NetGroupEnum
T1047 Windows Management Instrumentation WMI via COM (ISWbemObject, ISWbemLocator, ISWbemObjectPath) for CPU/system queries
T1480 Execution Guardrails Config-driven targeting with config2.txt, allpath.txt, per-file size/RAM thresholds
T1485 Data Destruction Original files overwritten in-place with encrypted content (not recoverable without key)
T1106 Native API NtQuerySystemInformation for process enumeration, InterlockedCompareExchange64 for atomic operations

YARA Rule

rule RANSOM_0APT_Rust_Windows {
    meta:
        description = "Detects 0APT ransomware (Rust, Windows)"
        author = "MalwareCakeFactory"
        date = "2026-03-10"
        hash = "b2f915cbf1a2b6879d278f613b13d790de9a460759142f30457c3238e598e077"
        tlp = "WHITE"

    strings:
        // Ransom note header
        $note1 = "::: 0APT LOCKER :::" ascii
        $note2 = "!!! ALL YOUR FILES ARE ENCRYPTED !!!" ascii

        // File artifacts
        $file1 = "README0apt.txt" ascii
        $file2 = ".0apt" ascii
        $file3 = "config2.txt" ascii
        $file4 = "public_key.pem" ascii
        $file5 = "company.txt" ascii
        $file6 = "allpath.txt" ascii

        // Onion URL
        $url1 = "oaptxiyisljt2kv3we2we34kuudmqda7f2geffoylzpeo7ourhtz4dad.onion" ascii

        // Hindi OPSEC leak
        $hindi1 = "Wallpaper set ho raha hai" ascii

        // Wallpaper artifact
        $wallpaper = "embedded_wallpaper.png" ascii

        // Rust encrypt module symbols
        $rust1 = "encrypt::main" ascii
        $rust2 = "encrypt::process_file_with_logic" ascii
        $rust3 = "encrypt::is_skipped_dir" ascii
        $rust4 = "encrypt::Config" ascii
        $rust5 = "encrypt::ACTIVE_THREADS_RAM_BYTES" ascii
        $rust6 = "encrypt::change::run_wallpaper_module" ascii

        // Build path (OPSEC leak)
        $build = "/home/kali/.cargo" ascii

        // Source file (OPSEC leak)
        $src = "src/bin/encrypt.rs" ascii

        // Victim ID pattern
        $vid = "0APT-KEY" ascii

        // Embedded RSA-4096 key header (first 32 bytes of PEM)
        $rsa_key = "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A" ascii

        // Two-pass execution strings
        $cycle1 = "Cycle" ascii
        $cycle2 = "Process finished after 2 cycles" ascii

        // Status logging
        $log1 = "LOCKED: " ascii

        // Process management
        $proc1 = "taskkill.exe" ascii

    condition:
        uint16(0) == 0x5A4D and
        filesize < 10MB and
        (
            ($note1 and $note2) or
            ($url1) or
            ($hindi1 and $wallpaper) or
            (3 of ($file*)) or
            (2 of ($rust*) and $src) or
            ($vid and $file2) or
            ($rsa_key and $file2 and $file1) or
            ($cycle2 and $log1 and $file2)
        )
}

Additionally, a YARA rule to detect encrypted files produced by 0APT:

rule RANSOM_0APT_Encrypted_File {
    meta:
        description = "Detects files encrypted by 0APT ransomware"
        author = "MalwareCakeFactory"
        date = "2026-03-10"

    condition:
        // RSA-4096 blob: first 4 bytes = big-endian length 0x200 (512 bytes)
        // followed by RSA ciphertext (512 bytes for RSA-4096)
        uint32be(0) == 0x00000200 and
        filesize > 532 and           // 4 + 512 (RSA) + 16 (IV) minimum
        // File extension check would be .0apt (handled by filename)
        // RSA-4096 ciphertext should have high entropy in bytes 4-515
        uint8(4) != 0x00             // RSA ciphertext non-zero start
}

Conclusion

0APT represents a focused, operator-configurable ransomware built with modern Rust cryptographic libraries. Key takeaways:

  1. Robust cryptographic implementation: The hybrid AES-256-CTR + RSA-4096 OAEP scheme (despite the ransom note falsely claiming RSA-2048) uses RustCrypto’s audited crates with proper OAEP padding (SHA-256/MGF1-SHA-256, empty label), per-file CSPRNG-generated keys via ProcessPrng, hardware-accelerated AES-NI with constant-time software fallback, and aggressive zeroize cleanup. No viable cryptographic attack surface exists. Decryption without the private key is not feasible.

  2. Operator-friendly RaaS design: External configuration (config2.txt with 8 tunable parameters, allpath.txt, public_key.pem, company.txt) allows operators to customize targets, parallelism, RAM thresholds, file size limits, and start delays per deployment. The hardcoded fallback RSA key ensures encryption even if the operator deploys without public_key.pem.

  3. Performance engineering: RAM-aware throttling with atomic CAS memory tracking, GlobalMemoryStatusEx/K32GetPerformanceInfo monitoring, 3-second backoff, and Rayon work-stealing thread pools demonstrate sophisticated engineering. The two-pass execution cycle with inter-cycle sleep provides automatic retry for deferred files.

  4. Notable OPSEC failures:
    • Hindi debug strings (“Wallpaper set ho raha hai”) revealing South Asian developer origin
    • Default Kali Linux build paths (/home/kali/.cargo/registry/src/...)
    • Source filename exposed (src/bin/encrypt.rs)
    • Rust compiler commit hash embedded
    • No anti-analysis techniques whatsoever (no anti-debug, anti-VM, anti-sandbox)
    • No string obfuscation or encryption on ransom note, config keys, or debug messages
  5. Limited post-encryption impact: Unlike mature ransomware families (LockBit, Qilin, BlackCat), 0APT does not delete shadow copies, disable recovery, stop services (SQL, Exchange, backup), clear event logs, enable safe mode, establish persistence, or communicate with C2. It is a pure encryption tool — the operator is expected to handle pre-encryption reconnaissance and data exfiltration separately.

  6. Ransom note inaccuracy: The ransom note claims “RSA-2048” encryption but the actual implementation uses RSA-4096. This is either intentional downplaying of the encryption strength or a copy-paste error from a template.

The 0APT group appears to be an emerging threat actor with competent cryptographic engineering but immature operational security. The Rust + MinGW cross-compilation approach from Kali Linux mirrors the broader trend of ransomware groups adopting Rust for its memory safety guarantees, performance, and cross-platform potential.


Baking up malware analysis, one sample at a time. 🎂