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
- 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.
The way the sample performs self-injection is through the following steps:
- Loading DLL’s obfuscated code from the
.data
section into memory withHeapAlloc()
. - Deobfuscating parts of the code with a bitwise
NOT
operation on the memory region. - Loading more encrypted code into memory with
VirtualAlloc()
. - Changing the protection of the committed section, enabling execution, with
VirtualProtect()
. - Transferring the execution flow to the newly allocated region of memory (the now fully unpacked DLL).
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).
The data copied into memory (orange) is being read from the
.data
section pointed by the address stored in the ESI
register.
A quick look at that section does in fact reveal the same data being written.
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.
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:
After the deobfuscation:
The ransomware’s packer then performs some allocations of memory using the
VirtualAlloc()
API. Following is a list of subsequent calls to this API:
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).
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.
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.
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.
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.
The DLL “entry point” can be found at
0x10004630
while the call to the configuration decryption routine is at 0x1000463D - call 0x10002F20
.
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:
Without diving too far into how we reverse-engineered the decryption routine, here the generic methodology we used:
- General looking at the ASM representation of the decryption routine at runtime.
- Determining the function calling conventions, the parameters being passed to the functions and the returning values.
- Deriving generic function’s functionality based on the previous step (looking into the actual function to confirm the thesis).
- Commenting ASM representation with high-level information.
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+=1Which results in the configuration being decrypted.
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: 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 theLoadLibraryA
API as shown below:
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:
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.
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.
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.
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.