Introduction

In the last stage of our analysis we uncovered yet another layer in this malware’s multi-stage loading process. By this point, the pattern is clear: each step we peel back only serves to reveal a new mechanism designed to frustrate static inspection. Now, as we enter the eighth stage, we find ourselves facing a loader that takes full advantage of .NET’s flexibility.

Unlike the earlier layers that relied on simple obfuscation and resource embedding, this stage demonstrates a more advanced approach dynamically loading assemblies directly from memory, decrypting them on the fly, and invoking methods without ever touching disk. This significantly raises the bar for detection and complicates traditional static analysis.

What follows is a closer look at this executable, its obfuscation techniques, and how we pivoted from static reverse engineering into a more dynamic approach to uncover its hidden payloads.


In the previous part we extracted an executable from another executable hidden through Base64 encoding and XOR obfuscation and now, looking at that in more detail, we arrive at the 8th stage.

image

  • SHA256: 06FC70AA08756A752546198CEB9770068A2776C5B898E5FF24AF9ED4A823FD9D
  • Original filename: maegkffm.exe
  • Detection: No hits on VirusTotal — appears to be previously unknown.

Running FLOSS against the binary yields a variety of strings pointing to encryption routines and injection functionality. Unlike previous stages, however, there are no immediate plaintext artefacts or “quick wins” to accelerate analysis. This suggests the malware authors have deliberately shifted to stronger concealment techniques at this point in the chain, forcing us to pivot towards deeper static and dynamic examination.

image

As this stage is a .NET executable, we can load it into dnSpy to take a closer look. Right-clicking on the assembly and selecting Go to Entry Point drops us into the method defined under: public static class \u0008\u2009

image

Clicking on \u0006\u2009 lets us follow the control flow and inspect its code.

image

This appears to be the loader. Stepping through it, we can unpack each call:

Load Bytes from Obfuscated Source

byte[] array = \u000E\u2009.\u0003();
  • Calls a method \u0003() on the class \u000E\u2009 (both obfuscated).
  • It returns a byte[], presumably representing a compiled .NET assembly.

Validate the Byte Array

if (array == null || array.Length == 0) { 	throw new InvalidOperationException(); }
  • Defensive check: ensures the byte array isn’t null or empty.

Load the Assembly from Memory

Assembly assembly = Assembly.Load(array);
  • Dynamically loads the byte array as a .NET assembly in memory.
  • Nothing is written to disk makes it harder to detect or analyse statically.

Get a Type from the Assembly

Type type = assembly.GetType(\u0003\u0019.\u0003(1682298672));
  • \u0003\u0019.\u0003(1682298672) is likely a a string resolver or decryptor.
  • It’s used to get the full name of a type in the loaded assembly.

Get a Method from That Type

MethodInfo method = type.GetMethod(\u0003\u0019.\u0003(1682298624), Type.EmptyTypes);
  • Again uses the string resolver to get the method name.

Call the Method via Delegate

((Action)Delegate.CreateDelegate(typeof(Action), method))();
  • Wraps the target method in an Action delegate and invokes it.
  • Functionally equivalent to calling the method, but adds another layer of indirection to hinder straightforward decompilation and string matching.

Lets follow byte[] array = \u000E\u2009.\u0003(); to see if we can find the assembly.

image image image

At a high level, the data’s path is: embedded_bytes → AES decrypt → GZip inflate → managed Assembly.Load()

Breaking it down:

Load Bytes

private static byte[] ()

This method returns an encrypted, compressed byte array.

  • Contains a hardcoded byte[] literal that represents the embedded payload:
return new byte[] { 63, 207, 118, 142, 197, 124, 32, ... };

Decrypt Bytes

private static byte[] (byte[] encrypted)

This method decrypts the byte array returned from () using AES-256 in CBC mode.

  • Uses Aes.Create() with:

  • aes.KeySize = 256;

  • aes.Key = Convert.FromBase64String(...)

  • aes.IV = Convert.FromBase64String(...)

  • These values are dynamically retrieved from an obfuscated string resolver:

\u0003\u0019.\u0003(1682298750) // Base64-encoded AES key  
\u0003\u0019.\u0003(1682298697) // Base64-encoded IV
  • Sets up a decryption stream:
CryptoStream cryptoStream = new CryptoStream(
    new MemoryStream(encrypted),
    aes.CreateDecryptor(aes.Key, aes.IV),
    CryptoStreamMode.Read
);
cryptoStream.CopyTo(outputStream);
  • Returns the decrypted result as a byte[] which is still GZip-compressed.

Inflate Bytes

private static byte[] (byte[] compressed)

