Malware Analysis: Ragnarok Ransomware

Back to Posts

Malware Analysis: Ragnarok Ransomware

Reading Time: 11 minutes
The analysed sample is a malware employed by the Threat Actor known as Ragnarok. The ransomware is responsible for files’ encryption and it is typically executed, by the actors themselves, on the compromised machines. The name of the analysed executable is xs_high.exe, but others have been found used by the same ransomware family (such as xs_normal.exe and xs_remote.exe). The configuration within the malware contains information regarding the encryption activities, from whitelisted countries to the contents of the ransom note. It is interesting to note that the same configuration has been used across different victims, suggesting that the actor’s activities are not specifically tailored to them. The ransomware is a DLL named cry_demo.dll, which is packed inside the xs_high.exe executable. The unpacking process takes place with self-injection (also known as PE-overwrite), an injection technique that does not involve spawning new processes or threads. All the packed DLL code is contained in the .data section of the xs_high.exe executable and will get unpacked and decrypted by a custom loader before being executed.

IOC

Filename: xs_high.exe Hash:
  • MD5: A0FC3C769DE5CF550DFFB49FF6521A81
  • SHA1: 3B7CA963CB2FC5ABC9F09AA075E51A5782BCFDE4
  • SHA256: 360FFABF106C665CA7332F84BA158335FAA487E3B47C675FD2715FC7A37DB6A7
Network indicators:
  • IP: 64.32.25.202
  • Port: 8081

Analysis

Configuration Extraction

The analysed sample (xs_high.exe) contains some obfuscated and encrypted code in its .data section. It copies such data in memory and, once decrypted, transfers the execution flow to it. The encrypted data is a DLL (cry_demo.dll) that contains, among other things, the ransomware’s configuration. Once opened in PE-Bear, the size of the sample’s .data section stands out.
PE-Bear view of the ransomware xs_high.exe showing a big .data section
The way the sample performs self-injection is through the following steps:
  1. Loading DLL’s obfuscated code from the .data section into memory with HeapAlloc().
  2. Deobfuscating parts of the code with a bitwise NOT operation on the memory region.
  3. Loading more encrypted code into memory with VirtualAlloc().
  4. Changing the protection of the committed section, enabling execution, with VirtualProtect().
  5. Transferring the execution flow to the newly allocated region of memory (the now fully unpacked DLL).
