Overview
Upon publishing a post about my analysis on a newly discovered Ransomware called “Donex”, I received a hint about a possible vulnerability inside the ransomware by a friend of mine, Josh from InvokeRE, huge thank you to him for making this post possible. The vulnerability we will be covering today is called the Reused key attack, which is present in all stream ciphers. In case you are a victim of this ransomware and need help to recover your files, feel free to contact me on Twitter.
Sample information
SHA256: 0adde4246aaa9fb3964d1d6cf3c29b1b13074015b250eb8e5591339f92e1e3ca
Timestamp: 2024-02-18
Compiler: Microsoft Visual C++
Filetype: Executable
Imported DLLs: Kernel32.dll, User32.dll, Shell32.dll, Advapi32.dll, Mpr.dll, Ws2_32.dll, RstrtMgr.dll
Introduction
In a nutshell, Donex generates a Salsa20 key prior to the file encryption and enumeration and uses this single key for encrypting all of the files. As the name of the Reused key attack vulnerability suggests, reusing a key in a stream cipher can be problematic. In order to recover the files, we need a file that hasn’t been encrypted and it’s encrypted counterpart. For this post I will be using the abstract.h and datetime.h from my python folder as an example. In order to recover your files, you need the plaintext version of a fairly big file and it’s encrypted counterpart. Additionally you need a file you want to recover, this file has to be smaller (on disk) than your plaintext file.
Encryption
First of all, Donex uses a Salsa20 implementation written in C language from GitHub. The function s20_crypt is the function we will focus on. As shown in the picture below, the first argument of this function is the key, the second argument is the key size (note that this argument doesn’t take the key size in bytes but an enum) and the third argument is a pointer to the nonce. The last three arguments won’t be that important for our analysis but they essentially specify the data to be encrypted/decrypted.
Figure 1: Function signature of the Salsa20 routine
Here we can see the key size enum where the value 0 represents S20_KEYLEN_256 and the value 1 S20_KEYLEN_128. In the next picture you will see that the third value on the stack, which is the second function argument is the value 1, which indicates the S20_KEYLEN_128 option. Therefore we can safely say, that this ransomware uses a 128-bit Salsa20 key to encrypt the files on the victims system. Note that the first argument on the stack is the return address to the previous function and is not a function parameter.
Figure 2: Available key sizes
Debugging
I’ve opened up the sample in the x64dbg debugger (32-bit version) and put a breakpoint on the file encryption routine for three different files, when we observe the function parameters, we will see that the second value on the stack, which is the key (01119670), is same for all three files. This ransomware also uses the same nonce for all files which consist of 8 null-bytes.
Figure 3: First file to be encrypted
Figure 4: Second file to be encrypted
Figure 5: Third file to be encrypted
Decryption using the Reused key attack
After detonating the ransomware in my vm, I’ve put three files and my decryptor inside a folder. The first file is the plaintext version of the encrypted abstract.h. As you might’ve already noticed, I also put a decryptor.exe in this folder, which is the decryption tool I’ve written. The source code of this decryption tool will be at the end of this post.
Figure 6: Files prior to decryption
As I’ve mentioned previously, the file size of the plaintext file must be larger than the file, you want to decrypt, in this case, the size of abstract.h is 32KB.
Figure 7: Filesize of abstract.h
On the other hand, the file size of the encrypted datetime.h.f58A66B51 is only 11KB.
Figure 8: Filesize of datetime.h.f58A66B51
When we open the datetime.h.f58A66B51 file in notepad++ or a hex editor, we will see, that the file content is unreadable and thus in an encrypted state.
Figure 9: File content of datetime.h.f58A66B51 before decryption
By typing “decryptor” in a newly opened command prompt, we will see the following prompt. The first argument will be the path or name to the larger encrypted file A and the second argument will be the path/name of the plaintext version of the A. Lastly, the third argument serves as the path/name to the file, that you want to decrypt.
Figure 10: Arguments of the decryptor
In our case, we will type the following command into our command prompt:
decryptor abstract.h.f58A66B51 abstract.h datetime.h.f58A66B51
Figure 11: Using the decryptor
This will prompt us with a message, that our file was successfully decrypted, and when you take a look at the contents in your folder, you will notice, that the campaign ID has been removed from your file name.
Figure 12: Datetime.h decrypted
Lastly, when we open up the datetime.h file in notepad++, we will see, that our file has been successfully recovered.
Figure 13: File content of datetime.h after decryption
Source Code
I’ve compiled the decryptor in Microsoft Visual Studio 2022 in C++14.
#include <Windows.h>
#include <iostream>
typedef unsigned char U8;
typedef unsigned short U16;
typedef unsigned int U32;
typedef unsigned long long U64;
#define RANSOM_EXTENSION ".f58A66B51"
#define HEADER_SIZE 512
/*
1st argument = ciphertext a file path
2nd argument = ciphertext b file path
3rd argument = plaintext a path
*/
void xor_crypt(U8* data, U8* keystream, size_t size_keystream) {
for (size_t i = 0; i < size_keystream; ++i) {
data[i] = data[i] ^ keystream[i];
}
return;
}
std::string stripExtension(const std::string& filePath) {
return { filePath, 0, filePath.rfind(RANSOM_EXTENSION) };
}
int main(int argc, char** argv) {
if (argc < 4) {
// Print Help Message
std::cout <<
"Donex Ransomware decryption using key reuse attack PoC (.f58A66B51 campaign) by https://dissect.ing/\n\n" <<
"1st argument - ciphertext (A) file path (This file should be larger than ciphertext B)\n2nd argument - plaintext version of file A\n3rd argument - ciphertext (B) file path\n\n";
return 0;
}
// Get arguments
const std::string cipher_path_a = argv[1];
const std::string file_to_decrypt_path = argv[2];
const std::string cipher_path_b = argv[3];
// Open Handles to Files
HANDLE hCipherA{ CreateFileA(cipher_path_a.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)};
HANDLE hCipherB{ CreateFileA(cipher_path_b.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL) };
HANDLE hFileToDecrypt{ CreateFileA(file_to_decrypt_path.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL) };
// Get file sizes
DWORD dwFilesizeCiphertextA = GetFileSize(hCipherA, NULL);
DWORD dwFilesizeCiphertextB = GetFileSize(hCipherB, NULL);
DWORD dwFilesizeFileToDecrypt = GetFileSize(hFileToDecrypt, NULL);
// Allocate buffers for storing the file contents
U8* ciphertext_a = (U8*)malloc(dwFilesizeCiphertextA);
U8* ciphertext_b = (U8*)malloc(dwFilesizeCiphertextB);
U8* file = (U8*)malloc(dwFilesizeFileToDecrypt);
// Read file contents into the previously allocated buffers
(void)ReadFile(hCipherA, ciphertext_a, dwFilesizeCiphertextA, NULL, 0);
(void)ReadFile(hCipherB, ciphertext_b, dwFilesizeCiphertextB, NULL, 0);
(void)ReadFile(hFileToDecrypt, file, dwFilesizeFileToDecrypt, NULL, 0);
// Decrypt
xor_crypt(ciphertext_a, ciphertext_b, dwFilesizeCiphertextA);
xor_crypt(ciphertext_a, file, dwFilesizeFileToDecrypt);
// Write back to file
(void)SetFilePointer(hCipherB, 0, 0, FILE_BEGIN);
(void)WriteFile(hCipherB, ciphertext_a, dwFilesizeCiphertextB-HEADER_SIZE, NULL, 0);
(void)SetFilePointer(hCipherB, dwFilesizeCiphertextB - HEADER_SIZE, 0, FILE_BEGIN);
(void)SetEndOfFile(hCipherB);
// Close Handles to Files
(void)CloseHandle(hCipherA);
(void)CloseHandle(hCipherB);
(void)CloseHandle(hFileToDecrypt);
// Free the allocated buffers
free(ciphertext_a);
free(ciphertext_b);
free(file);
// Remove ransomware extension
const std::string filepath = stripExtension(cipher_path_b);
BOOL bRenamed = MoveFileA(cipher_path_b.c_str(), filepath.c_str());
if (bRenamed) std::cout << "Successfully decrypted! :D\n";
return 0;
}
References
https[:]//twitter.com/JershMagersh - for giving me the hint :D
https[:]//invokere.com/ - Josh’s website
https[:]//en.wikipedia.org/wiki/Stream_cipher_attacks - explanation for the vulnerability
https[:]//bazaar.abuse.ch/sample/0adde4246aaa9fb3964d1d6cf3c29b1b13074015b250eb8e5591339f92e1e3ca/ - The sample used in this post
https[:]//chuongdong.com/reverse%20engineering/2022/09/03/PLAYRansomware/ - inspired me with his beautiful layout :P
https[:]//github.com/alexwebr/salsa20 - The Salsa20 C implementation used by Donex
https[:]//chat.openai.com/ - for fixing my broken english