Malware Config Extractor Remcos
The sample is a .NET packed remcos sample.
MD5: 5e9770c2b22b03e5726285900afab954
SHA256: ad3dc7a0c6ce33a7e45775b3452343eb748fab8823311df58d4599d6a203ff80
MalwareBazaar Link: https://bazaar.abuse.ch/sample/ad3dc7a0c6ce33a7e45775b3452343eb748fab8823311df58d4599d6a203ff80/
Table of Contents
Analysing the sample
First Stage
Upon examining the sample in dnspy, we observe the following:
- Initially, the sample commences execution from
myObject()
, which triggersmonoFlat_b()
and invokes theGetTypes()
function on the returned value. The variabletype
holds the method defined in the object returned bymonoFlat_b()
. monoFlat_b()
calls the functionmonoFlatxB()
. The object returned by it is loaded into memory usingAppDomain.CurrentDomain.Load()
which is of type assembly. This object is then returned by the functionmonoFlat_b()
.- In
monoFlatxB()
, artifacts present in subresource are extracted. The variableMainWindow.nabexx
is set to “XX”. So, 2 files are extracted from the sample: XX and XXXXXX. Then it calls the functionDmonoFlatECXES()
- In
DmonoFlatECXES()
, the resource named “XXXXXX” is decrypted using artifact “XX” as the key. The decryption process utilizes AES-ECB mode, which is not the most secure version of AES due to its susceptibility to cryptographic attacks.
After decryption, it is loaded into memory, initiating the next phase of execution.
Now we can write a Python script to extract subresources and decrypt it using AES-ECB. We will use the library dotnetfile in order to extract subresources from the sample and the library pycryptodome for implementing cryptographic functions.
Python code:
import dotnetfile
from Crypto.Cipher import AES
def aes_ecb_decrypt(key,data):
''' decrypts data using AES-ECB mode '''
cipher = AES.new(key,AES.MODE_ECB)
decrypted_data = cipher.decrypt(data)
return decrypted_data
def subresource_extract(filename):
''' extracts subresources from dotnetfile '''
''' reference : https://github.com/pan-unit42/dotnetfile/blob/main/examples/dotnetfile_dump.py '''
extracted_subresources = []
dotnet_file =dotnetfile.DotNetPE(filename)
resource_data = dotnet_file.get_resources()
for data in resource_data:
for resource_item in data.items():
if resource_item[0] == 'SubResources':
if resource_item[1]:
for sub_resource in resource_item[1]:
for sub_resource_item in sub_resource.items():
if sub_resource_item[0] == "Data":
extracted_subresources.append(sub_resource_item[1])
return extracted_subresources
def main ():
''' Name of the file'''
filename = input("Filename: ")
''' Extract subresources which contains the dll file in encrypted form'''
data_from_pe = subresource_extract(filename)
''' Getting the key and data pair correct'''
if len(data_from_pe[0]) >= len(data_from_pe[1]):
key = data_from_pe[1]
data = data_from_pe[0]
else:
key = data_from_pe[0]
data = data_from_pe[1]
''' Decrypting the first payload'''
Decrypted_payload = aes_ecb_decrypt(key,data)
write_to_file(Decrypted_payload,"first_payload")
if __name__ == '__main__':
main()
Second Stage
With the file loaded into memory extracted, we can proceed with static analysis. The file is a .NET Framework DLL file, containing three resources as depicted in below Fig 6. All three resources are decrypted and loaded into memory. The resource named “TypeIdXC1XSJP” contains the remcos sample, while the remaining two resources are .NET dll files utilized to load the decrypted remcos into memory and initiate its execution.
Examining the relevant code in the sample in dnspy, we observe the following:
- The main function is invoked in the dll sample. The highlighted portion shows the sample sleeping for 1000ms. Initially,
XDDEKFADNMADHA()
extracts a resource named “TypeIdXC1XSJP”. Subsequently, it undergoes a gunzip operation inPaddingYBobhvB()
. Finally, it is decrypted inPaddingYBobhvBDecryptBytes()
using the hardcoded password and salt. - The
PaddingYBobhvBDecryptBytes()
function utilizes AES-CBC mode with a 32-byte key and a 16-byte IV. It usesPasswordDeriveBytes
, a Microsoft implementation of an extended PBKDF1 version, to generate key and IV bytes for decryption.
Now, we have a couple of challenges in writing our own extractor:
- Implementing
PasswordDeriveBytes
in Python. Further details can be found in the provided links below.: - Extracting hardcoded password and salt.
- we can use the hardcoded password and salt in our extractor however it might change in a future version.
Implementation of PasswordDeriveBytes in Python
There are implementation of PasswordDeriveBytes available online:
- .Net implementation
- Java implementation
- This implementation is quite good however it doesn’t behave the same way as original implementation when
getBytes()
is called
- This implementation is quite good however it doesn’t behave the same way as original implementation when
- Python implementation
- This implementation only works for scenario where
getBytes()
is called only once.
- This implementation only works for scenario where
So, we need to rewrite .NET code in Python. While AI tools were considered for translation, they were full of errors. Thus, a manual approach was chosen to ensure accurate conversion and same behaviour with multiple getBytes()
calls.
Disclaimer: This Python implementation may lack certain methods which are irrelevant to the current use case.
Python implementation of PasswordDeriveBytes
:
class PasswordDeriveBytes:
def __init__(self,password,salt):
self.iterations = 100
if isinstance(password,str):
self.password = password.encode()
if isinstance(salt,str):
self.salt = salt.encode()
self.lasthash = []
self.outbytes = []
self.extra = []
self.extraCount = 0
self.ctrl = 0
def ComputeBaseValue(self):
lasthash = hashlib.sha1(self.password+self.salt).digest()
self.iterations -= 1
for i in range(self.iterations-1):
lasthash = hashlib.sha1(lasthash).digest()
self.lasthash = lasthash
def ComputeBytes(self,cb):
num2 = 20
num = 0
array = []
if self.ctrl == 0:
self.outbytes = hashlib.sha1(self.lasthash).digest()
self.ctrl += 1
else :
self.outbytes = hashlib.sha1(str(self.ctrl).encode()+self.lasthash).digest()
array[num:num2+num] = self.outbytes[0:num2]
num = num + num2
while (cb > num):
self.outbytes = hashlib.sha1(str(self.ctrl).encode()+self.lasthash).digest()
self.ctrl += 1
array[num:num+num2] = self.outbytes[0:num2]
num += num2
return array
def getBytes(self,cb):
num = 0
array = []
array2 = []
if len(self.lasthash) == 0:
self.ComputeBaseValue()
elif len(self.extra) != 0:
num = len(self.extra) - self.extraCount
if num >= cb :
array[0:cb] = self.extra[self.extraCount:self.extraCount+cb]
if num > cb :
self.extraCount += cb
else :
self.extra = []
return bytearray(array)
array[0:num] = self.extra[num:num+num]
self.extra =[]
array2 = self.ComputeBytes(cb - num)
array[num:cb-num+num] = array2[0:cb-num]
if len(array2) + num > cb:
self.extra = array2
self.extraCount = cb - num
return bytearray(array)
Extraction of hardcoded password and salt
In order to extract password and salt from .NET file, we utilize dnlib library. A comprehensive tutorial by Embee Research provides detailed instructions on how to utlize dnlib: https://www.embeeresearch.io/introduction-to-dotnet-configuration-extraction-revengerat/
By following the steps outlined in the tutorial and modifying the provided code, we can successfully extract the hardcoded password and salt. We need to modify the signature and code a bit to suit our specific use case. We must update the signature array with highlighted IL opcode shown in Fig 9. above and adjust the code to locate the signature within the opcodes extracted by dnlib.
Code Snippet with comments:
#Initializing dnlib
import clr
clr.AddReference("dnlib")
import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes
def has_config_pattern(method,signature):
if method.HasBody:
if len(method.Body.Instructions) >= len(signature):
ins = [x.OpCode.Name for x in method.Body.Instructions] # create a list
if any(signature == list(x) for x in zip(*[ins[i:] for i in range(len(signature))])): # creates various list and matches it against signature
return True
return False
def extract_password_salt(filename,signature):
strings_from_file = []
module = dnlib.DotNet.ModuleDefMD.Load(filename) # loads the file
for type in module.GetTypes():
for method in type.Methods:
if has_config_pattern(method,signature) and method.HasBody:
for instr in method.Body.Instructions:
if instr.OpCode == OpCodes.Ldstr: # opcode ldstr loads the hardcoded strings. So we are checking for this opcode.
strings_from_file.append(instr.Operand)
return strings_from_file
..............................................................................................................................................
..............................................................................................................................................
# used to find the functions where password and salt is used
signature = ["ldc.i4", "call", "ldarg.0", "ldarg.0", "ldarg.0", "ldstr", "callvirt", "callvirt", "ldstr", "ldstr", "callvirt", "stloc.0"]
strings_from_file = extract_password_salt("first_payload",signature)
# discarding the first string which refers to name of the executable
# name of the resource containing malware
resource_name = strings_from_file[1]
# password used for decrypting
password = strings_from_file[2]
# salt used for decrypting
salt = strings_from_file[3]
Now we can combine the above code with our previously written code and successfully extract second stage
Python Code
import dotnetfile
from Crypto.Cipher import AES
import gzip
import hashlib
import clr
clr.AddReference("dnlib")
import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes
class PasswordDeriveBytes:
def __init__(self,password,salt):
self.iterations = 100
if isinstance(password,str):
self.password = password.encode()
if isinstance(salt,str):
self.salt = salt.encode()
self.lasthash = []
self.outbytes = []
self.extra = []
self.extraCount = 0
self.ctrl = 0
def ComputeBaseValue(self):
lasthash = hashlib.sha1(self.password+self.salt).digest()
self.iterations -= 1
for i in range(self.iterations-1):
lasthash = hashlib.sha1(lasthash).digest()
self.lasthash = lasthash
def ComputeBytes(self,cb):
num2 = 20
num = 0
array = []
if self.ctrl == 0:
self.outbytes = hashlib.sha1(self.lasthash).digest()
self.ctrl += 1
else :
self.outbytes = hashlib.sha1(str(self.ctrl).encode()+self.lasthash).digest()
array[num:num2+num] = self.outbytes[0:num2]
num = num + num2
while (cb > num):
self.outbytes = hashlib.sha1(str(self.ctrl).encode()+self.lasthash).digest()
self.ctrl += 1
array[num:num+num2] = self.outbytes[0:num2]
num += num2
return array
def getBytes(self,cb):
num = 0
array = []
array2 = []
if len(self.lasthash) == 0:
self.ComputeBaseValue()
elif len(self.extra) != 0:
num = len(self.extra) - self.extraCount
if num >= cb :
array[0:cb] = self.extra[self.extraCount:self.extraCount+cb]
if num > cb :
self.extraCount += cb
else :
self.extra = []
return bytearray(array)
array[0:num] = self.extra[num:num+num]
self.extra =[]
array2 = self.ComputeBytes(cb - num)
array[num:cb-num+num] = array2[0:cb-num]
if len(array2) + num > cb:
self.extra = array2
self.extraCount = cb - num
return bytearray(array)
def gzip_decrompress(data):
data_decompress = gzip.decompress(data)
return data_decompress
def write_to_file(data,filename):
with open(filename, "wb") as binary_file:
binary_file.write(data)
return None
def aes_ecb_decrypt(key,data):
cipher = AES.new(key,AES.MODE_ECB)
decrypted_data = cipher.decrypt(data)
return decrypted_data
def aes_cbc_decrypt(key,data,salt):
derivedBytes = PasswordDeriveBytes(key,salt)
derivedkey = derivedBytes.getBytes(32)
iv = derivedBytes.getBytes(16)
cipher = AES.new(derivedkey,AES.MODE_CBC,iv)
decrypted_data = cipher.decrypt(data)
return decrypted_data
def resource_extract(filename):
''' extracts subresources from dotnetfile '''
''' reference : https://github.com/pan-unit42/dotnetfile/blob/main/examples/dotnetfile_dump.py '''
extracted_resources = {}
dotnet_file = dotnetfile.DotNetPE(filename)
resource_data = dotnet_file.get_resources()
for data in resource_data:
for resource_item in data.items():
if resource_item[0] == "Name":
key = resource_item[1]
if resource_item[0] == "Data":
extracted_resources[key] = resource_item[1]
return extracted_resources
def subresource_extract(filename):
''' extracts subresources from dotnetfile '''
''' reference : https://github.com/pan-unit42/dotnetfile/blob/main/examples/dotnetfile_dump.py '''
extracted_subresources = []
dotnet_file =dotnetfile.DotNetPE(filename)
resource_data = dotnet_file.get_resources()
for data in resource_data:
for resource_item in data.items():
if resource_item[0] == 'SubResources':
if resource_item[1]:
for sub_resource in resource_item[1]:
for sub_resource_item in sub_resource.items():
if sub_resource_item[0] == "Data":
extracted_subresources.append(sub_resource_item[1])
return extracted_subresources
def has_config_pattern(method,signature):
if method.HasBody:
if len(method.Body.Instructions) >= len(signature):
ins = [x.OpCode.Name for x in method.Body.Instructions] # create a list
if any(signature == list(x) for x in zip(*[ins[i:] for i in range(len(signature))])): # creates various list and matches it against signature
return True
return False
def extract_password_salt(filename,signature):
strings_from_file = []
module = dnlib.DotNet.ModuleDefMD.Load(filename)
for type in module.GetTypes():
for method in type.Methods:
if has_config_pattern(method,signature) and method.HasBody:
for instr in method.Body.Instructions:
if instr.OpCode == OpCodes.Ldstr: # opcode ldstr loads the hardcoded strings. So we are checking for this opcode.
strings_from_file.append(instr.Operand)
return strings_from_file
def main ():
''' Name of the file'''
filename = input("Filename: ")
''' Extract subresources which contains the dll file in encrypted form'''
data_from_pe = subresource_extract(filename)
''' Getting the key and data pair correct by ensuring size of key is less than size of data'''
if len(data_from_pe[0]) >= len(data_from_pe[1]):
key = data_from_pe[1]
data = data_from_pe[0]
else:
key = data_from_pe[0]
data = data_from_pe[1]
''' Decrypting the first payload'''
Decrypted_payload = aes_ecb_decrypt(key,data)
write_to_file(Decrypted_payload,"first_payload") #saving it to the file
''' Extracting resources from first payload'''
data_from_first_playload = resource_extract("first_payload") # extracting the resources
''' Extracting the hardcoded password and salt from the file '''
# used to find the functions where password and salt is used
signature = ["ldc.i4", "call", "ldarg.0", "ldarg.0", "ldarg.0", "ldstr", "callvirt", "callvirt", "ldstr", "ldstr", "callvirt", "stloc.0"]
strings_from_file = extract_password_salt("first_payload",signature)
# discarding the first string which refers to name of the executable
# name of the resource containing malware
resource_name = strings_from_file[1]
# password used for decrypting
password = strings_from_file[2]
# salt used for decrypting
salt = strings_from_file[3]
''' Processing the first extracted resource'''
decompress_data = gzip_decrompress(data_from_first_playload[resource_name]) # gunzipping the required resource.
Decrypted_final_payload = aes_cbc_decrypt(password,decompress_data,salt) # decrypting it using extracted hardcoded password and salt
write_to_file(Decrypted_final_payload,"second_payload") # Saving it to the file
if __name__ == '__main__':
main()
Final Stage
At this point, we have successfully extracted the remcos malware. After analyzing the remcos sample for a period of time, we can identify where the configuration is stored and decrypted. Tools like capa can be used in Ghidra to reduce analysis time.
Upon examining the functions using Ghidra, we observe the following:
FUN_004098e8
calls 3 functions: FUN_00409d73, FUN_00402f8b and FUN_0040308e. It also allocates various buffer.FUN_00409d73
extracts resource named: “SETTINGS” from the sample.FUN_00402fa6
implements the first phase : Key Scheduling Algorithm of RC4FUN_0040308e
implements the second phase : Pseudo-Random Generation Algorithm of RC4 and XORring with encrypted config.
Upon analyzing the FUN_004098e8
function, it becomes clear that the first byte of the data read by FUN_00409d73
represents the length of the key, followed by the key itself, and then the encrypted configuration.
All that is left for us to do is to write the code for extracting resource name Settings, separate the key and encrypted config and finally decrypt it using RC4.
Python Code :
import pefile
import dotnetfile
from Crypto.Cipher import AES,ARC4
import gzip
import hashlib
import clr
clr.AddReference("dnlib")
import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes
class PasswordDeriveBytes:
def __init__(self,password,salt):
self.iterations = 100
if isinstance(password,str):
self.password = password.encode()
if isinstance(salt,str):
self.salt = salt.encode()
self.lasthash = []
self.outbytes = []
self.extra = []
self.extraCount = 0
self.ctrl = 0
def ComputeBaseValue(self):
lasthash = hashlib.sha1(self.password+self.salt).digest()
self.iterations -= 1
for i in range(self.iterations-1):
lasthash = hashlib.sha1(lasthash).digest()
self.lasthash = lasthash
def ComputeBytes(self,cb):
num2 = 20
num = 0
array = []
if self.ctrl == 0:
self.outbytes = hashlib.sha1(self.lasthash).digest()
self.ctrl += 1
else :
self.outbytes = hashlib.sha1(str(self.ctrl).encode()+self.lasthash).digest()
array[num:num2+num] = self.outbytes[0:num2]
num = num + num2
while (cb > num):
self.outbytes = hashlib.sha1(str(self.ctrl).encode()+self.lasthash).digest()
self.ctrl += 1
array[num:num+num2] = self.outbytes[0:num2]
num += num2
return array
def getBytes(self,cb):
num = 0
array = []
array2 = []
if len(self.lasthash) == 0:
self.ComputeBaseValue()
elif len(self.extra) != 0:
num = len(self.extra) - self.extraCount
if num >= cb :
array[0:cb] = self.extra[self.extraCount:self.extraCount+cb]
if num > cb :
self.extraCount += cb
else :
self.extra = []
return bytearray(array)
array[0:num] = self.extra[num:num+num]
self.extra =[]
array2 = self.ComputeBytes(cb - num)
array[num:cb-num+num] = array2[0:cb-num]
if len(array2) + num > cb:
self.extra = array2
self.extraCount = cb - num
return bytearray(array)
def gzip_decrompress(data):
data_decompress = gzip.decompress(data)
return data_decompress
def write_to_file(data,filename):
with open(filename, "wb") as binary_file:
binary_file.write(data)
return None
def aes_ecb_decrypt(key,data):
cipher = AES.new(key,AES.MODE_ECB)
decrypted_data = cipher.decrypt(data)
return decrypted_data
def aes_cbc_decrypt(key,data,salt):
derivedBytes = PasswordDeriveBytes(key,salt)
derivedkey = derivedBytes.getBytes(32)
iv = derivedBytes.getBytes(16)
cipher = AES.new(derivedkey,AES.MODE_CBC,iv)
decrypted_data = cipher.decrypt(data)
return decrypted_data
def rc4_decrypt(data,key):
cipher = ARC4.new(key)
decrypted_data = cipher.decrypt(data)
return decrypted_data
def resource_extract(filename):
''' extracts subresources from dotnetfile '''
''' reference : https://github.com/pan-unit42/dotnetfile/blob/main/examples/dotnetfile_dump.py '''
extracted_resources = {}
dotnet_file = dotnetfile.DotNetPE(filename)
resource_data = dotnet_file.get_resources()
for data in resource_data:
for resource_item in data.items():
if resource_item[0] == "Name":
key = resource_item[1]
if resource_item[0] == "Data":
extracted_resources[key] = resource_item[1]
return extracted_resources
def subresource_extract(filename):
''' extracts subresources from dotnetfile '''
''' reference : https://github.com/pan-unit42/dotnetfile/blob/main/examples/dotnetfile_dump.py '''
extracted_subresources = []
dotnet_file =dotnetfile.DotNetPE(filename)
resource_data = dotnet_file.get_resources()
for data in resource_data:
for resource_item in data.items():
if resource_item[0] == 'SubResources':
if resource_item[1]:
for sub_resource in resource_item[1]:
for sub_resource_item in sub_resource.items():
if sub_resource_item[0] == "Data":
extracted_subresources.append(sub_resource_item[1])
return extracted_subresources
def extract_encrypted_resource(filename):
pe = pefile.PE(filename)
for resource in pe.DIRECTORY_ENTRY_RESOURCE.entries:
for entry in resource.directory.entries:
if entry.name is not None:
if str(entry.name) == "SETTINGS":
offset = entry.directory.entries[0].data.struct.OffsetToData
size = entry.directory.entries[0].data.struct.Size
encrypted_resource = pe.get_memory_mapped_image()[offset:offset+size]
return encrypted_resource
def has_config_pattern(method,signature):
if method.HasBody:
if len(method.Body.Instructions) >= len(signature):
ins = [x.OpCode.Name for x in method.Body.Instructions] # create a list
if any(signature == list(x) for x in zip(*[ins[i:] for i in range(len(signature))])): # creates various list and matches it against signature
return True
return False
def extract_password_salt(filename,signature):
strings_from_file = []
module = dnlib.DotNet.ModuleDefMD.Load(filename)
for type in module.GetTypes():
for method in type.Methods:
if has_config_pattern(method,signature) and method.HasBody:
for instr in method.Body.Instructions:
if instr.OpCode == OpCodes.Ldstr: # opcode ldstr loads the hardcoded strings. So we are checking for this opcode.
strings_from_file.append(instr.Operand)
return strings_from_file
def main ():
''' Name of the file'''
filename = input("Filename: ")
''' Extract subresources which contains the dll file in encrypted form'''
data_from_pe = subresource_extract(filename)
''' Getting the key and data pair correct by ensuring size of key is less than size of data'''
if len(data_from_pe[0]) >= len(data_from_pe[1]):
key = data_from_pe[1]
data = data_from_pe[0]
else:
key = data_from_pe[0]
data = data_from_pe[1]
''' Decrypting the first payload'''
Decrypted_payload = aes_ecb_decrypt(key,data)
write_to_file(Decrypted_payload,"first_payload") #saving it to the file
''' Extracting resources from first payload'''
data_from_first_playload = resource_extract("first_payload") # extracting the resources
''' Extracting the hardcoded password and salt from the file '''
# used to find the functions where password and salt is used
signature = ["ldc.i4", "call", "ldarg.0", "ldarg.0", "ldarg.0", "ldstr", "callvirt", "callvirt", "ldstr", "ldstr", "callvirt", "stloc.0"]
strings_from_file = extract_password_salt("first_payload",signature)
# discarding the first string which refers to name of the executable
# name of the resource containing malware
resource_name = strings_from_file[1]
# password used for decrypting
password = strings_from_file[2]
# salt used for decrypting
salt = strings_from_file[3]
''' Processing the first extracted resource'''
decompress_data = gzip_decrompress(data_from_first_playload[resource_name]) # gunzipping the required resource.
Decrypted_final_payload = aes_cbc_decrypt(password,decompress_data,salt) # decrypting it using extracted hardcoded password and salt
write_to_file(Decrypted_final_payload,"second_payload") # Saving it to the file
'''final config extraction from remcos malware'''
# extract resource named SETTINGS
encrypted_data = extract_encrypted_resource("second_payload")
keylen = encrypted_data[0] # size of the key
key = encrypted_data[1:keylen+1] # key
encrypted_config = encrypted_data[keylen+1:] # encrypted config
decrypted_config = rc4_decrypt(encrypted_config,key)
print(decrypted_config.decode("utf-8"))
if __name__ == '__main__':
main()
Running the extractor
When we run the final code against our malware sample, we can see that it prints decrypted config:
Further reading regarding .NET internals:
- https://www.red-gate.com/simple-talk/blogs/anatomy-of-a-net-assembly-methods/
- https://exploitreversing.com/wp-content/uploads/2022/05/mas_4.pdf
- Alexandre Borges - .NET Malware Threats: Internals And Reversing : Video and Slides