Firstly, a memory area of around 93k bytes (16E00h) is allocated using the HeapAlloc() API and is going to be filled with parts of the .data section of the packer’s executable. This can be seen in the following x64dbg screenshot, showing the rep movsd instruction (in red) being executed ecx times (green).
Data being copied from the .data section (ESI) into the memory allocated with HeapAlloc (EDI)
The data copied into memory (orange) is being read from the .data section pointed by the address stored in the ESI register.
The ransomware mapped into memory, highlighted is the section containing the data being copied
A quick look at that section does in fact reveal the same data being written.
View of the .data section, showing the data being copied into memory
By the MZ header, one can already guess that this is a PE being written. The packed DLL (the actual ransomware) is now being copied into memory to perform some deobfuscation on it. Parts of the DLL does not need deobfuscation, like the next copied section. A good indicator is the first bytes (55 8B EC) that represent the opcodes of a prologue, correctly disassembled by x64dbg, as can be seen in the below picture.
Assembly code being copied into the memory allocated with HeapAlloc()
Newly copied data correctly disassembled by x64dbg
Finally, packer’s code copies some other parts from the .data section, which get deobfuscated by a routine that performs a bitwise NOT operation on every byte:
Bitwise NOT routine (red) deobfuscates the data (green)
After the deobfuscation:
Deobfuscated data after the bitwise NOT
The ransomware’s packer then performs some allocations of memory using the VirtualAlloc() API. Following is a list of subsequent calls to this API:
Trace of the memory allocations performed by the ransomware with VirtualAlloc()
The memory areas allocated are contiguous: every time the VirtualAlloc() API is called, a different piece of the memory previously allocated with HeapAlloc() and now deobfuscated is copied, effectively ending up copying it all. The part containing the configuration is copied by the fifth VirtualAlloc() API call (6k bytes).
Encrypted configuration being copied after the bitwise NOT deobfuscation
After the data has been written into these newly allocated memory regions, different protections attributes are changed using VirtualProtect(). In particular, the area containing the .text section of the DLL is rendered executable, passing the address (in this case 0x10001000) and the PAGE_EXECUTE_READ flag (0x20) as parameters to VirtualProtect() API.
Execution protection is given to the .text section of the DLL
At this stage of the injection, the unpacked ransomware DLL is present in memory in its mapped format. This means that the physical and virtual addresses of the PE are not aligned, and must be re-based: if we dump it, analysis tools are not going to recognise its structure due to this misalignment, and they will not be able to detect a PE file.
Dumped PE with misaligned addresses
So, for the DLL to be a valid PE file, we need to manually resize the Raw Addresses and adjust the sizes accordingly. Once that is done, PE-Bear successfully parses it and detects it as a DLL named cry_demo.dll with one exported function (start). It was compiled on the 28th of January, 2021 at 05:52:19 UTC.
Adjusted PE showing the DLL’s name and exports
The DLL does not have a DllMain function. The custom loader directly passes the address of the only exported function (start) to a call function via the eax register (xs_high.exe+1A83). This is a code obfuscation technique known as “Opaque Predicates”, which prevents the predicates from being evaluated during static analysis, thus requiring dynamic analysis.
CALL to eax, which holds the "entry point" of the DLL
The DLL “entry point” can be found at 0x10004630 while the call to the configuration decryption routine is at 0x1000463D - call 0x10002F20.
The exported function address, which is passed to the call
The section containing the ransomware’s configuration (0x10017000) is given the PAGE_READWRITE permission (0x04): the reason for this is that the configuration is decrypted, later on, with a custom algorithm that can be found in the DLL itself. Here below the decryption routine used to decrypt its configuration:
Decryption algorithm as can be seen in IDA
Without diving too far into how we reverse-engineered the decryption routine, here the generic methodology we used:
  1. General looking at the ASM representation of the decryption routine at runtime.
  2. Determining the function calling conventions, the parameters being passed to the functions and the returning values.
  3. Deriving generic function’s functionality based on the previous step (looking into the actual function to confirm the thesis).
  4. Commenting ASM representation with high-level information.
Once reaching juicy routines and loops, passing the selected instructions forming the routine to the snowman disassembler in order to gain some insight on the routine itself: Begin replacing variable names with meaningful ones, getting usages and memory location content references from the malware itself at runtime:
void fun_10002ffe() {
    k = 0;
    w = 0;
    i = 0;
    do {
        asm("cdq");
        y = (w + 1) % 0x7e; // counter
        *(int32_t*)(ebp5 - 4) = y; // memory region
        tmp1 = *reinterpret_cast<uint8_t*>(decryption_key + y - 0x104);
        tmp2 = tmp1;
        asm("cdq");
        k = (uint32_t)((int32_t)(tmp2 + k) % 0x7e);
        tmp3 = *reinterpret_cast<int32_t*>(ebp10 - 4); // memory region
        tmp4 = *reinterpret_cast<uint8_t*>(decryption_key + k - 0x104); 
        *(int8_t*)(decryption_key + tmp3 - 0x104) = *reinterpret_cast<int8_t*>(&tmp4);
        *(uint8_t*)(ebp14 + k - 0x104) = tmp1; //memory region2?
        tmp5 = *reinterpret_cast<uint8_t*>(enc_config + (*reinterpret_cast<uint8_t*>(enc_config + tmp3 - 0x104) + tmp2) % 0x7e - 0x104);
        *(uint8_t*)(i + enc_config) = (uint8_t)(*reinterpret_cast<uint8_t*>(i + enc_config) ^ *reinterpret_cast<uint8_t*>(&tmp5));
        ++i;
        w = *reinterpret_cast<int32_t*>(ebp18 - 4); // memory region
    } while (i < 0x1970);
}
Transferring the newly acquired knowledge to the more compact and better disassembled IDA version while simultaneously porting the code to python:
x = 0
k = 0
w = 0

