RansomEXX, Fixing Corrupted Ransom

Brenton Morris
ProferoSec
Published in
13 min readSep 30, 2021

Since the sudden disappearance of the REvil ransomware operation, there has been a rise in other “ransomware as a service” (RaaS) operators attempting to claim their piece of the RaaS market share left behind. Among the most prominent of these groups is RansomEXX / RansomX. They have become infamous not only for their high-profile attacks, but also for the leak site they use to name and shame their victims who don’t adhere to their ransom demands, and for their deployment of ransomware payloads for both Windows and Linux devices.

Based on the high number of recent attacks by this group, the Profero IR team has encountered multiple ransomware cases involving the Linux variant of this malware. During some of these incidents, we analyzed the ransomware and a version of its decryption tool and discovered a bug in the encryption process that left some files corrupted and unable to be decrypted by the tool.

In the following report, we describe how this ransomware and the decryption tool work — and how some corrupted files can potentially be rescued. We also introduce a new tool that can be used to extract the decryption key information from the Linux version of the decryption tool provided by the attackers and then use that configuration to decrypt affected files. We hope that this tool will remove the need to reverse engineer the attacker’s decryption tool during an incident and make for a speedier recovery.

The source code of the tool is publicly available here.

Ransomware Analysis

Summary

RansomEXX has the ability to recursively encrypt files in a list of provided directories using symmetric encryption (AES-CBC). Each file is appended with a header containing information encrypted with an RSA public key — such as the AES key and IV values — so that they can be decrypted. Additionally, this header is regenerated roughly every 0.18 seconds along with a new AES key and IV to prevent decrypting all files with a key and IV recovered by analyzing memory dumps taken from an infected machine. If two files are encrypted within the same 0.18 second period, they will be encrypted with the same key.

The malware appears to be specifically compiled for each attack, with the target organization’s name included in the embedded ransom note, making it harder to share samples publicly. There is also an unused config value containing file extensions which indicate this malware can also be compiled to run on Windows. The use of the mbedTLS library also supports this conclusion, as it can be compiled for various target platforms.

There is no persistence method enabled in this ransomware and it runs as a standalone command line tool which can be executed on the victim machines as part of a multi-staged attack.

Overview

The analyzed samples were not packed or stripped, making our analysis easier as we can see the original function names used by the author. The malware uses the mbedtls library for encryption capabilities:

Execution Flow

When initiated, the malware first loads the ransomware config with the ConfigLoadFromBuffer function and then calls the GeneratePreData function:

The GeneratePreData function (pictured below) carries out the tasks of setting up the mbedtls context, including generating a random seed using the current time as personalization data to add extra entropy and generating the “ransom header” which contains RSA encrypted initialization information for the encryption such as IV and key used. This “header” is appended to each encrypted file so that it can be decrypted:

Next, the malware starts a thread to re-run the above function every 0.18 seconds (180,000 microseconds). Note that inside the GeneratePreData function a mutex is acquired to prevent the context from changing while in use. This means there is no guarantee that it will regenerate any of these values on time. This is likely used to ensure that any key recovered from a memory dump would only be able to decrypt a small number of the most recently encrypted files.

Once this thread is running the main work begins: the malware loops through a list of directories passed to it by a command line and calls the EnumFiles function on each.

From here, the malware initializes the same number of worker threads as the system has processors, and it is these worker threads that handle the actual encryption of each file. The encrypt_dir function is then called.

The encrypt_dir function loops through the directory recursively, creating the ransom note inside each directory and assigning each file discovered to a worker thread, which then perform the encryption. This function skips calling itself on the current directory or the parent directory, and does not encrypt any ransom notes. Interestingly, this function does not make use of the list of file extensions in the config item with index 11, which appears to be a list of file types to skip when encrypting. Instead, it encrypts every file it locates that is not a ransom note.

A high-level view of the function calls made when the malware starts can be seen below:

Configuration

The malware configuration is stored in a list of dynamically sized items. Each item contains the following elements:

This data structure is parsed by the malware in the ConfigLoadFromBuffer function and stored in an easily accessible global config structure to be used during runtime:

By storing the config values in this way, the malware author has applied some very basic obfuscation and hidden the code that references these values from the disassembler, lengthening the time required to analyze this sample.