This method decompresses the GZip-compressed byte array returned from the decryption step.

  • Uses GZipStream to decompress the input:
GZipStream gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress);

gzipStream.CopyTo(memoryStream2);
  • Returns the fully decrypted and decompressed byte array, ready to be loading into memory by the previous method.

We have the data and the encryption routine now we just need the key and IV, clicking on \u0003\u0019.\u0003 to follow it we see how the keys are generated.

image

The AES key and IV are generated and deobfuscated at runtime. It is buried under layers of junk logic and opaque helper calls, which makes static inspection tedious and low value. That section alone spans ~668 lines, and most of it appears to exist purely to waste analysis time by constructing variables and branches that are never used.

At this point, continuing statically would be a poor use of my time. We will pivot to dynamic analysis. Since this stage behaves like a packer or loader, the fastest path forward is to let it run, then dump the decrypted, decompressed payload from memory after the loader has done the hard work for us.


Why Static?

I prefer to stick with static analysis for as long as possible. It gives me a deeper understanding of how the malware operates and more importantly, how the threat actor thinks and writes. This approach helps uncover what capabilities we might expect to see, and if we’re lucky, we might even catch mistakes the threat actor made, things we could easily miss with dynamic analysis alone.

But in this case, the time cost isn’t worth the insight. It’s time to move forward.


Dynamic Analysis

The typical approach here would be to run the malware inside a debugger and set a breakpoint right after it loads the payload into memory, but before it invokes or executes it. That way, you can catch the decrypted, unpacked binary at rest and dump it cleanly.

However, we’re going to cheat a little and use ExtremeDumper, a tool purpose-built to make memory dumping from .NET binaries much easier. It saves us a lot of time and lets us focus on analysing the payload, not reinventing the wheel.

Running both maegkffm.exe and ExtremeDumper.exeas administrator we see the option to dump selected process.

image The output of ExtremeDumper shows it found something image

Hashing the dump set shows the .exe matches our original sample, so we can ignore it. The interesting part is Mhgljosy.dll appearing twice with different hashes.

image

To understand why the two DLLs differ, I loaded both into HxD and used the built-in comparison feature:

Analysis → Data comparison → Compare (or simply hit Ctrl + K).

image

When comparing the two files in HxD, we notice that differences start appearing around offset 0x000A0000. Interestingly, the file on the left appears to be malformed, we can see the magic bytes and DOS stub message (e.g. “This program cannot be run in DOS mode”) showing up partway through the file, rather than at the beginning as expected in a valid PE (Portable Executable) format.

This strongly suggests that the left file is either partially overwritten, misaligned, or was not dumped correctly.

For the purposes of this analysis, we’ll proceed with the file on the right, as it appears to be the more structurally valid of the two.


FIle Name: Mhgljosy.dll SHA265: e0e724c40dd350c67f9840d29fdb54282f1b24471c5d6abb1dca3584d8bac0aa Detection: No hits on VirusTotal — appears to be previously unknown.

Running FLOSS doesn’t yield much in the way of useful strings, which immediately hints at obfuscation or packing.

image

Confirming this, Detect It Easy (DiE) flags the DLL as being protected by .NET Reactor a commercial protector commonly abused by malware authors to frustrate analysis. .NET Reactor typically.


Pivoting from EXE to DLL

At this point in the analysis, we’re shifting focus from a traditional PE executable (.exe) to a DLL (.dll). While both share the same underlying PE (Portable Executable) format, analysing a DLL introduces a few key differences:

  • No standard entry point — instead, execution typically begins via exported functions, or sometimes when the DLL is loaded (e.g. DllMain).
  • More context-dependent — DLLs are often designed to be loaded by a specific application or loader. As a result, some functionality may not initialise properly in isolation.
  • May behave passively — if not invoked correctly, a DLL may appear inert during static or dynamic analysis.

Because of this, we’ll need to adjust our approach, focusing on exported functions, examining DllMain, and possibly simulating or reconstructing the loading environment to trigger the behaviour we’re trying to observe.

But interestingly, when we inspect the sample in PEStudio, we see that it has no exported functions: image

Commonly, malware leverages an exported function (e.g. RunPayload) that gets explicitly called by the loader or dropper. However, this sample is doing something different.

Before diving deeper, let’s quickly confirm that this is in fact a DLL using CFF Explorer it wouldn’t be the first time a misleading file extension was used to confuse an analyst.

image

Everything checks out, this is a legitimate DLL. So what’s going on?

Even when a DLL exposes no exports, it still has a built-in entry point: DllMain(). While not a typical function you would call directly, the Windows loader automatically executes it, and threat actors can exploit this behaviour.


DLL Entry Point: DllMain()

A DLL doesn’t have a traditional entry point like an executable’s. Instead, it provides an entry routine usually DllMain() that the Windows loader automatically triggers during key lifecycle events:

  • When the DLL is loaded (DLL_PROCESS_ATTACH)
  • When a thread is created (DLL_THREAD_ATTACH)
  • When a thread ends (DLL_THREAD_DETACH)
  • When the DLL is unloaded (DLL_PROCESS_DETACH)

This design means code can still run automatically as soon as the DLL is loaded, even without exports. Malware authors can abuse this by embedding payloads inside the DLL_PROCESS_ATTACH case, ensuring execution without needing a visible entry point like a standard executable.


.NET DLLs are weird.

Although that’s not the case here either, as this is a .NET DLL, so the traditional DllMain() behaviour doesn’t apply. In .NET assemblies, the .NET runtime handles the loading, and the actual execution flow usually starts elsewhere.

Since this sample is .NET-based, let’s load it into dnSpy. Normally with .NET executables, we can jump straight to the Main() method or an obvious export. But here, we’re dealing with a DLL that has:

  • No exports
  • No Main method
  • No clear entry point

So where’s the execution logic hiding?


Going Back to the Loader

Let’s pause and retrace our steps. Looking back at the loader, remember in \u0006\u2009 where it appeared to decrypt or decode a method at runtime, then invoke it through a delegate? I think that might be our method it was sitting there the whole time, but we got sidetracked going down the path of traditional dll analysis.

Why does this work? **.NET assemblies are a Special case:

  • In our sample, both the EXE and the DLL are .NET assemblies, so things are a little different.
  • The loader (Assembly.Load(bytes)) pulls a DLL into memory directly.
  • Instead of needing exports, the EXE asks the .NET runtime for a Type and MethodInfo, then uses reflection (GetMethod, Delegate.CreateDelegate) to invoke it.
  • This means the DLL doesn’t even need to have “traditional” exports like DllMain or ExportedFunction. As long as the .NET runtime can find the class and method, it can be executed.

This is exactly why static analysis matters: if we had only dumped this DLL from memory, we’d have the payload but no real context for where or how it was invoked. Our earlier Static work gives us that insight.

With that in mind, let’s pivot back to the loader executable and try a feature of dnSpy we haven’t used yet. Since both the type and method are only revealed at runtime, we can take advantage of dnSpy’s debugging capabilities to observe this process directly. To start, we’ll set a breakpoint right at the program’s entry point, then run it.

image Next, we can track down the delegate we identified earlier and set an additional breakpoint there. Running the program up to that point reveals the local variable being assigned to the method:

image

Mhgljosy.Formatting.TransferableFormatter.SelectFormatter()

This confirms that the loader is dynamically resolving and invoking this function at runtime.

Back in the DLL, searching for SelectFormatter quickly brings us to its definition, where we can start breaking down its behaviour.

image

Although after stepping through various methods and functions, it’s clear we won’t get much further — the code is heavily obfuscated by .NET Reactor, as noted earlier. With that, I’m going to wrap up this part, but first, a quick recap.


Recap Time: What Did We Find in Stage 8 and 9

What began as yet another obfuscated executable peeled back into a secondary .NET loader, one crafted not just to frustrate the analyst, but to deliberately shift analysis towards an unusual execution of DLLs.

This stage introduced:

  • A memory-only execution chain:
    AES-256 decryption → GZip inflation → Assembly.Load()
  • Hardcoded payload hidden in a byte array, never touching disk
  • Dynamic string resolution for runtime-only type names, method names, and AES key/IV values, all buried beneath hundreds of lines of junk logic
  • Reflection-based invocation via delegates, bypassing traditional DLL export functions
  • Dumped payload (Mhgljosy.dll) protected by .NET Reactor.

Each layer was carefully crafted to ensure the payload remained invisible to static tools like VirusTotal, FLOSS, or simple string scans, while forcing analysts to pivot into dynamic techniques such as in-memory dumping and debugger-assisted tracing.

This approach:

  • Raises the bar for detection and reverse engineering
  • Shows deliberate use of commercial protections in a malicious context
  • Suggests a modular, evolving loader–DLL architecture, where each stage acts as a gatekeeper for the next.

Up Next: Part 6 — Deep Dive into Mhgljosy.dll

We’ve reached the next stage and spoiler alert, it’s the final one. With dynamic tracing, we’ve pinned down the true entry point:

Mhgljosy.Formatting.TransferableFormatter.SelectFormatter()

So join me in Part 6 as I take on the challenge of deobfuscating .NET Reactor and finally uncover what this payload is really designed to do.