Rhysida Ransomware Malware Analysis - Part 2: How to Decrypt
In the second part of our malware analysis walkthrough of Rhysida ransomware, we will pick up where we left off from part 1 (analysis and debugging) and explore how to decrypt the ransomware.
In the second part of our malware analysis walkthrough of Rhysida ransomware, we will pick up from where we left off from part 1 (analysis and debugging), and we will explore how to decrypt the ransomware.
Encryption process
First of all, there are the preliminary initialization phases in which the random keys for file encryption are prepared.
.text:0000000000419B69 48 8D 05 94 62 05 00 lea rax, PRNG_IDX
.text:0000000000419B70 48 89 C2 mov rdx, rax ; n
.text:0000000000419B73 48 8D 05 E6 62 05 00 lea rax, prng
.text:0000000000419B7A 48 89 C1 mov rcx, rax ; prng_val
.text:0000000000419B7D E8 D1 FA FF FF call init_prng
[...]
.text:0000000000419C0E 8B 05 74 26 03 00 mov eax, cs:__PUB_DER_LEN
.text:0000000000419C14 48 8D 15 45 A7 05 00 lea rdx, key
.text:0000000000419C1B 49 89 D0 mov r8, rdx
.text:0000000000419C1E 89 C2 mov edx, eax
.text:0000000000419C20 48 8D 0D 39 24 03 00 lea rcx, __PUB_DER
.text:0000000000419C27 E8 A4 9F 00 00 call rsa_import
[...]
.text:0000000000419C41 48 8B 0D 18 9A 04 00 mov rcx, cs:_refptr_aes_enc_desc
.text:0000000000419C48 E8 43 5F 00 00 call register_cipher
[...]
.text:0000000000419C7D 48 8D 0D ED DB 03 00 lea rcx, aAes ; "aes"
.text:0000000000419C84 E8 37 5C 00 00 call find_cipher
[...]
.text:0000000000419CB3 48 8B 0D C6 99 04 00 mov rcx, cs:_refptr_chc_desc
.text:0000000000419CBA E8 A1 61 00 00 call register_hash
[...]
.text:0000000000419CEF 48 8D 05 0A 61 05 00 lea rax, CIPHER
.text:0000000000419CF6 8B 00 mov eax, [rax]
.text:0000000000419CF8 89 C1 mov ecx, eax
.text:0000000000419CFA E8 81 46 00 00 call chc_register
[...]
.text:0000000000419D2F 48 8D 0D 93 DB 03 00 lea rcx, aChcHash ; "chc_hash"
.text:0000000000419D36 E8 D5 5C 00 00 call find_hash
[...]
The two components of the key (initialization vector and secret) are prepared...
[...]
.text:0000000000417FA1 48 8D 05 20 C4 05 00 lea rax, prngs
.text:0000000000417FA8 48 8B 10 mov rdx, [rax]
.text:0000000000417FAB 8B 85 B0 32 10 00 mov eax, [rbp+103290h+thread_n]
.text:0000000000417FB1 48 98 cdqe
.text:0000000000417FB3 48 69 C0 F0 44 00 00 imul rax, 44F0h
.text:0000000000417FBA 48 01 C2 add rdx, rax
.text:0000000000417FBD 48 8D 85 D0 31 10 00 lea rax, [rbp+103290h+cipher_key]
.text:0000000000417FC4 49 89 D0 mov r8, rdx
.text:0000000000417FC7 BA 20 00 00 00 mov edx, 20h ; ' '
.text:0000000000417FCC 48 89 C1 mov rcx, rax
.text:0000000000417FCF E8 0C C9 00 00 call chacha20_prng_read
.text:0000000000417FD4 48 8D 05 ED C3 05 00 lea rax, prngs
.text:0000000000417FDB 48 8B 10 mov rdx, [rax]
.text:0000000000417FDE 8B 85 B0 32 10 00 mov eax, [rbp+103290h+thread_n]
.text:0000000000417FE4 48 98 cdqe
.text:0000000000417FE6 48 69 C0 F0 44 00 00 imul rax, 44F0h
.text:0000000000417FED 48 01 C2 add rdx, rax
.text:0000000000417FF0 48 8D 85 C0 31 10 00 lea rax, [rbp+103290h+cipher_IV]
.text:0000000000417FF7 49 89 D0 mov r8, rdx
.text:0000000000417FFA BA 10 00 00 00 mov edx, 10h
.text:0000000000417FFF 48 89 C1 mov rcx, rax
.text:0000000000418002 E8 D9 C8 00 00 call chacha20_prng_read
[...]
During file encryption, both of the keys are generated. The specific encrypted thread also uses them. Here is the first one, but it is the same for the second one:
[...]
.text:00000000004180E3 48 8D 05 66 C2 05 00 lea rax, HASH_IDX
.text:00000000004180EA 44 8B 00 mov r8d, [rax]
.text:00000000004180ED 48 8D 05 10 7D 05 00 lea rax, PRNG_IDX
.text:00000000004180F4 8B 08 mov ecx, [rax]
.text:00000000004180F6 48 8D 05 CB C2 05 00 lea rax, prngs
.text:00000000004180FD 48 8B 10 mov rdx, [rax]
.text:0000000000418100 8B 85 B0 32 10 00 mov eax, [rbp+103290h+thread_n]
.text:0000000000418106 48 98 cdqe
.text:0000000000418108 48 69 C0 F0 44 00 00 imul rax, 44F0h
.text:000000000041810F 4C 8D 1C 02 lea r11, [rdx+rax]
.text:0000000000418113 BE 0B 00 00 00 mov esi, 0Bh
.text:0000000000418118 4C 8D 8D FC 1F 10 00 lea r9, [rbp+103290h+cipher_key_out_length]
.text:000000000041811F 4C 8D 55 E0 lea r10, [rbp+103290h+cipher_key_out]
.text:0000000000418123 8B 95 20 32 10 00 mov edx, [rbp+103290h+cipher_key_length] ; Size
.text:0000000000418129 48 8D 85 D0 31 10 00 lea rax, [rbp+103290h+cipher_key]
.text:0000000000418130 48 8D 1D 29 C2 05 00 lea rbx, key
.text:0000000000418137 48 89 5C 24 50 mov [rsp+103310h+var_1032C0], rbx ; __int64
.text:000000000041813C C7 44 24 48 02 00 00 00 mov [rsp+103310h+var_1032C8], 2 ; int
.text:0000000000418144 44 89 44 24 40 mov [rsp+103310h+var_1032D0], r8d ; int
.text:0000000000418149 89 4C 24 38 mov [rsp+103310h+var_1032D8], ecx ; int
.text:000000000041814D 4C 89 5C 24 30 mov [rsp+103310h+var_1032E0], r11 ; __int64
.text:0000000000418152 89 74 24 28 mov [rsp+103310h+var_1032E8], esi ; int
.text:0000000000418156 48 8D 0D 83 E3 03 00 lea rcx, PROGRAM_NAME
.text:000000000041815D 48 89 4C 24 20 mov [rsp+103310h+var_1032F0], rcx ; __int64
.text:0000000000418162 4D 89 D0 mov r8, r10 ; __int64
.text:0000000000418165 48 89 C1 mov rcx, rax ; Src
.text:0000000000418168 E8 73 B3 00 00 call rsa_encrypt_key_ex
[...]
They're also written (encrypted) with the relative dimensions within the same file to be encrypted.
[...]
.text:00000000004181AF 48 8B 85 28 32 10 00 mov rax, [rbp+103290h+f]
.text:00000000004181B6 41 B8 02 00 00 00 mov r8d, 2 ; whence
.text:00000000004181BC BA 00 00 00 00 mov edx, 0 ; offset
.text:00000000004181C1 48 89 C1 mov rcx, rax ; stream
.text:00000000004181C4 E8 37 1E 03 00 call fseeko64
.text:00000000004181C9 8B 85 FC 1F 10 00 mov eax, [rbp+103290h+cipher_key_out_length]
.text:00000000004181CF 89 C1 mov ecx, eax
.text:00000000004181D1 48 8B 95 28 32 10 00 mov rdx, [rbp+103290h+f]
.text:00000000004181D8 48 8D 45 E0 lea rax, [rbp+103290h+cipher_key_out]
.text:00000000004181DC 49 89 D1 mov r9, rdx ; Stream
.text:00000000004181DF 41 B8 01 00 00 00 mov r8d, 1 ; ElementCount
.text:00000000004181E5 48 89 CA mov rdx, rcx ; ElementSize
.text:00000000004181E8 48 89 C1 mov rcx, rax ; Buffer
.text:00000000004181EB E8 00 31 03 00 call fwrite
[...]
.text:000000000041820C 48 8B 95 28 32 10 00 mov rdx, [rbp+103290h+f]
.text:0000000000418213 48 8D 85 FC 1F 10 00 lea rax, [rbp+103290h+cipher_key_out_length]
.text:000000000041821A 49 89 D1 mov r9, rdx ; Stream
.text:000000000041821D 41 B8 01 00 00 00 mov r8d, 1 ; ElementCount
.text:0000000000418223 BA 04 00 00 00 mov edx, 4 ; ElementSize
.text:0000000000418228 48 89 C1 mov rcx, rax ; Buffer
.text:000000000041822B E8 C0 30 03 00 call fwrite
[...]
Following this, the process branches based on a variable that can take two values (1 or 2), which are assigned during the decryption phase at the end of the file.
The first branch is relatively straightforward: the file is divided into blocks of approximately 260 KB, with a maximum block size of 1 MB. Any remaining part of the file retains its original data. These blocks are then encrypted with previously generated keys replacing the original blocks.
The second branch also seems to involve processing the blocks, though we will not delve into that for now, as discussing one of the branches is sufficient. We won't cover all aspects of the malware, but we'll focus solely on how to recover damaged files.
The flaw in the decryption system
So, the encryption process can be represented schematically as in the screenshot below.
The IV and keys are randomly generated for each processor (on which a decryption thread is launched) and used to encrypt the blocks of the file with an AES algorithm, which are in turn encrypted with an RSA algorithm using a public key embedded in the code and rewrote the file with the encrypted information.
For those unfamiliar with decryption algorithms, RSA requires two keys to encrypt and decrypt: a public key and a private key. The encryption operation can be performed with either key, but it is essential that the reverse operation is performed with the other key. On the other hand, AES is reversible through the use of the two keys: a secret (or encryption) key and an initialization vector (IV) with which it was previously encrypted.
This, instead, should be the reverse process diagram for restoring encrypted files.
Then, recover the secret and the IV to be decrypted using the RSA Private Key, then use them to decrypt the blocks of the file; the problem is the lack of the Private Key.
Let's then move on to understand the "vulnerability" that allowed researchers to recover the two keys necessary to restore the files without using the Private Key of the Rhysida group.
In reality, the vulnerability at the basis of the Rhysida malware flaw is much more banal and profound than what is believed and, honestly, one should not even talk about a "flaw" or "vulnerability" (in my humble opinion obviously), as it is a natural behaviour of most programming languages, that is, the "generation of random numbers". In reality, the numbers are not generated in a truly random way. To somehow give an appearance of randomness, the initialization of the generator has been introduced through an initial numerical value in almost all programming languages, which, to make it as random as possible, is almost always derived from the tick count of the time in which it is initialized (the C# DateTime.Now tick to give a practical example). Using the tick time gives the appearance of a random number, but, in reality, it isn’t truly random at all.
To understand better, let's take an example in C++. Below is the creation, from Best Practice, of a random number generator.
std::srand(static_cast<unsigned>(std::time(nullptr)));
std::cout << std::rand() << "\r\n";
This code generates an ever-changing sequence of “random” numbers.
std::srand(static_cast<unsigned>(std::time(nullptr)));
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
Even by reinitializing the randomizer within the same code, the sequence of numbers will always be different.
std::cout << "first randomizer:\r\n";
std::srand(static_cast<unsigned>(std::time(nullptr)));
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "second randomizer:\r\n";
std::srand(static_cast<unsigned>(std::time(nullptr)));
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
The output will be as follows:
first randomizer:
9170
17183
17286
11035
28401
21725
27459
second randomizer:
9187
5389
8302
280
14438
8591
10253
However, by fixing the initialization value of the randomizer, each execution will always repeat the same sequence of numbers, even after days, months or years.
unsigned initializer = static_cast<unsigned>(std::time(nullptr));
std::cout << "first randomizer:\r\n";
std::srand(initializer);
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::this_thread::sleep_for(std::chrono::seconds(5));
std::cout << "second randomizer:\r\n";
std::srand(initializer);
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
std::cout << std::rand() << "\r\n";
The related output will be:
first randomizer:
9856
13357
442
18059
31773
27138
25714
second randomizer:
9856
13357
442
18059
31773
27138
25714
Based on these simple concepts, and remembering that the randomizer of the encryption keys was created based on the time of the machine affected by the malware, one could think of reconstructing the two keys by passing the same value at the time of encryption.
So the process we are trying to apply without using the private key is as follows.
Decryptor implementation
To create the decrypted, the same VM will be used. Install a version of Visual Studio which will compile the necessary libraries and project. However, we need a clean VM. First, create a snapshot of the VM in its current state. Next, copy the folder with the encrypted documents (which will be used to verify that the decrypter works). Finally, restore the clean image, before the infection.
Once Visual Studio is installed, we will be ready (for this phase, I chose the Visual Studio 2022 Enterprise Edition, but even a simple Community should be fine). As previously established, the open source library "libtomcrypt" is used for encryption, the first step to develop a tool that restores files, downloads and compiles the library.
The repository:
Inside the README of the repository, it is clear that there is a need for another library, also from the LibTom project, namely the LibTomMath, whose repository is the following:
Compile in sequence first the "math" (the VS will update the PATHs for the reference to the library and insert it among the shared ones) and then the "crypt."
For the compilation of the libraries, although the compilation may be successful, during development, some problems emerged which forced me to modify the project settings, I report them here for simplicity. The answer can be found in the comments information of the source code file github.com/libtom/libtommath/blob/develop/s_mp_rand_platform.c. When compiling libtommath, the optimization option cannot be set to Project Configuration->C/C++->Optimization. "/Od".
But changing optimization options involves changing two other settings:
C/C++->General->Debug information format, "/ZI" cannot be used, change it to "/Zi".
C/C++->Code Generation->Basic Runtime Control, "/RTC1" cannot be used, change this to "default" too.
If everything goes as it should, the libraries will be available and ready to use in the new project that will be created.
For simplicity, since this article is for educational purposes only, I will create a console application (obviously a more user friendly interface would be more appropriate, but that is not the purpose of this article) in the same solution as the "libTomCrypt" library.
Reference the library in the new project.
Add headers between additional directories.
Next, prepare a new file in which to restore the contents of the original, encrypted file.
void processFileEnc(char* file_name)
{
std::ifstream fileInput(file_name, std::ios::binary);
if (!fileInput.is_open()) {
std::cerr << "Cannot open file: " << file_name << std::endl;
return;
}
int filenameoriginallenght = strlen(file_name) - strlen(".rhysida");
char* originalFileName = (char*)malloc(sizeof(char) * filenameoriginallenght);
std::copy(file_name, file_name + filenameoriginallenght, originalFileName);
originalFileName[filenameoriginallenght] = '\0';
std::ofstream fileOutput(originalFileName, std::ios::binary);
if (!fileOutput.is_open()) {
std::cerr << "Cannot create file: " << originalFileName << std::endl;
return;
}
[...]
fileInput.close();
fileOutput.close();
}
The first thing to do is initialize the randomizer using the time of the encrypted file.
[...]
struct stat fileInfo;
if (stat(file_name, &fileInfo) != 0) {
std::cerr << "Cannot read file info." << std::endl;
return;
}
std::srand(static_cast<unsigned>(fileInfo.st_mtime));
[...]
Since Ghidra does its job very well, i.e., decompiling, we will use it for more user-friendly reading of the code, which, will be shown to us in part already in C++ language, with the variables and parameters already passed correctly.
Obviously, there will be some registers and variables to adjust that the decompiler won't be able to reconvert, but the bulk of the work is already done.
To start, recover some of the code from the encryption engine initialization functions.
I can already hear the cries of readers who are wondering why they didn't immediately read the decompiled code, which is certainly easier to understand. Well, the dynamic analysis in a malware analysis serves to better understand the behavior and interactions of the malware during execution, in order to provide an in-depth view of the ransomware in action. Dynamic analysis through binary disassembly and debugging provides a deeper perspective of malware behavior than simply understanding the decompiled code. While decompiled code may be more readable, dynamic exploration allows you to grasp crucial details about the malware's interactions during execution, revealing malicious actions and providing a more complete view of its capabilities. And anyway, do we want to take away the pleasure of a pure assembler code debugging session?
Perfect. Some steps need to be simplified and adapted to our code. In the end, you will get something like this.
[...]
int result;
int* in_RDX = new int;
unsigned char prng_entr[0x28];
prng_state* in_RCX = new prng_state();
int iVar1 = register_prng(&chacha20_prng_desc);
*in_RDX = iVar1;
if ((result = chacha20_prng_start(in_RCX)) != CRYPT_OK)
{
std::cerr << "ERROR starting prng: " << error_to_string(result) << std::endl;;
return;
}
if ((result = chacha20_prng_ready(in_RCX)) != CRYPT_OK)
{
std::cerr << "ERROR checking readyness of prng: " << error_to_string(result) << std::endl;;
return;
}
for (int i = 0; i < 0x28; i = i + 1) {
prng_entr[i] = ((char)i + (char)*in_RDX + '\x01') * (char)(rand() % 0x100);
}
if ((result = chacha20_prng_add_entropy(prng_entr, 0x28, in_RCX)) != CRYPT_OK)
{
std::cerr << "ERROR adding entropy prng: " << error_to_string(result) << std::endl;;
return;
}
unsigned char* _Memory = (unsigned char*)malloc(rand() % 0x100 + 1);
chacha20_prng_read(_Memory, 8, in_RCX);
[...]
Before proceeding with the decryption, you need to know the length of the file to be decrypted.
I remember that at the moment, just to demonstrate that it is possible, we are using a file that has been encrypted in its entirety (its size does not exceed one mega) and the encryption process used was the simplest one, so I'm not going to too many problems and I will proceed towards the resolution of this specific flow, also because the complete decryptor has already been released by much smarter people than me.
Define a couple of functions that will serve to recover the dimensions of the additional sections at the bottom of the file and then subtract them from the total size of the file.
unsigned char* readDimFromFile(std::istream& fileInput, int offset) {
fileInput.seekg(-4 - offset, std::ios::end);
unsigned char buffer[4];
fileInput.read(reinterpret_cast<char*>(buffer), 4);
return buffer;
}
unsigned char* readFile(std::istream& fileInput, int dimension, int offset) {
fileInput.seekg(0 - dimension - offset, std::ios::end);
unsigned char *buffer = (unsigned char*)malloc(sizeof(unsigned char)*dimension);
fileInput.read(reinterpret_cast<char*>(buffer), dimension);
return buffer;
}
The first reads the dimensions set at four bytes, moving by the relative offset starting from the end of the file, while the second recovers the number of bytes returned by the first function, always moving by the relative offset. To be used as follows.
[...]
int offset = 0;
unsigned char* bufferRead = readDimFromFile(fileInput, offset);
int curType_n = 0;
std::memcpy(&curType_n, bufferRead, sizeof(curType_n));
offset += 4;
bufferRead = readDimFromFile(fileInput, offset);
int numBytesToRead = 0;
std::memcpy(&numBytesToRead, bufferRead, sizeof(numBytesToRead));
offset += 4;
unsigned char* firstBufferRead = readFile(fileInput, numBytesToRead, offset);
offset += numBytesToRead;
bufferRead = readDimFromFile(fileInput, offset);
numBytesToRead = 0;
std::memcpy(&numBytesToRead, bufferRead, sizeof(numBytesToRead));
offset += 4;
unsigned char* secondBufferRead = readFile(fileInput, numBytesToRead, offset);
offset += numBytesToRead;
offset = static_cast<long long>(static_cast<std::streampos>(fileInfo.st_size)) - offset;
[...]
It's time to move on to the heart of the decryptor. Recover the text from the file...
[...]
fileInput.seekg(0, std::ios::beg);
unsigned char* bufferIn = (unsigned char*)malloc(sizeof(unsigned char) * offset);
fileInput.read(reinterpret_cast<char*>(bufferIn), offset);
unsigned char* bufferOut = (unsigned char*)malloc(sizeof(unsigned char) * offset);
[...]
...and let's try to restore it.
[...]
if ((result = register_all_ciphers()) != CRYPT_OK)
{
std::cerr << "ERROR registering all ciphers: " << error_to_string(result) << std::endl;
return;
}
if ((result = register_all_hashes()) != CRYPT_OK)
{
std::cerr << "ERROR registering all hashes: " << error_to_string(result) << std::endl;
return;
}
int CIPHER = find_cipher("aes");
int HASH_IDX = find_hash("chc_hash");
unsigned char* cipher_IV = (unsigned char*)malloc(sizeof(unsigned char) * 0x20);
unsigned char* local_e0 = (unsigned char*)malloc(sizeof(unsigned char) * 0x10);
chacha20_prng_read(cipher_IV, 0x20, in_RCX);
chacha20_prng_read(local_e0, 0x10, in_RCX);
symmetric_CTR cipher_IV_out_length;
if ((result = ctr_start(CIPHER, local_e0, cipher_IV, 0x20, 0xe, 0, &cipher_IV_out_length)) != CRYPT_OK)
{
std::cerr << "ERROR starting the cipher: " << error_to_string(result) << std::endl;
return;
}
if ((result = ctr_setiv(local_e0, 0x10, &cipher_IV_out_length)) != CRYPT_OK)
{
std::cerr << "ERROR setting the IV: " << error_to_string(result) << std::endl;
return;
}
if ((result = ctr_decrypt(bufferIn, bufferOut, offset, &cipher_IV_out_length)) != CRYPT_OK)
{
std::cerr << "ERROR decrypting the file content: " << error_to_string(result) << std::endl;
return;
}
if (std::strstr(reinterpret_cast<const char*>(bufferOut), "Ripristinata") != nullptr) {
std::cout << std::endl << std::endl << bufferOut << std::endl;
break;
}
std::cout << ".";
[...]
Now, before proceeding, some considerations must be made.
First of all, the time of the file that is relied on to initialize the randomizer may not be correct since from the moment in which the random function is initialized to when the file is encrypted, a certain amount of time has passed, and although the malware is relatively quick to execute, the number of iterations and files it processes is quite high. Once you have identified the date and time of modification of the file, you will have to proceed backwards by trial and error to find the correct time at which the random number was initialized.
The second point is the cycles of random generations that are executed. In the debug session in the previous article, three different initializations are performed (init_prng); the first is ignored, the other two are linked to the number of processors available in the computer (in the case of the VM, two) and assigned to the relevant processor.
Therefore, when adapting the code, you will have to carry out two nested cycles, the first on the date and time of the file you are trying to restore (after you have found the exact date, it will also work for all the other files) and a more internal one which will generate three pairs of keys to be used for decryption (I could have excluded the first one as already mentioned, but forgive me for the imprecision and lack of optimization, as I was saying, this article is for demonstration purposes only that it is possible to restore encrypted files with the Rhysida malware).
Something is still missing; the decrypting function (ctr_decrypt) will always return CRYPT_OK, even if the key and initialization vector (IV) are not the correct ones to use. So as not to increase the complexity of the program, a small trick will be used, but the correct procedure to complete the decryptor will still be described (which will not be done in this article). The trick is to focus on a single encrypted file, in particular, a text file containing an excerpt and some reflections from the poem "The Divine Comedy" by Dante Alighieri. Knowing part of its content, we can verify that the word "Alighieri" is contained within the decrypted text to confirm a successful decryption. The correct process, however, would be to encrypt the two secrets (key and IV) with the RSA algorithm through the public key inside the malware and compare them with those contained inside the damaged file (remember that have been inserted at the end of the file with their respective dimensions).
Having said that, let's enrich the code with suitable log messages. Finally, it is possible to try to decrypt the text file, which is the subject of this tutorial.
0:Sat Mar 9 21:37:15 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
1:Sat Mar 9 21:37:14 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
2:Sat Mar 9 21:37:13 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
3:Sat Mar 9 21:37:12 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
4:Sat Mar 9 21:37:11 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
5:Sat Mar 9 21:37:10 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
6:Sat Mar 9 21:37:09 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
7:Sat Mar 9 21:37:08 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
8:Sat Mar 9 21:37:07 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
9:Sat Mar 9 21:37:06 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
10:Sat Mar 9 21:37:05 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
11:Sat Mar 9 21:37:04 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
12:Sat Mar 9 21:37:03 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
13:Sat Mar 9 21:37:02 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
14:Sat Mar 9 21:37:01 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
15:Sat Mar 9 21:37:00 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
16:Sat Mar 9 21:36:59 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
17:Sat Mar 9 21:36:58 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
18:Sat Mar 9 21:36:57 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
19:Sat Mar 9 21:36:56 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
20:Sat Mar 9 21:36:55 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
21:Sat Mar 9 21:36:54 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
22:Sat Mar 9 21:36:53 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
23:Sat Mar 9 21:36:52 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
24:Sat Mar 9 21:36:51 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
25:Sat Mar 9 21:36:50 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
26:Sat Mar 9 21:36:49 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
27:Sat Mar 9 21:36:48 2024 - try rand #0 - try rand #1 - try rand #2 - ..... number of time rand function called = 123
28:Sat Mar 9 21:36:47 2024 - try rand #0 - Data decrypted: i=28, j=1
C:\temp\libtomcrypt\x64\Debug\Rhysida_Decryptor.exe (process 14032) exited with code 0.
Conclusion
Well, before proceeding to conclusions, there are some other considerations needed. First, let's also take a look at the official decryptor made available to unfortunate victims of the Rhysida malware.
As often happens in these cases, the developers decline any responsibility for the possible damage that the use of the software could cause. As always, they recommend a backup of the files before using the decryptor and do not guarantee the effectiveness of the tool on all files encrypted by the malware. Well, after starting the tool, the result was what I expected, that is, most of the encrypted files were not restored successfully.
Why did I say I expected it? Because even in my case, the code I wrote didn't always work and the number of files it manages to decrypt is very low. However, this highlights that although the original code can be retraced and the process reversed, some aspects can escape the careful analysis carried out and human intervention often remains inevitable.
As said and repeated several times in this article, the decryptor is not complete, some aspects and some flows were deliberately left out, as it was not the objective of this article to reconstruct the Rhysida decryption tool (already available at the links at the bottom), rather demonstrate that it is possible to recover lost data.
The "flaw" (or "vulnerability") is the normal behaviour of the random number generation algorithm used within all computer systems.
The ransomware itself does appear to be too complex, avoiding hiding inside background processes (a shell in plain sight opens as soon as it is launched) or starting services that restart when the OS starts like its larger predecessors (see WannaCry). The encryption process appears to have been done in a rush to speed up the propagation phase. In short, yes, a threat that, at the moment, seems to have been slowed down, but which could soon return in a perfected and more insidious version.
Useful links
Follow some useful links used to write this article.
- Link to the repository of the source code described in this article:
- Link to the libtommath and libtomcrypt used in the project:
- Link to the official decryptor:
https://seed.kisa.or.kr/kisa/Board/166/detailView.do
- Link to a document that explained the method used to decrypt the data infected from Rhysida ransomware
https://regmedia.co.uk/2024/02/12/method_for_decrypting_data_infected_with_rhysida_ransomware.pdf
- Additional links that explain the recover process of the files encrypted: