Introduction
We continuously track various malware families, including many loaders. Since early 2022, we have been monitoring a .NET loader that deploys payloads through multiple stages. The payloads consist primarily of commodity stealers and RATs.
In this brief write-up, we provide an overview of the loading behavior, describe our tracking strategy, share payload statistics, and provide a YARA rule to help fellow researchers track this loader in their malware feeds.
While we cannot attribute this loader to any specific malware family or actor, we welcome insights and information from the community. Unit42 from Palo Alto Networks has recently observed and reported on the same loader: https://unit42.paloaltonetworks.com/malicious-payloads-as-bitmap-resources-hide-.net-malware/
Multi-stage loader
The loader consists of three stages that work together to deliver the final payload (see Figure 1).
- The first stage is a .NET executable that embeds the second and third stages in encrypted form. While earlier versions embedded the second stage as a hardcoded string, more recent versions use a bitmap resource. The first stage extracts and decrypts this data, then executes it in memory to launch the second stage.
- The second stage is a .NET DLL that takes three parameters. Using these parameters, it locates and XOR-decrypts a bitmap resource from the first stage. This decrypted data is then loaded and executed in memory as the third stage. See the appendix for an example of this loading behavior.
- The third stage, also a .NET DLL, manages the final payload's deployment in memory.

Tracking the multi-stage loader
While the first two stages frequently change and are difficult to track, the third stage—which deploys the malware payloads—maintains a relatively stable code structure. This stability has enabled us to track it effectively using our code reuse technology. Through code reuse clustering, we discovered this loader and identified a cluster of 20,000 samples across approximately three years.
To analyze this cluster more deeply, we performed a code reuse analysis at the function level, examining the frequency of specific functions across all samples. This approach allowed us to identify similar versions of functions, rather than relying on simple instruction-based matching.
Our analysis revealed one specific function that appears consistently across all samples, with minor variations. This function handles the loading and execution of the final payload in memory. The source code in Figure 2 shows an example variant of this function from sample 873eb1535c73bab017c8e351443519d576761c759884ea95e32d3ed26173fddc.
private static void execute_final_payload()
{
try
{
Assembly u = main_class.assembly_load(main_class.byte_pattern);
object[] u2 = null;
bool flag = main_class.get_method_parameters(main_class.get_assembly_entry_point(u)).Length != 0;
if (flag)
{
u2 = new object[]
{
new string[1]
};
}
main_class.invoke(main_class.get_assembly_entry_point(u), null, u2);
}
catch (Exception ex)
{
main_class.execute_again(0, main_class.get_assembly_location(main_class.get_entry_assembly()));
}
}
Figure 2: Source code of function responsible for payload execution. Variable, method and class names were renamed for better readability.
After analyzing byte patterns across all variants of this function in our clustered samples, we identified common patterns that enabled us to create the YARA rule in Figure 3 for detecting the loader's third stage. For quality assurance, we tested this rule on a random subset of 5,000 out of 20,000 third stages.
import "dotnet"
rule multi_layer_loader_3rd_stage {
meta:
description = "Detects third stage multi-layer .NET loader"
date = "2025-05-09"
author = "Threatray (David Pastor)"
hash = "063ca3294442e1194f637e02186e9682f3872c59e6247b8a8c759e9cba936669, 873eb1535c73bab017c8e351443519d576761c759884ea95e32d3ed26173fddc, d3987a5d9cb294e7cc7990c9a45b2a080dc99aa7b61fc4c9e437fc4659effda7, 7532336b3fb752a7fa95aa1da5ddc527600d0cbba1aa2d77b46052439a32e619, 685478424a00d7690aad5768bf08e9a61f335dae5706eebf23e612b6d2cacdf8, f6ae4366b5e0ae5e46c9c1ec6045cdfec80fed0e3292f3275a74f81800109d42, 052efeadeb1533936df0a1656b6f2f59f47ef10698274356e3231099f87427c4, 401ed7a01b85082da8ab1d2400a209724353af167100a98b864b4e7365daf4e9"
strings:
/**
28 ?? 0? 00 06 call class [mscorlib]System.Reflection.MethodInfo
14 ldnull
11 0? ldloc.s 1
**/
$func1_1 = {28 ?? 0? 00 06 14 11 0?}
/**
17 ldc.i4.1
8D 1? 00 00 01 newarr [mscorlib]System.Object
25 dup
16 ldc.i4.0
17 ldc.i4.1
8D 0? 00 00 01 newarr [mscorlib]System.String
A2 stelem.ref
13 01 stloc.s 1
38 CB FF FF FF br
**/
$func1_2 = {17 8D 1? 00 00 01 25 16 17 8D 0? 00 00 01 A2 13 01 38}
/**
7E 69 00 00 04 ldsfld unsigned int8[]
28 ?? 0? 00 0? call class [mscorlib]System.Reflection.Assembly
13 00 stloc.s 0
**/
$func1_3 = {7E ( 36 | 69 ) 00 00 04 ( 7E | 28 ) ?? 0? 00 0? ( 28 D? 04 00 06 13 00 | 13 00 )}
/**
38 00 00 00 00 br
00 nop
16 ldc.i4.0
28 ?? 0? 00 06 call class [mscorlib]System.Reflection.Assembly
28 ?? 01 00 06 call string
28 ?? 0? 00 06 call void
38 00 00 00 00 br
00 nop
00 nop
DD ?? ?? ?? ?? leave
38 ?? ?? ?? ??
**/
$func1_4 = {( 38 ( 00 00 00 00 | 00 00 00 00 00 ) 16 | 7E D3 01 00 04 ) 28 ?? 0? 00 06 ( 28 ?? 01 00 06 | 7E BA 02 00 04 ) 28 ?? 0? 00 06 38 00 00 00 00 ( DD | 00 DD | 00 00 DD ) ?? ?? ?? ?? 38}
condition:
dotnet.is_dotnet and all of them
}
Figure 3: YARA rule for stage 3 of the loader.
Payloads observed
We have tracked this loader's third stage for several years and analyzed its deployed payloads. The loader primarily distributes stealers, keyloggers, and RATs—with AgentTesla, Formbook, Remcos, and 404Keylogger being the most prevalent variants. The detailed statistics are shown in Figure 2.