The malware config contains the following values encoded in this way, as seen below. Blank values are currently unknown or unused

Some of these config values are not used in this binary. This suggests the malware is written using a modular design, which allows the attackers to turn features on or off at compile time. Along with the fact that the malware contains references to the victim organization in the ransom note, this indicates that the malware is compiled for each individual attack.

Encryption Process

When a worker thread receives a file path to encrypt, it calls the CryptOneFile function. This function oversees the target file’s encryption in AES CBC mode using a key size of 256 bits. Each file is appended with the “ransom header” generated in the GeneratePreData function.

The ransomware encrypts files using a rolling window which moves through the file in a manner which depends on which CryptLogic values the malware is using for that particular file.

A CryptLogic is a set of values which determine how a file should be encrypted or decrypted in blocks. The decrypt logic to use for a given file is determined by its size in bytes. Each CryptLogic is a struct with the following C definition:

struct CryptLogic {
uint64_t lowerLimit;
uint64_t upperLimit;
uint64_t chunkSize;
uint64_t blockSize;
};

The lowerLimit value is the lower limit of files to be encrypted/decrypted with the contained chunkSize and blockSize values while the upperLimit is the upper limit for the file size.

The chunkSize is the number of bytes which are read, encrypted/decrypted and then written back to one affected file at a time, while the blockSize is the number of bytes in the file after each chunk is encrypted/decrypted. In the sample analyzed, the blockSize is larger than the corrosponding chunkSize, so the malware will only partially encrypt affected files but it is still enough to render the files unusable.

A description of each value in the CryptLogic is below:

After the CryptOneFile function has appended the encrypted header to a file it calls the ProcessFileHandleWithLogic function — which will get the correct crypt logic values to use — it then works its way through the file using the method described above:

If successful, the ransomware will then rename the encrypted file to indicate it has been encrypted:

A high-level overview of the function calls made by the encrypt_worker thread can be seen below:

Decryption Tool Analysis

Summary

The decryption tool is able to recursively decrypt files in a list of provided directories using AES in CBC mode. Each encrypted file contains a header with information required to decrypt, such as the AES key and IV values used to encrypt the file. This header is read and the key and IV are decrypted, and then removed from the file. Subsequently, the file is decrypted, and returns to its original state.

Due to failure to acquire a lock on the file, it is possible that while the file is being encrypted it is in use by the system and being written to in parallel. This would lead to a file corruption, with encrypted data mixed in with unencrypted data or with extra data being appended to the file after the encrypted header — causing the decryption tool to fail to obtain the correct keys to decrypt the file. We encountered several log files that were partially corrupted due to this flaw. This presence of this flaw could mean that even after paying a ransom to the attackers, victim organizations would not be able to recover some files.

Overview

This file is not packed or stripped and uses the mbedTLS library for AES decryption:

Execution Flow

This file looks very similar to the ransomware component analyzed in this post.

When comparing the two files we can see that there are only a small number of different functions between these samples.

It starts off with the main function similar to the ransomware component without the call to GeneratePreData or the creation of the regenerate_pre_data worker thread. It loads the ransomware config from a buffer using the exact same method documented in the ransomware analysis above, and then calls EnumFiles on each directory passed in via the command line args:

EnumFiles is identical to the ransomware component’s EnumFiles function. It creates a pool of worker threads equal to the number of the victim machine’s CPUs using the init_workers function, and then calls encrypt_dir, passing the target directory path as the only parameter:

The encrypt_dir function is almost identical to the function in the ransomware component with the same name, it loops through all subdirectories recursively and assigns each file found to a worker thread. The only difference here is that this function removes the ransom note instead of dropping one, this can be seen in the picture below:

Ransomware encrypt_dir function compared to the decryption tool.

Configuration

The configuration provided with the decryption tool contains everything it needs to decrypt affected files.

The decryption tool uses the exact same encoding mechanism for its config values as the ransomware component itself, but this time using a lot more config values:

The configuration contains the following encoded values:

Decryption Process

The decryption tool worker threads are almost identical to the workers in the ransomware, except that they call DeCryptOneFile when they receive a path to decrypt, while the ransomware calls CryptOneFile:

The DeCryptOneFile function calls the DeCryptOneFileEx function and then removes the file extension that was added to the file during encryption:

