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.
Figure 1: Loader stages.

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.

Figure 4: Relative frequency of families dropped by the loader over the observation period from March 2022 to February 2025.

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.

Figure 5: The frequency of NovaStealer, XWorm, and VIPKeylogger over the observation period.

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.

Figure 8: Second stage function names inspired by gaming franchises (Fruit Ninja, Monster Hunter, and Dungeons & Dragons).

IOCs

Stage 1 Payload Location Hash Initial Sample Hash Stage 3 (extracted from memory) Final Payload Family
Bitmap Resource 2a3ef660bc5ddec834f1f6473e07d4a2581dd0139d6f84742a1c2e9b5fd4561b 873eb1535c73bab017c8e351443519d576761c759884ea95e32d3ed26173fddc RedLineStealer
Bitmap Resource 609bc44c18519741abb62259b700403e05cc0fd57b972ef68ca6ae8194d27f2a 052efeadeb1533936df0a1656b6f2f59f47ef10698274356e3231099f87427c4 AgentTesla
Bitmap Resource 6ced7485ee8e4bb2aa919984473fed8a6c9201b29dbd1930d41126521524483e 063ca3294442e1194f637e02186e9682f3872c59e6247b8a8c759e9cba936669 DarkCloudStealer
Bitmap Resource 81ccf158093718305b3499d0f16d8a82bcad69f2740066daca8d5b5ca9979688 d3987a5d9cb294e7cc7990c9a45b2a080dc99aa7b61fc4c9e437fc4659effda7 Remcos
Hardcoded Data d81a0fe47c7cc9fdba1c13c2aa4f0372579f4c9ac51e16b7384da4b19c7c26a0 7532336b3fb752a7fa95aa1da5ddc527600d0cbba1aa2d77b46052439a32e619 Remcos
Bitmap Resource 51c95e12d8dcab7607fd6d5a2bbd4d524ebf7797e6857d6ec25f257c67d9b465 685478424a00d7690aad5768bf08e9a61f335dae5706eebf23e612b6d2cacdf8 Remcos
Bitmap Resource 26a36920e7a463398a4251828ec02fd965ad1d782f819b0c04904706efb083be f6ae4366b5e0ae5e46c9c1ec6045cdfec80fed0e3292f3275a74f81800109d42 Remcos
Bitmap Resource 8b25b0ed0e18bb24684d10bb3afccf6e6290c95e89a79733914117e2c7b46b09 67834ed25fdfb709729e96fe948b4ada4a306e7aabb74f0e88e026fcf8b5a7af VIPKeylogger
Bitmap Resource 98a11f5e0943f5a8e0475b4c7d87b5f67a1d39f7c44e564d86df4ebd687686f8 55dcc1f79972364f36ff76259502e5736c803fed67be7e586f51805a2471d7a8 Formbook
Bitmap Resource 342a6c5178eacedd99cd66da3bd81e767d0aa311441ad2089b087def3f5cb088 8ff3c42d9d0af296c0b6406bc8ac4253c938396c2fc5a33c7b8c8f47212eee8d VIPKeylogger
Hardcoded Data 5631b2c6aa5495d9756f92501442b809e0f004d9fe2c1d423ef8906ca912c69b d6e90b0ada45774227c6e3b6b1d14303188312b1e7dbf0b2a09f909fdf41dac9 404Keylogger

Ready to find out how Threatray can protect your organization?

Talk to an expert