{
	"id": "3efaf09a-3e7f-433a-8687-98af6ea3b1ed",
	"created_at": "2026-04-06T00:09:39.480786Z",
	"updated_at": "2026-04-10T13:11:37.499456Z",
	"deleted_at": null,
	"sha1_hash": "2581a2276de96a0ad9471c33859312ee26835ccb",
	"title": "How To Write a Simple Configuration Extractor For .NET Malware - RevengeRAT",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 8613115,
	"plain_text": "How To Write a Simple Configuration Extractor For .NET\r\nMalware - RevengeRAT\r\nBy Matthew\r\nPublished: 2023-10-05 · Archived: 2026-04-05 18:05:41 UTC\r\nThis post is an introduction to developing configuration extractors for dotnet malware. The sample used here is\r\nRevengeRat, this rat typically employs minimal obfuscation and presents an ideal introduction for config\r\nextraction.\r\nThe sample has config which can be obtained via strings. However, it is far more interesting and useful to obtain\r\nthe same values by enumerating IL instructions present inside the code. This allows the analyst to hone in on\r\nparticular string values and eventually build more advanced configuration extractors.\r\nThe two primary samples we will be using are\r\nInitial Sample Link: 0d05942ce51fea8c8724dc6f3f9a6b3b077224f1f730feac3c84efe2d2d6d13e\r\nObfuscated Sample Link: dd203194d0ea8460ac3173e861737a77fa684e5334503867e91a70acc7f73195\r\nOverview\r\nFirst Step - Manually Locating the Configuration\r\nTo build a automated configuration extractor, we first need to be able to locate the configuration manually. For\r\n.NET based malware, this means opening up the file in Dnspy and attempting to locate configuration values or\r\nfunctions. .\r\nFor .NET malware, the entry point is a good place to start looking. This is because configuration is generally\r\nresolved early in the malware execution.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 1 of 24\n\nFor this sample, the Entry Point is the Main function. Lucky for us, the config values are directly above the entry\r\npoint inside of Atomic() .\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 2 of 24\n\nThis is a rare case where the configuration is already in plaintext and is extremely simple to find. Since it is\r\nextremely simple to find, it's also extremely simple to write an extractor.\r\nFor this sample, you could just run strings and you would obtain the same values, but the point of this\r\npost is to do the entire process via scripting. This will build foundational skills that are essential for\r\nbuilding extractors for more complex malware.\r\nNow that the config has been found, we want to hone in deeper on the Atomic() method that contains the config\r\nvalues.\r\nThis can be done by clicking on Atomic() in the side menu.\r\nThis ensures that the decompiled code is only that of the relevant function.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 3 of 24\n\nNow this is where things get interesting.\r\nSwitching to IL Instructions\r\nTo build configuration extractors for dotnet malware, we generally need to leverage dnlib .\r\nAs far as we can tell, dnlib has no knowledge of the decompiled c# code that we see in Dnspy.\r\nDnlib works best with Intermediate Language (IL) instructions and not decompiled c# code.\r\nTo accommodate this, we also need to switch to Intermediate Language Instructions.\r\nWe can do this by changing this dropdown box from C# to IL .\r\nThe Atomic() code has now changed significantly. The output now contains Intermediate Language instructions\r\nand opcodes instead of the usual c# code.\r\nEverything in this view can be accessed and enumerated via dnlib inside of a python script.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 4 of 24\n\nHeres a quick screenshot to better understand the output.\r\nFun fact - the bytecodes column is extremely useful for developing yara rules targeting dotnet malware.\r\nThese are the bytecodes that are present in the raw binary. Binary Defense blog\r\nWe now want to locate the same configuration values within the IL instructions.\r\nLuckily, they're all still there. Noting that each of the config values are referenced as part of ldstr operations.\r\nldstr is short for \"Load String\" and is unsurprisingly used to load strings.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 5 of 24\n\nFor more complex malware this will look almost exactly the same, with the exception that the strings will be\r\nencrypted.\r\nThe first step of dealing with more complex malware is locating the encrypted values using an identical process to\r\nwhat we're doing here with RevengeRat.\r\nBelow is an Asyncrat sample, where config values are loaded via ldstr operations before undergoing decryption.\r\nInteracting with Dotnet Using Python\r\nNow that we have located the plaintext configuration inside of our file, we want to locate those same values using\r\nan automated script.\r\nTo do this, we will use Python and the dnlib library.\r\nThe following code will load the revenge.bin file into Python using dnlib .\r\nNote that \"dnlib.dll\" must be inside the same directory as your script.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 6 of 24\n\nFor all future code snippets, we will assume you have the above code at the beginning of your script.\r\nThis ensures that all the relevant libraries and options are imported.,\r\nWith the module now loaded, we can perform some simple operations to replicate our process in Dnspy.\r\nFor example, we can list all available namespaces to match that of Dnspy. They aren’t in the same order but you\r\ncan see that they are all there.\r\nNote that when using dnlib, everything has to be first accessed via it’s associated class/type.\r\nEg type → namespace ( to obtain a namespace, you must first access a type) or type → method (To obtain a\r\nmethod/function, you must first access a type. )\r\nThis is slightly different to how dnspy displays namespace → type → method\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 7 of 24\n\nfor type in module.GetTypes() - this enumerates all types within the malware.\r\nif type.Namespace not in namespaces - this is to avoid printing the same namespace twice.\r\nnamespaces.append(type.Namespace) - adds the namespace to a list\r\nprint(type.Namespace) - this prints the namespace\r\nTo obtain all available methods in the Nuclear_Explosion namespace, we can do something like this. Note that\r\nthe types must be referenced first.\r\nThis will display all available methods in the nuclear_explosion namespace. Although they are in a\r\nslightly different order by default.\r\nNote that since the Atomic() method has the same name as the parent type of Atomic , it is classed as a\r\nconstructor as is named as .ctor when accessed via dnlib .\r\nThis is slightly confusing but something you have to get used to if you haven’t worked with object oriented (c#,\r\njava etc) code before.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 8 of 24\n\nAccessing IL Instructions\r\nIf we hone in on a particular method name, we can obtain the IL instructions just as they were seen in dnspy.\r\nIn this case we have chosen the BS method, simply because it’s short and easy to demonstrate the concept.\r\nBelow, see how the IL instructions printed via python match those displayed via Dnspy.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 9 of 24\n\nNow, we can make it more interesting and do the same with the original Atomic() method that contains the\r\nrelevant config.\r\nNote that since Atomic() has the same name as the Atomic type/class, it is classified as a constructor which is\r\nshortened to .ctor .\r\nIf you haven’t worked with object oriented code before, it may be worth googling constructors to get a\r\nbasic understanding of what they are.\r\nTLDR:\r\n- Constructors are methods/functions that are automatically executed when an object/type/class is\r\ncreated.\r\n- Constructors have the same name as the parent object/type/class.\r\n- Values that require initialization (eg config), are very often found in the constructor for the relevant\r\nclass/type/object.\r\nFor now, just know that the config is inside the .ctor method and you will see this often.\r\nWith this knowledge, we can change the previous code to print instructions for the .ctor method.\r\nUsing the previous code and updating the method name to .ctor , we can print all of the relevant instructions to\r\nmatch that of Dnspy.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 10 of 24\n\nIn the printed instructions, we can see the IL instructions containing plaintext config values. The same as can be\r\nseen in Dnspy.\r\nThe config values are all referenced via ldstr operations. The script can be modified to only print instructions\r\ncontaining ldstr .\r\n(Make sure you have the line from dnlib.Dotnet.Emit import OpCodes line at the beginning of your script)\r\nWith the additional filtering for ldstr operations, running the script will now output the config related\r\ninstructions.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 11 of 24\n\nModifying the final line to print only instr.Operand makes the output even cleaner.\r\nAt this point. You can add your own code to provide additional formatting and or adjustments to the values. we\r\nwon’t really cover that here as the format requirements will be different for everyone.\r\nTesting on additional Samples\r\nFrom here, you can obtain an additional sample for testing.\r\nIn this case, we have used the sample.\r\n2b89a560332bbc135735fe7f04ca44294703f3ae75fdfe8e4fc9906521fd3102\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 12 of 24\n\nRunning the script on the second file produces the following results.\r\nAdding Resilience By Improving Method Signatures.\r\nAt this point, you can obtain config values from other samples. But this assumes that the additional samples have\r\nnot employed any obfuscation and have kept the same method/namespace/class names.\r\nNow there is just one problem, what happens if the malware author decides to modify any of those?\r\nThe sample dd203194d0ea8460ac3173e861737a77fa684e5334503867e91a70acc7f73195 introduces this exact\r\nproblem.\r\nThis sample uses largely the same structure as before, but uses randomized namespace and type names.\r\nThis breaks our original script as there is no Nuclear_Explosion namespace or Atomic class to signature from.\r\nRunning the script on the new sample produces no results.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 13 of 24\n\nWe can see below that the code is largely the same, but the method and class names are different.\r\nThere are some similarities in other method names, (data, decode, BS etc) but these could be easily changed as\r\nwell so we will avoid using this as part of a signature.\r\nFor the most resilient approach, we will instead use the IL operations.\r\n(There are other signature opportunities, but they will not be covered in this post)\r\nSee below, the obfuscated sample and the original sample contain the same IL instructions for loading config\r\nvalues.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 14 of 24\n\nIf we implement the following code. We can enumerate all available types and methods in the obfuscated sample,\r\nprinting all values contained in ldstr operations.\r\nhas_config_pattern(method) - a (currently) empty function for enumerating configuration patterns.\r\nmethod.HasBody - this ensures that empty methods/functions are skipped.\r\nThis script will enumerate all ldstr operations within the obfuscated file and print the loaded value.\r\nTechnically, this prints the config values, but it also prints 269 other string values which are not useful. So we\r\nwant to improve the has_config_pattern function to hone in only on the methods containing relevant IL\r\ninstructions.\r\n(Note that we are using the initial file here for readability)\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 15 of 24\n\nLet’s modify the has_config_pattern function to filter on matching IL instructions.\r\nFor this example, we will use the last 14 instructions of the Atomic function. You can use more or less,\r\nexperiment to see what works best for you.\r\nWe will re-use one of the previous code snippets, which prints the .ctor IL instructions related to\r\nNuclear_Explosion .\r\nThis prints a long list of instructions, but as mentioned, we will be using the last 14 for our signature.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 16 of 24\n\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 17 of 24\n\nTo generate a signature, we can copy out the values and create a string array like this.\r\nThe entire code now looks like this.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 18 of 24\n\nand the signature checking code has_config_pattern now looks like this.\r\nmethod.HasBody - this is a filter to ensure the checked method is not empty\r\nif len(method.Body.Instructions) \u003e= len(signature) - this is a filter to ensure the checked method is\r\nat least as long as the signature.\r\nins = [x.OpCode.Name for x in method.Body.Instructions] - this creates an array of instructions for\r\nmethod being checked.\r\n[x.OpCode.Name](\u003chttp://x.OpCode.Name\u003e) - this obtains only the instruction opcode name, which\r\nproduces an array that looks like our signature array.\r\nif ins[-len(signature:] == signature - we only want to check the last instructions against our\r\nsignature. if our signature is 14 instructions, we only want to check the last 14 instructions against our\r\nsignature.\r\nThis is the most important piece of the has_config_pattern function. Which compares the final instructions\r\nagainst our signature.\r\nWith the new signature added, we can remove the .ctor and nuclear_explosion check and re run against our\r\noriginal sample.\r\nThe config is found exactly as before. Despite the name signatures being removed. Only the IL instructions are\r\nused to locate the config values.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 19 of 24\n\nRunning Against The Obfuscated Sample.\r\nRunning the new code against the obfuscated sample\r\ndd203194d0ea8460ac3173e861737a77fa684e5334503867e91a70acc7f73195 . The config values are able to be\r\nobtained.\r\nThe configuration values are able to be extracted from both. Regardless of the fact that the method and class\r\nnames are different between samples.\r\nThis is due to the identical opcode instructions between the two samples.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 20 of 24\n\nBy very slightly modifying the script to take a filename as argument sys.argv[1] , we can implement a bulk\r\nextractor for many files.\r\nFor bulk extraction, the final code has been modified to print everything on a single line. As well as printing the\r\nfilename.\r\nThis produces a slightly cleaner output for an individual file.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 21 of 24\n\nNow, if we can obtain a set of samples (We used unpacme).\r\nWe can combine this with a short powershell script for bulk config extraction.\r\nThis particular script has been placed in a folder with lots of RevengeRat Samples.\r\nThe sample folder is shown below\r\nRunning the powershell script, produces the following results. There are some failures but the extractor mostly\r\nworks. The failures are due to slightly differing patterns in some obfuscated samples. This is something that will\r\nbe covered in a future post.\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 22 of 24\n\nConclusion and Final Takeaways\r\nIn this post, we have covered the basics of extracting configuration from a very basic dotnet malware sample. The\r\ntechniques covered here form the basis of configuration extraction for most dotnet malware. Advanced samples\r\nwill not store values in plaintext, but encrypted values will typically be stored in a very similar way via ldstr\r\noperations.\r\nThe initial steps (prior to decryption) for advanced samples will be the same as seen here today.\r\nIf you found any of this useful, consider signing up to the site. Signed up members will receive access to a discord\r\nserver, bonus content and early access to future posts.\r\nSign up for Embee Research\r\nMalware Analysis Insights\r\nNo spam. Unsubscribe anytime.\r\nReferences\r\nA collection of blogs and scripts that have helped me learn these concepts.\r\nRussianPanda - https://russianpanda.com/2023/07/04/WhiteSnake-Stealer-Malware-Analysis/\r\nN1ghtw0lf - https://n1ght-w0lf.github.io/tutorials/dotnet-string-decryptor/\r\nPolish Cert - https://cert.pl/en/posts/2023/09/unpacking-whats-packed-dotrunpex/\r\nOALabs Research - https://research.openanalysis.net/dotnet/static\r\nanalysis/stormkitty/dnlib/python/research/2021/07/14/dot_net_static_analysis.html\r\nFull Script\r\n\"\"\"\r\nRevenge Rat Config Extractor Example\r\n@embee_research\r\nSamples\r\n2b89a560332bbc135735fe7f04ca44294703f3ae75fdfe8e4fc9906521fd3102\r\n0d05942ce51fea8c8724dc6f3f9a6b3b077224f1f730feac3c84efe2d2d6d13e\r\n\"\"\"\r\nimport clr,sys\r\nclr.AddReference(\"dnlib\")\r\nimport dnlib\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 23 of 24\n\nfrom dnlib.DotNet import *\r\nfrom dnlib.DotNet.Emit import OpCodes\r\nfilename = sys.argv[1]\r\nmodule = dnlib.DotNet.ModuleDefMD.Load(filename)\r\nsignature = [\"call\",\"stfld\",\"ldarg.0\",\"ldstr\",\"stfld\",\"ldarg.0\",\"ldstr\",\"stfld\",\"ldarg.0\",\"ldc.i4.0\",\"stfld\",\"ld\r\ndef has_config_pattern(method):\r\n if method.HasBody:\r\n if len(method.Body.Instructions) \u003e= len(signature):\r\n ins = [x.OpCode.Name for x in method.Body.Instructions]\r\n if ins[-len(signature):] == signature:\r\n return True\r\n return False\r\nresults = []\r\nfor type in module.GetTypes():\r\n for method in type.Methods:\r\n if has_config_pattern(method) and method.HasBody:\r\n for instr in method.Body.Instructions:\r\n if instr.OpCode == OpCodes.Ldstr:\r\n results.append(instr.Operand)\r\n \r\nprint(\"Sample: \" + filename, end=\"\")\r\nprint(\": \" + str(results))\r\nSource: https://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nhttps://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/\r\nPage 24 of 24",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://embee-research.ghost.io/introduction-to-dotnet-configuration-extraction-revengerat/"
	],
	"report_names": [
		"introduction-to-dotnet-configuration-extraction-revengerat"
	],
	"threat_actors": [],
	"ts_created_at": 1775434179,
	"ts_updated_at": 1775826697,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/2581a2276de96a0ad9471c33859312ee26835ccb.pdf",
		"text": "https://archive.orkl.eu/2581a2276de96a0ad9471c33859312ee26835ccb.txt",
		"img": "https://archive.orkl.eu/2581a2276de96a0ad9471c33859312ee26835ccb.jpg"
	}
}