The DeCryptOneFileEx function reads the encryption information stored in the “ransomware header” at the end of the file and ensures that it parses correctly. Then it truncates the file to remove the added header:

It then calls the ProcessFileHandleWithLogic function, instructing it to use the DeCryptOneBlock function to decrypt the file block by block. This function decrypts files in the same way the ransomware encrypted them, calculating which crypt logic configuration to use based on the original file size.

The DeCryptOneBlock function is a simple function which decrypts one 16-byte chunk of a file:

Files Failing to Decrypt

When running this decryption tool on a directory of files, you may find that some files do not decrypt but no visible error is shown. This is due to the bug in the encryption process — files that are being written to at the time of the encryption may be corrupted, especially if the ransomware has already added the “header.” This happens because the ransomware does not lock the files to prevent other applications from writing to them during encryption.

This can be seen in the following screenshot. Above the line highlighted in red is encrypted data, below is the legitimate log data.

When the decryption tool attempts to decrypt a file, it reads the RSA encrypted AES key and IV from the end of the file, parsing the unencrypted log data that was appended as a blob of RSA encrypted data. This causes the call to mbedtls_rsa_pkcs1_decrypt to fail. This is demonstrated below — note the return value in rax is non-zero right after the call to mbedtls_rsa_pkcs1_decrypt:

The decryption tool does not report this error to the user and instead will silently fail and move on to the next file.

Recovering the Corrupted Files

Due to the above bug in the encryption process, some files encrypted by this malware could have been written to by a legitimate application during the encryption process, corrupting the file. If the nature of this corruption is simple, such as ASCII log file data being appended after the encrypted portion of the file, a file can be easily recovered.

During encryption, the malware will append each file with RSA encrypted key information required to decrypt a file once a ransom is paid. When a file is being decrypted this information is read from the end of the file, decrypted with the RSA private key, and then used to decrypt the file. For decryption to work in the case that legitimate data has been appended after the key information we need to truncate the file to remove this data, decrypt the file, and then append the data previously truncated.

In the example below the file would need to be truncated to 0x000013b8 bytes in size. The decryption tool will then run correctly.

If the nature of the corruption is more complex than the above example it may still be possible to recover the file by untangling the encrypted and unencrypted data and then splicing it back together in the correct order but if the legitimate file data was high entropy data (such as an encrypted file) this would likely be impossible.

Config Extractor and Decryption Tool

Because the attackers provide paying victims with a decryption tool they must run to decrypt their files there is a risk that the decryption tool may be malicious. This requires affected victims to reverse engineer the provided decryption tool to ensure there is no hidden payload or malicious features, a time investment that can be problematic for some organizations during a ransomware incident. Due to this we decided to write our own implementation of the Linux decryption tool which can be used to carry out the following tasks:

  • Extract the config required to decrypt files from a Linux decryption tool provided by the attackers
  • Use this config to decrypt files encrypted by the Linux version of RansomEXX

Today we are releasing this tool as an open-source command-line application written in Go. Its usage is as follows:

Usage of ./ransomexx-tools:
-config string
Path of the extracted config file to use for decryption
-debug
Log debug output
-decrypt
Decrypt a list of directories
-decryption-tool string
Path to the decryption tool to extract the config from. Required when using -exconfig
-dirs string
A list of directories to recursively decrypt, separated by a comma
-exconfig
Extract the config from a decryption tool provided by the RansomEXX group
-num-workers int
Number of workers to use for decryption (default 4)
-out string
The file to save the extracted config to.

This tool can be found on our GitHub here: proferosec/RansomEXX-Tools (github.com)

Extracting the Config

To extract the config containing all decryption key information from a decryption tool provided by the attackers simply run the tool with the following parameters:

./ransomexx-tools -exconfig -decryption-tool /path/to/attcker/provided/decryption-tool -out config.json

You will then have a file in the current directory named config.json with the extracted configuration values.

Decrypting Files

Once the config has been extracted you can use this tool to decrypt your files instead of the attacker-provided decryption tool. Run the tool with the following parameters:

./ransomexx-tools -decrypt -config config.json -dirs /path/to/decrypt,/second/path/to/decrypt

The -decrypt mode takes the path to the config file in -config and a comma-separated list of directories to recursively search for files to decrypt in the -dirs parameter.

--

--