while w < 6512:
    tmp1 = (k + 1) % 126;
    tmp2 = decryption_key[tmp1];
    x = (tmp2 + x) % 126;
    decryption_key[tmp1] = decryption_key[x];
    decryption_key[x] = tmp2;
    enc_config[w] ^= decryption_key[(tmp2 + decryption_key[(k + 1) % 126]) % 126];
    k = tmp1;
    w+=1
Which results in the configuration being decrypted.
Decryption of the configuration
With all the acquired knowledge we ported all the decryption routine to a python script allowing us to programmatically decrypt multiple sample’s configurations directly from encrypted memory dumps. As always, the code shown below can also be found on my GitHub repository as well as the x64dbg and IDA database used for this analysis.
"""
Title:              Ragnarok Ransomware Configuration Decrypter
Author:             Paolo Stagno aka VoidSec - voidsec@voidsec.com - https://voidsec.com
Date:               23/04/2021
IOC:                
"""
#!/usr/bin/python
import argparse
parser = argparse.ArgumentParser(prog="RagnarokConfigDecrypt.py",
                                description="Decrypt the encrypted configuration of the Ragnarok Ransomware")
parser.add_argument("-k", "--key", default="comehereplz", dest="key", help="Key")
parser.add_argument("-c", "--config", required=True, type=argparse.FileType('r'), dest="config", 
                                help="Dumped Encrypted Configuration File (usually start at 0x10017000)")
args = parser.parse_args()

# retrieve encrypted configuration file
enc_config_file = args.config

print("[-] Configuration Key: {}".format(args.key))
key = []
# split the key string char by char into a list
key[:] = args.key

print("[-] Generating in-memory structures")
charset = [0] * 256
big_key = [0] * 256
decryption_key = [0] * 256

i = 0
while i < 256:
    charset[i] = i
    big_key[i] = key[i % len(key)]
    i += 1

print("[-] Generating decryption key")
i = 0
z = 0
while z < 256:
    tmp1 = charset[z]
    i = (i + ord(big_key[z]) + tmp1) % 126
    charset[z] = charset[i]
    charset[i] = tmp1
    z += 1
decryption_key = charset
decryption_key_hex = ""
for byte in decryption_key:
    decryption_key_hex += "{0:0{1}X} ".format((byte), 2)
# "{0:0{1}x}".format(1,2) fill single byte with leading 0
# "{0:#0{1}x}".format(1,4) -> '0x01

print("[+] Decryption Key:")
print("----------------------------")
print(decryption_key_hex)
print("----------------------------")

f = open("decryption_key.bin", "w")
f.write(decryption_key_hex)
f.close()
print("[-] saved in: decryption_key.bin\n")

print("[-] Reading Encrypted Configuration File")
enc_config = []
tmp = enc_config_file.read().lstrip().rstrip()

for byte in tmp.split(" "):
    enc_config.append(int(byte, 16))
enc_config_file.close()

print("[-] Decrypting Ransomware Configuration")
x = 0
k = 0
w = 0
while w < 6512:
    tmp1 = (k + 1) % 126
    tmp2 = decryption_key[tmp1]
    x = (tmp2 + x) % 126
    decryption_key[tmp1] = decryption_key[x]
    decryption_key[x] = tmp2
    enc_config[w] ^= decryption_key[(
        tmp2 + decryption_key[(k + 1) % 126]) % 126]
    k = tmp1
    w += 1

# convert back from int to ASCII char
enc_config = [chr(byte) for byte in enc_config]
dec_config = ""
# join item in list forming a string
dec_config = dec_config.join(enc_config)

print("[+] Configuration:")
print("----------------------------")
print(dec_config)
print("----------------------------")

f = open("config.json", "w")
f.write(dec_config)
f.close()
print("[-] saved in: config.json")
# =^.^=
 

Configuration's Content

