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 ifpublic_key.pemmissing) - Embedded 800x600 PNG wallpaper (24,886 bytes) dropped to
%TEMP%\embedded_wallpaper.png - Process termination via
taskkill.exe /PID <pid> /Fwith 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:
- Trimming whitespace using
core::str::trim_matches - Splitting on
:usingCharSearcher::next_match - Converting the key to lowercase for case-insensitive matching
- 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):

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 5280SubjectPublicKeyInfoformat) - 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.

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

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_psfor 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)

Three inner encryption functions confirm 14 AES rounds (AES-256):
| Address | Size | Function | Pattern |
|---|---|---|---|
0x412e60 |
125 B | Single block encrypt | XOR(RK0) → 13× AESENC → AESENCLAST(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::fixslicefromaes-0.8.4/src/soft/fixslice32.rs - Technique: Bitsliced constant-time implementation resistant to timing side-channel attacks
- Functions: Located at
0x5dc090–0x5e16f0(multiple helper functions) - Performance: Slower than AES-NI but provides identical security guarantees

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 0x6A09E667…0x5BE0CD19) |
| 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 | OsRng → ProcessPrng (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 (fromMEMORYSTATUSEX) - 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

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

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:
- 4 bytes:
_byteswap_ulong(rsa_blob_size)— big-endian RSA blob length - N bytes: RSA-OAEP encrypted AES key (typically 512 bytes for RSA-4096)
- 16 bytes: AES IV (stored in plaintext for decryption)
- Variable bytes: AES-256-CTR encrypted original file contents
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.docx→document.docx.0apt - Files without extension:
README→README.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:
- Extract the parent directory of the encrypted file via
std::path::Path::parent() - Join with
"README0apt.txt"(14 bytes at0x68C2AF) - Write the ransom note via
std::fs::write()— the 1,876-byte constant at0x68C3EC - 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:
walkdir::WalkDirrecursively enumerates directoriesFilterEntryappliesencrypt::is_skipped_dirto skip system directoriesfilter_mapfilters and transforms entriespar_bridgeconverts the sequential iterator into a parallel iteratorfor_eachdispatchesprocess_file_with_logicacross the thread pool
The thread count defaults to rayon_core::current_num_threads() (CPU core count) but can be overridden via:
max_parallelinconfig2.txtRAYON_NUM_THREADSenvironment variable (Rayon built-in)
Path Collection
The ransomware supports two path enumeration modes:
Mode 1 — Operator-specified paths (allpath.txt):
- If
allpath.txtexists, 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.txtdoes not exist, the ransomware enumerates all mounted disks viasysinfo::common::Disks::new_with_refreshed_list() - For each disk, uses
WalkDir-style recursive iteration - Entries are piped through
par_bridgefor 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

The encrypt::change::run_wallpaper_module function (0x40f0c0, 687 bytes) performs:
- Gets the Windows temp directory via
std::env::temp_dir() - Creates a file at
%TEMP%\embedded_wallpaper.png - Writes 24,886 bytes (
0x6136) of embedded PNG data from address0x68D855 - Converts the file path to UTF-16 wide string
- Calls
SystemParametersInfoW(0x14, 0, pvParam, 3):0x14=SPI_SETDESKWALLPAPER3=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

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
.onionaddress - 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:
- The developers prioritized encryption speed over evasion
- The ransomware is deployed post-exploitation (after initial access is already achieved)
- 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.

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
0x68CD2Cmeans that if an operator deploys the ransomware withoutpublic_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:
- Obtaining the attacker’s RSA-4096 private key (law enforcement infrastructure seizure)
- If the fallback RSA key was used, compromising the corresponding private key
- Recovering files that failed encryption during both passes (file lock protection)
- Memory forensics during active encryption (before
zeroizewipes 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:
-
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 aggressivezeroizecleanup. No viable cryptographic attack surface exists. Decryption without the private key is not feasible. -
Operator-friendly RaaS design: External configuration (
config2.txtwith 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 withoutpublic_key.pem. -
Performance engineering: RAM-aware throttling with atomic CAS memory tracking,
GlobalMemoryStatusEx/K32GetPerformanceInfomonitoring, 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. - 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
-
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.
- 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. 🎂