All malware families shown in Figure 4 were distributed by the loader between March 2022 and February 2025.
In Figure 5, we analyze how frequently three new malware families appeared over time. We examined whether monitoring this loader could help detect new malware families early. While XWorm and NovaStealer appeared in this loader more than a year after their initial discovery, VIPKeylogger emerged simultaneously through this loader and in the wild—providing us early access to this new family.
We conclude that this loader's primary value lies in providing fresh samples and IOCs, rather than early detection of new malware families.
.png)
Appendix
Second stage loading example
In this section, we'll examine the second stage loading behavior from sample 8b25b0ed0e18bb24684d10bb3afccf6e6290c95e89a79733914117e2c7b46b09.
The main purpose of this stage is to decrypt and execute the third stage. Figure 6 shows the function responsible for invoking the decryption and execution routine, shown in Figure 7. While the exact invocation approach varies between samples, the characteristic usage of the three parameters remains consistent.
- The first parameter refers to the resource name in the first stage. It's passed as an integer but decoded to a 4-character string (
rEgh
). - The second parameter is the key for XOR-decoding the resource extracted from the first stage.
- The third parameter refers to the name of the first stage module loaded in memory (
Warehouse
).
public Form3()
{
this.InitializeComponent();
Type t = this.main; // see Figure 7
string[] args = new string[]
{
"72456768", // "rEgh"
"6F7566",
"Warehouse"
};
object O0O = AppDomain.CurrentDomain
.GetAssemblies()
.First(a => a.GetType("System.Activator") != null)
.GetType("System.Activator")
.GetMethods()
.First(m =>
m.Name == "CreateInstance" &&
m.GetParameters().Length == 2 &&
m.GetParameters()[0].ParameterType == typeof(Type))
.Invoke(null, new object[] { t, args });
}
Figure 6: Invocation of the decryption and loading functionality. Variable, method and class names were renamed for clarity.
Figure 7 shows the 'main' function of the second stage, which uses the first and third parameters to extract the bitmap resource from the first stage. The second parameter is then used to XOR-decode this resource, producing the .NET assembly code of the third stage. Finally, this assembly is loaded into memory and executed.
public static void main(string ResourceName, string XORKey, string ModuleName)
{
MainForm.loops_1(); // loops and arithmetic operations
MainForm.sleep(); // sleeps several seconds
ResourceName = MainForm.decode_string(ResourceName);
MainForm.loops_2(); // loops and arithmetic operations
XORKey = MainForm.decode_string(XORKey);
Bitmap bitmap = MainForm.get_resource(ResourceName, ModuleName);
MainForm.loops_1(); // loops and arithmetic operations
byte[] array = MainForm.convert_bitmap_to_array(bitmap);
array = MainForm.decrypt_array(array, XORKey);
Assembly assembly = MainForm.assembly_load(array);
MainForm.get_type_20_method_29_and_invoke(assembly); // obtains method to be invoked and calls it
MainForm.loops_2(); // loops and arithmetic operations
MainForm.exit();
}
Figure 7: Main function of the second stage responsible for loading the third stage. Variable, method and class names were renamed for clarity.
Function names inspired by computer games
The developers named functions after popular games like Fruit Ninja, Monster Hunter, and Dungeons & Dragons when implementing the second-stage deployment functionality, as shown in Figure 8. These distinctive function names could serve as additional IOCs to track the loader.
