ELF Technical Analysis
The ELF Cl0p variant is developed in a similar logic to the Windows variant, though it contains small differences mostly attributed to OS differences such as API calls. It appears to be in its initial development phases as some functionalities present in the Windows versions do not currently exist in this new Linux version.A reason for this could be that the threat actor has not needed to dedicate time and resources to improve obfuscation or evasiveness due to the fact that it is currently undetected by all 64 security engines on VirusTotal . SentinelOne Singularity detects Cl0p ransomware on both Linux and Windows devices.
Initially, the ransomware creates a new process by calling fork and exits the parent-process. The child-process sets its file mode creation mask to any permission (read, write, execute) by calling umask(0) . It then calls setsid , creates a session and sets the process group ID. It tries to access root by changing the working directory to “/” ( chdir(“/”) ). Once the permissions are set, the ransomware proceeds encrypting other directories.
Targeted Folders & Files
While the Windows versions contain a hashing algorithm in order to avoid encrypting specific folders and files, such functionality was not observed in the Linux variant. The ELF variant targets specific folders, subfolders and all files/types.The discovered ELF sample targets files contained in the following directories for encryption, though we do not exclude the possibility of future versions including more directories.
| Folder | Description |
| /opt | Contains subdirectories for optional software packages |
| /u01 | Oracle Directory, mount point used for the Oracle software only. |
| /u02 | Oracle Directory, used for the database files. |
| /u03 | Oracle Directory, used for the database files. |
| /u04 | Oracle Directory, used for the database files. |
| /home | Contains the home directory of each user. |
| /root | Contains the home directory of the root user. |
Encryption Flaw
Windows versions of Cl0p ransomware use a Mersenne Twister PRNG (MT19937) to generate a 0x75 bytes size RC4 key for each file. This key is then validated (checks if the first five bytes are NULL) and used for file encryption. Then, by using the RSA public key, it encrypts the generated RC4 key and stores it to $filename.$clop_extension. Victims who pay the ransom demand receive a decryptor which decrypts the generated Cl0p file using the RSA private key, retrieves the generated RC4 key, and then decrypts the encrypted file.This core functionality is missing in the Linux variant. Instead, we discovered a flawed ransomware-encryption logic which makes it possible to retrieve the original files without paying for a decryptor.
The Linux variant contains a hardcoded RC4 “master-key” which, during the execution of the main function, is copied into the global variable szKeyKey.
Sample’s RC4 “master-key”:
Jfkdskfku2ir32y7432uroduw8y7318i9018urewfdsZ2Oaifwuieh~~cudsffdsd
During the file encryption phase, the ransomware – similar to the Windows version – generates a 0x75 bytes size RC4 key, with the use of a lookup table and a PRNG byte. This generated RC4 key is used to encrypt the mappedAddress and write it back to the file.
Then by using the RC4 “master-key” the ransomware encrypts the generated RC4 key and stores it to $filename.$clop_extension. By using a symmetric algorithm (second RC4) to “encrypt” the file’s RC4 key, we were able to take advantage of this flaw and decrypt Cl0p-ELF encrypted files.
Cl0p-ELF Decryption Logic:
- Retrieve RC4 “master-key”.
- Read all $filename.$clop_extension.
- Decrypt with RC4 using the RC4 “master-key”, the generated RC4 key.
- Decrypt $filename with RC4 using the generated RC4 key.
- Write decrypted to $filename.
Cl0p File-Key Creation Flaw
The 0x75 bytes size PRNG RC4 key is encrypted with RC4 using the RC4 “master-key”. The encrypted RC4 output is 0x75 bytes size, though writes 0x100 bytes into the created Cl0p key $filename.$clop_extension. This results in writing memory data to the file and more specifically stack variables.
This flaw provides some information regarding the file before encryption. This includes:
- File fstat64 result
- total size, in bytes, file size (st_size)
- time of last status change, exact time of file encryption (st_ctime)
- and more forensics information regarding the file before the encryption.
- Size of buffer for file encryption (with check of >= 0x5f5e100 )
- RC4 “master-key” size
- RC4 PRNG key size
byte encr_rc4key[117]; // encrypted RC4 PRNG key, size 0x75 bytes
stat fdstat; // stat(fd, &fdstat), size 0x58 bytes
long fdid; // file node unique id, size 0x8 bytes
int fd; // file descriptor, size 0x4 bytes
int fdmappedaddr; // file mapped address, size 0x4 bytes
off_t fdsize; // file size, size 0x8 bytes
int rc4_msize; // RC4 "master-key" size, size 0x4 bytes
long rc4_fsize; // RC4 PRNG key size, size 0x8 bytes
int fdnameaddr; // filename string address, size 0x4 bytes
intframeaddr; // frame pointer address, size 0x4 bytes
int retadd; // function return address, size 0x4 bytes
byte fdpathaddr[3]; // part of filepath strings address, size 0x3 bytes
}
Developed Functions & Names
In ELF binaries the .symtab, Symbol Table Section , holds information needed to locate and relocate a program's symbolic definitions and references, allowing us to retrieve function and global variable names.| function name | Description |
| do_heartbeat(void) | Main function which starts the encryption of various folders. |
| find(char*,char const*) | Multiple calls of this function are done by do_heartbeat; this function takes as parameter 1) the starting folder to encrypt (example, “/opt”) 2) regex of files to encrypt (example, “*.*”) and performs a recursive search from the starting folder until encrypts the “matching regex files. |
| CreateRadMe(char *) | This function takes as parameter the folder to create the ransom note. |
| EncrFile(char*) | Encrypts given filepath. |
| existsFile(char*) | Checks if File exists, or if the process has the permissions to open. |
| _rc4Full(void const*,ushort,void *,ulong) | Wrapper function to _rc4Init and _rc4, which is used to encrypt a buffer with a given key. |
| Createkey(char *,uchar *) | Creates and writes into "%s.C_I_0P" the encrypted buffer. |
| Global Variable | Description |
| szKeyKey | Global variable of 0x64 bytes size, initialized during main function, containing RC4 “master-key” which encrypts the “randomly” generated 0x75 bytes size RC4 key. |
Differences to Windows Variant
Rather than simply port the Windows version of Cl0p directly, the authors have chosen to build bespoke Linux payloads. We understand this to be the primary reason for the lack of feature parity between the new Linux version and the far more established Windows variant.SentinelLabs expects future versions of the Linux variant to start eliminating those differences and for each updated functionality to be applied in both variants simultaneously.
Some of the differences worth highlighting are detailed below:
| Differences | Description |
| Files/Folders exclusions | The Windows variant contains a hashing algorithm which excludes specific folders and files from encryption. This functionality was not observed in the Linux variant. |
| extension exclusions | The Windows variant contains a hardcoded list of extensions to exclude from encryption. This functionality was not observed in the Linux variant. |
| Different methods of Reading/Writing depending on file size. | The Windows variant, depending on the size of the file, will choose different methods of reading a file and writing the encrypted buffer. Small files are ignored, medium-sized files will make use of ReadFile / WriteFile , large files will use CreateFileMappingW / MapViewOfFile / UnmapViewOfFile . The Linux variant encrypts all the files using mmap64 / munmap . Both only variants encrypt the first 0x5f5e100 bytes of large files. |
| ransom note decryption | The Windows variant stores the encrypted ransom note as a resource and decrypts it with a simple XOR algorithm. The Linux variant stores the note as plain text in “.rodata”. |
| drive enumeration | The Windows variant initially enumerates through drives in order to “find” the starting point to recursively encrypt the folders. The Linux variant contains hardcoded “starting” folders. |
| RC4 Default Key | Once the Windows variant generates a 0x75 size PRNG RC4 Key, it will check if the first 5 bytes are NULL; if so, it uses the default key for encryption. The Linux version does not perform this validation and does not contain a default RC4 key in case the first 5 bytes of the PRNG RC4 are NULL. |
| Command Line Parameters | The Windows variant can be executed in three ways: 1) without parameters encrypting all local and network drives, 2) with “runrun” parameter encrypting only network drives, 3) with a file as parameter which contains the folders to be encrypted (observed temp .ocx/temp.dat). The Linux variant does not accept command line parameters and recursively encrypts the specified hardcoded folders. |
| RC4 Key Encryption | The Windows variant encrypts the generated RC4 key responsible for the file encryption using the asymmetric algorithm RSA and a public key. In the Linux variant, the generated RC4 key is encrypted with a RC4 “master-key” (flawed logic). |
ransom notes
The Linux variant of Clop ransomware drops a ransom note on victim machines with a .txt format.
This differs somewhat from the Windows .rtf ransom note, although both use the email addresses unlock@support-mult[.]com and unlock@rsv-box[.]com as ways for victims to contact the attackers.
Source: https://www.sentinelone.com/labs/cl...s-with-flawed-encryption-decryptor-available/
Decryptor (source: https://gist.github.com/Tera0017S1/c62a928a911442e1509d127fdcd93b71):
Python:
"""
Author: @Tera0017/@SentinelOne
Description: Clop-Linux ransomware variant files decryption.
Link: https://s1.ai/Clop-ELF
Execution help: $ python3 clop_linux_file_decr.py --help
"""
import argparse
import glob
import os.path
import struct
from arc4 import ARC4
def parse_arguments() -> argparse.Namespace:
"""
Parses commandline parameters if any.
@return: returns parse_args() result -> argparse.Namespace
"""
description = """Python3 script which decrypts files encrypted by flawed Cl0p ELF variant.
More info regarding Cl0p ELF variant and how decryptor was created at https://s1.ai/Clop-ELF
"""
print('=' * 40)
print('SentinelOne Cl0p ELF variant Decryptor.\nAuthor: @Tera0017/@SentinelOne\nLink: https://s1.ai/Clop-ELF')
print('=' * 40)
parser = argparse.ArgumentParser(
prog='clop_linux_file_decr.py',
description=description,
epilog='author:@Tera0017/@SentinelOne')
parser.add_argument('--elfile', default=None, help='ELF Cl0p Binary, is used to retrieve "RC4 master key" else default is used for decryption.')
parser.add_argument('--keys', default=None, help='File containing result of "$ find / -name *.$cl0p_extension -print 2>/dev/null > cl0p_keys.txt". Run with sudo if needed .')
parser.add_argument('--rc4key', default=None, help='RC4 master key for decryption of clop key files. If --elf is provided script will dynamically retrieve it.')
return parser.parse_args()
def message(msg: str) -> None:
"""
@param msg: message to print
@return: None
"""
print(f'*{msg}')
class ClopELFDecryptor:
def __init__(self, filepath=None, clop_find_file=None, rc4_master_key=None):
"""
@param filepath: str, filepath of cl0p elf variant ransomware found in encrypted machine.
@param clop_find_file: str, filepath containing result of "$ find / -name *.$cl0p_extension -print 2>/dev/null > clop_keys.txt"
@param rc4_master_key: str, rc4 master key is not extracted well from cl0p elf binary.
"""
# if elf sample does not exist tries with observed key.
self.elfdata = open(filepath, 'rb').read() if filepath is not None else None
# result of "$ find / -name *.$cl0p_extension -print 2>/dev/null > clop_keys.txt" containing clop keys
self.clop_keys_file = clop_find_file
self.rc4_master_key = rc4_master_key
# clop filekeys extension.
self.clop_ext = ".C_I_0P"
# RC4 generated key size.
self.rc4_gen_key_size = 0x75
def get_rc4_master_key(self) -> bytes:
"""
Retrieves RC4 master key from ELF binary. If elf is not found returns default observed key.
@return: bytes, RC4 master key.
"""
if self.rc4_master_key is not None:
message('User provided RC4 master key')
return self.rc4_master_key
elif self.elfdata is None:
message('Retrieved previous observed RC4 key.')
# observed RC4 master key
return b'Jfkdskfku2ir32y7432uroduw8y7318i9018urewfdsZ2Oaifwueh~~cudsffdsd'
# dirty way to retrieve master key.
f = b'/root'
idx = self.elfdata.find(f) + len(f) + 1
return self.elfdata[idx: idx + 100].lstrip(b'\x00').split(b'\x00')[0]
def get_clop_keys(self) -> list:
"""
Based on the filekeys clop extension retrieves all the encrypted files from the machine.
* If you need to speed up process add specific folders where encryption took place.
* Or pass result of "$ find / -name *.$cl0p_extension -print 2>/dev/null > clop_keys.txt"
as argument to "--keys".
@return: list, encrypted filepaths
"""
if self.clop_keys_file is not None:
# get clop keys "$ find / -name *.$cl0p_extension -print 2>/dev/null > clop_keys.txt"
with open(self.clop_keys_file, 'r') as hfile:
lines = hfile.readlines()
return [l.strip() for l in lines if l.strip()]
# enumerate all folders and find clop extension files.
message(f'Searching for encrypted file extension {self.clop_ext}.')
message('This operation will take several minutes...')
message('To speed up process prefer to use "--keys", parameter.')
return glob.glob(f'/**/*{self.clop_ext}', recursive=True)
def decrypt(self) -> None:
"""
Main function decrypts Clop-ELF encrypted files.
@return: None
"""
message('Starting decryption process.')
# 1. Retrieve RC4 "master-key".
rc4_master_key = self.get_rc4_master_key()
message(f'RC4 Master Key: "{rc4_master_key}"')
# 2. Read all $filename.$clop_extension.
file_keys = self.get_clop_keys()
message(f'Encrypted Files: {len(file_keys)}')
for file_key in file_keys:
message(f'File: {file_key}')
with open(file_key, 'rb') as hfile:
file_key_data = hfile.read()
# 3. Decrypt with RC4 using the RC4 "master-key", the generated RC4 key.
cipher = ARC4(rc4_master_key)
file_rc4_key = cipher.decrypt(file_key_data)[:self.rc4_gen_key_size]
# getting encrypted file size (if file is written again after encryption then
# encrypted_file_size != file_size
size_off = 0x75 + 0x58 + 0x8 + 0x4 + 0x4
try:
encr_file_size = struct.unpack('Q', file_key_data[size_off: size_off + 0x8])[0]
except struct.error:
message(f'[ERROR] Clop key file seems corrupted: {file_key}')
continue
encr_file = file_key.replace(self.clop_ext, '')
# decrypted files have extension '.decrypted_by_S1', once validated can delete and replace encrypted.
decr_file = file_key.replace(self.clop_ext, '.decrypted_by_S1')
if os.path.isfile(encr_file):
with open(encr_file, 'rb') as hfile:
encr_file_data = hfile.read()
else:
message(f'[ERROR] Unable to find encrypted file: {encr_file}')
continue
# 4. Decrypt $filename with RC4 using the generated RC4 key.
cipher = ARC4(file_rc4_key)
decrypted_file_data = cipher.decrypt(encr_file_data[:encr_file_size]) + encr_file_data[encr_file_size:]
# 5. Write decrypted to $filename.
with open(decr_file, 'wb') as hfile:
hfile.write(decrypted_file_data)
message(f'Decrypted: {decr_file}')
if __name__ == '__main__':
# parsing command line arguments for the decryptor. Use --help for more information
parsed = parse_arguments()
ClopELFDecryptor(parsed.elfile, parsed.keys, parsed.rc4key).decrypt()