Once dumped, the configuration appears as a JSON object, containing different fields:
Decrypted configuration in JSON format, data between square brackets have been removed
The configuration allows us to gain some insight into the Threat Actor, as well as the ransomware mechanisms. The malware dynamically imports APIs listed in cleartext in the DLL using the LoadLibraryA API as shown below:
Dynamic import of some APIs
APIs functions used by the ransomware, ~60, are contained in the configuration’s API field. One of the first checks the ransomware performs is about the UI language of the target OS: it queries the registry key SYSTEM\CurrentControlSet\Control\Nls\Language\InstallLanguage through RegQueryValueExA API and checks its value against a list of language codes contained in the language configuration’s field. The ransomware aborts its execution if any of the following languages is found:
Language Code (Hex) Language Identifier Language and country
0419 ru-RU Russian - Russia
0804 zh-CN Chinese (Simplified) - China
0480 ug-CN Uyghur - China
0478 ii-CN Yi - China
0451 bo-CN Tibetan - China
042b hy-AM Armenian - Armenia
042c az-Latn-AZ Azeri (Latin) - Azerbaijan
082C az-Cyrl-AZ Azeri (Cyrillic) - Azerbaijan
0423 be-BY Belarusian - Belarus
043f kk-KZ Kazakh - Kazakhstan
0440 ky-KG Kyrgyz - Kyrgyzstan
0819 ru-MO Russian - Moldova
0428 tg-Cyrl-TJ Tajik (Cyrillic) - Tajikistan
0443 uz-Latn-UZ Uzbek (Latin) - Uzbekistan
0442 tk-TM Turkmen - Turkmenistan
0422 uk-UA Ukrainian - Ukraine
040d he-IL Hebrew - Israel
040A es-ES_tradnl Spanish - Spain
Interestingly, besides the usual CIS countries, there are some uncommon ones, such as Israel, Spain and China: this usually implies that the Threat Actor does not want to be hostile against these countries, possibly hinting at their likely geographical origin of its affiliates. Then, the ransomware sends an HTTP request to the IP and port contained in the configuration, ip (64.32.25.202) and port (8081) fields. The request contains the hostname and private IP address of the machine, as well as a parameter named start, that indicates the beginning of the encryption phase:
HTTP request performed at the beginning of the encryption
Then, depending on the machine architecture, the ransomware executes the commands contained in cmd_shadow, cmd_shadow1, cmd_boot, cmd_recovery and cmd_firewall, which results in the shadow copies being deleted, the firewall being disabled and some options being added to the boot’s settings. The ransomware also disables Windows Defender, using the contents of the key and value fields.
Contents of the key and value fields of the configuration
Lastly, the proc field contains some processes that may normally have a lock on some files, resulting in the ransomware not being able to encrypt those locked files. The ransomware simply kills these processes before starting the encryption routine.
List of processes to be terminated by the ransomware
Processes listed in the configuration being terminated as seen from the process tree
Now the ransomware is ready for the encryption phase. The first three fields of the configuration (calc, white and black) contain several file extensions: the main difference between them is that those contained in black are going to be excluded from the encryption. The reason for this is to avoid the compromise of vital OS components, such as executables and DLLs, which, if encrypted, would render the machine unusable. The files are encrypted with RSA 4096 and AES: the RSA modulus and exponent can be found in the configuration (_N and _E respectively). The extension .thor is contained in the ext field and is going to be appended to the encrypted files. An HTML file named !!Read_Me.html (name field) is going to be written in the directories where at least one file has been encrypted; this HTML file contains the ransom note and its contents is stored as base64 encoded data in the configuration’s content field.
Ransom note written on the compromised system
At the end of the encryption phase, an HTTP request is performed containing a parameter named end. Moreover, this last HTTP request also contains a counter that represents the number of encrypted files grouped by extensions. The configuration’s field named calc contains the list of extensions for which to count the encrypted files.
HTTP request performed at the end of the encryption with the encrypted extensions counter

Authors

Written by Elio Biasiotto & Paolo Stagno

Share this post

Back to Posts