Anatomy of a Multi-Stage .NET Loader: Deception, Embedded Assemblies, and Environment-Aware String Obfuscation

Author: Danyal Rana

SHA256: 89f6c08fa045d14eb73a0992145a724bc34d531d7af3dcc4901a1b7c7833aede

MD5: 3d639a627186e77730ff2d1294df49a3

Executive Summary

This analysis examines a multi-stage Windows malware sample that initially presents itself as a benign application through a combination of PDF-style iconography, misleading application metadata, and fake Paint/autosave functionality.

Initial static analysis suggested an ordinary .NET application containing references to AutosavePaint, WPF resources, backup files, and autosave functionality. That facade quickly fell apart.

The first stage performs a 33-second execution delay, extracts data from an embedded serialized System.Drawing.Bitmap, reconstructs a byte stream from RGB pixel data, applies an XOR transformation, decompresses the result, and reflectively loads the resulting .NET assembly with Assembly.Load().

The extracted second-stage assembly identifies itself internally as Lazarus. A YARA rule independently flags the sample as Loki, although neither identifier is treated here as definitive attribution.

The second stage introduces substantially more complex anti-analysis and obfuscation mechanisms, including:

  • Invisible Unicode identifiers
  • Runtime-generated manifest resource names
  • Integer-indexed string caching
  • Custom XOR transformations
  • A custom LZ-style decompressor
  • Assembly/environment fingerprinting
  • Caller and stack validation
  • Runtime-dependent execution branches
  • Process manipulation and native interop-related structures
  • Stateful string decoding whose output changes when expected execution context is absent

At the time of publication, analysis has reached the internal string-decoding subsystem and its encrypted resource table. The final payload has not yet been fully recovered, so conclusions are limited to the behavior observed in the analyzed stages.


1. Initial Triage

The initial executable was deliberately presented with a PDF-style icon, providing an immediate social-engineering layer.

PE resource inspection confirmed that the executable contains a native Windows RT_GROUP_ICON resource referencing three icon variants:

 
16 × 16
32 × 32
48 × 48
 

Each is a 32-bit icon resource.

The icon was only the beginning of the deception.

A string and metadata sweep revealed numerous references to:

 
AutosavePaint
PaintAppBackup.bk
Autosave
AutoSave Paint
Sheet1
 

along with user-facing strings such as:

 
Do you wish to load the last auto saved design?
Backup design has been loaded.
Kindly save the design to some location to avoid data loss.
Error loading the backup design
 

The application therefore attempts to present itself as a legitimate Paint/autosave utility.

It is not.


2. Stage One: A Benign Application Facade

The first-stage executable is a 32-bit .NET PE.

Its code contains a small but important routine responsible for reconstructing the next stage.

The loader begins with:

 
Thread.Sleep(33000);
 

The 33-second delay is significant because it introduces a basic form of sandbox and automated-analysis friction. Environments with short execution windows may terminate before the interesting behavior occurs.

Following the delay, the loader performs several transformations:

 
ResourceManager
      ↓
Bitmap extraction
      ↓
RGB traversal
      ↓
XOR transformation
      ↓
GZIP decompression
      ↓
raw .NET assembly
      ↓
Assembly.Load()
      ↓
EntryPoint.Invoke()
 

The assembly is loaded entirely through managed reflection:

 
Assembly.Load(rawAssembly)
    .EntryPoint.Invoke(...)
 

The first stage therefore acts primarily as a loader and concealment layer rather than the final malicious component.


3. Payload Concealment in a Serialized Bitmap

One of the most interesting aspects of the first stage is the use of a serialized bitmap as a carrier.

A resource named NGJiz was extracted from the first payload. The resource is a serialized .NET Bitmap rather than a conventional executable blob.

The bitmap was successfully reconstructed under Mono using a restricted analysis utility.

Result:

 
Dimensions   : 209 × 209
Pixel format : Format32bppArgb
 

The malware’s bitmap extraction routine traverses the image pixel-by-pixel and appends RGB components from non-transparent pixels:

 
Color pixel = bitmap.GetPixel(i, j);

if (pixel != Color.FromArgb(0, 0, 0, 0))
{
    list.Add(pixel.R);
    list.Add(pixel.G);
    list.Add(pixel.B);
}
 

This yielded:

 
Extracted RGB bytes: 106617
 

The resulting data is transformed using a derived XOR byte:

 
XOR byte: 0x70
 

After transformation, the stream is gzip-compressed data.

The reconstructed output was:

 
Size   : 138240 bytes
Type   : PE32 .NET executable
 

SHA-256:

 
b293342e115dc3903b73459d45d1b958d83c604c271479b975bf22049ecb2466
 

This represents the transition from the deceptive first-stage executable into the substantially more complex second stage.


4. Stage Two: Lazarus

The recovered assembly identifies itself as:

 
Assembly: Lazarus
Version : 0.0.0.0
 

It is a 32-bit .NET executable and contains three manifest resources:

 
Lazarus.Properties.Resources.resources
yBMsd.resources
<Unicode resource name>
 

A YARA rule independently flags the sample as Loki.

These observations should not be conflated with attribution. The assembly name is self-reported metadata, while the YARA result is a detection. Neither alone establishes an actor relationship.

The interesting part is what happens after the loader handoff.


5. Unicode Obfuscation

The second stage makes normal static analysis considerably more painful through aggressive identifier obfuscation.

Type and member names are composed of combinations of invisible and zero-width Unicode characters.

The resulting metadata contains names resembling:

 
 +              ​...
 

This creates several practical problems.

Traditional source-level navigation becomes unreliable.

Simple searches for meaningful class names become useless.

Decompiler output becomes difficult to reason about.

In several cases, ILSpy’s reconstructed C# was sufficiently distorted that the resulting source could no longer be treated as the authoritative representation of the original control flow.

The analysis therefore moved downward:

 
Decompiler
    ↓
Metadata tokens
    ↓
Raw IL
    ↓
CLR reflection
    ↓
Individual method/field resolution
 

This became essential for reconstructing the string engine.


6. The String Cache

The second stage does not decrypt every string from scratch.

Instead, it implements a custom integer-indexed cache.

The cache object contains an array of entries and a count:

 
TypeDef 0x0200001A

0x04000038 → entry[]
0x04000039 → count
 

Each entry is a value type containing:

 
Int32
String
 

Specifically:

 
0x0400003A → integer identifier
0x0400003B → decoded string
 

The string lookup routine is:

 
string Lookup(int id)
 

and scans the entry array for a matching identifier.

On a miss, the wrapper calls the considerably larger decoder:

 
0x06000054
 

which eventually inserts the decoded result back into the cache.

The architecture is therefore effectively:

 
String ID
   ↓
cache lookup
   ├── HIT → return cached string
   └── MISS
         ↓
      decode
         ↓
      cache result
         ↓
      return plaintext
 

This is a useful design choice for both performance and obfuscation: the expensive decoder only has to run once per string identifier.


7. Runtime-Generated Resource Names

The string decoder does not simply reference an obvious resource name.

Instead, it constructs the resource name at runtime using arithmetic operations and StringBuilder.

The resulting manifest resource name consists almost entirely of invisible Unicode characters:

 
U+2006 U+2002 U+2008 U+2000
U+2007 U+2004 U+2004 U+200B U+200A
 

The runtime then calls:

 
GetManifestResourceStream(...)
 

against that generated name.

The actual resource was identified and extracted directly from the assembly.

Size:

 
3349 bytes
 

SHA-256:

 
bd1dbc31ebd4f3457b9049e913e6ecaedf98cc29e0bf1e50561d1ac0f326fcbc
 

This provides a concrete example of the malware hiding the storage location and the data format simultaneously.


8. The Encrypted String Resource

The resource begins with a 16-byte header:

 
fe 3f 40 53 f6 c2 9d 36
ec 59 ec 6f 85 cc 0b 90
 

The stage contains a helper that treats this 16-byte value as an XOR key.

The remainder of the resource is transformed cyclically:

 
body[i] ^= key[i % 16];
 

This produces:

 
Original : 3349 bytes
Decoded  : 3333 bytes
 

SHA-256 of the transformed data:

 
576f72b12fc83105b543dbee0adbed1b27445147932223ce88f7db3fe2f85818
 

The resulting data does not correspond to a standard container such as PE, ZIP, or GZIP.

It is a custom binary structure consumed by the stage’s internal decoder.

This means the 16-byte XOR is only one layer of the string-decoding pipeline.


9. Environment and Caller Validation

One of the more interesting aspects of the sample is that it does not merely defend its payload through static obfuscation.

It also attempts to determine who is executing its code.

Multiple routines use combinations of:

 
Assembly.GetExecutingAssembly()
Assembly.GetCallingAssembly()
StackTrace
StackFrame.GetMethod()
MethodBase.DeclaringType
RuntimeMethodHandle
 

The code performs checks against the current execution context and modifies internal state depending on what it finds.

This becomes particularly important when analyzing internal methods directly.

For example, invoking one internal state routine through a reflection harness produced:

 
2918384
 

That value initially appeared valid.

It wasn’t.

The function had taken a fallback path because the calling context did not match the conditions expected by the malware.

In other words, the sample isn’t merely asking:

“Can I decode this?”

It is also asking:

“Are you the code that is supposed to be asking?”

That behavior makes naive reflective extraction surprisingly unreliable.


10. Assembly / Runtime Fingerprinting

The stage contains a custom fingerprinting routine that incorporates information derived from the running assembly and its metadata.

The recovered logic incorporates values related to:

 
Assembly identity
Assembly name
Public key token
Metadata tokens
Calling context
 

and performs a custom 64-bit mixing routine.

The analysis reproduced the following fingerprint:

 
0x6F78EF8B027F45C5
 

The low 32 bits of this value are then fed back into the string decoder as part of its state derivation.

This is a significant architectural detail.

The strings are therefore not simply protected by a static key.

Part of the decoder state depends on the environment in which the assembly believes it is executing.


11. Stateful String Decoding

The string decoder uses several derived integer states.

Among the recovered intermediate values are:

 
num11 = 0x1CE44278
num10 = 0x1AB5EEB1
num15 = 0x00808D2D
num16 = 0x7C2E0C12
 

The decoder also contains an internal iterator/state machine which produces a sequence of derived values before those values are fed through additional arithmetic and XOR operations.

The important point is not the individual constants.

It is the dependency chain:

 
Assembly/runtime state
        ↓
fingerprint
        ↓
decoder seed
        ↓
iterator
        ↓
state variables
        ↓
resource lookup
        ↓
record decoding
        ↓
optional decompression
        ↓
string
 

A single incorrect input causes the entire downstream result to become invalid.

This is why initial attempts to reconstruct the decoder produced apparently reasonable but impossible resource offsets.

The malware is effectively creating a stateful decoding environment, not just decrypting a blob with one hard-coded key.


12. Custom LZ-Style Decompression

The stage also contains its own decompression routine.

The decoder reads a control bit for each output operation.

A 0 control bit represents a literal byte.

A 1 control bit represents a back-reference containing:

 
length = high 6 bits + 3
distance = 10-bit value
 

The output is reconstructed by copying previously generated bytes from the destination buffer.

This is a small custom LZ-style codec rather than a standard Deflate implementation.

The presence of this codec adds another barrier for generic tooling: even after the record has been recovered and transformed, standard archive/carving tools are unlikely to identify the final data automatically.


13. Native Interop and Process-Oriented Functionality

Deeper portions of the second-stage assembly include structures based around IntPtr, process handles, memory-related operations, and process management.

The code also contains native-interaction infrastructure and branches which operate differently depending on pointer size and execution environment.

These sections have not been fully reconstructed yet, so this report deliberately avoids attributing a specific Windows API or native technique without a verified import/call mapping.

What can be established so far is that the stage is not purely a string-decoder toy: it contains functionality designed to interact with the host process and operating environment.


14. Anti-Analysis Architecture

The individual techniques used by this sample are not particularly novel.

The interesting part is their combination.

A simplified view is:

 
PDF-style icon
      ↓
Fake Paint application
      ↓
33-second delay
      ↓
Serialized Bitmap
      ↓
RGB extraction
      ↓
XOR
      ↓
GZIP
      ↓
Second .NET stage
      ↓
Unicode obfuscation
      ↓
Caller/environment validation
      ↓
Assembly fingerprint
      ↓
Integer-indexed string cache
      ↓
Generated Unicode resource name
      ↓
Encrypted custom binary table
      ↓
Custom record decoding
      ↓
Custom LZ decompression
      ↓
Next-stage data
 

Each individual layer is manageable.

The problem is that each layer depends on the previous one being exactly correct.

That creates an effective multiplicative cost for analysis.


15. Indicators and Artifacts

Initial executable

 
Filename: intentionally omitted
SHA-256:
89f6c08fa045d14eb73a0992145a724bc34d531d7af3dcc4901a1b7c7833aede
 

Extracted intermediate .NET payload

 
Size: 6656 bytes

SHA-256:
8cc8f6d82d589059867f2e321594d59b97759027b508b8d3640ecc8b838a513d
 

Stage 3

 
Size: 138240 bytes

SHA-256:
b293342e115dc3903b73459d45d1b958d83c604c271479b975bf22049ecb2466
 

String-decoder resource

 
Size: 3349 bytes

SHA-256:
bd1dbc31ebd4f3457b9049e913e6ecaedf98cc29e0bf1e50561d1ac0f326fcbc
 

Post-XOR string resource

 
Size: 3333 bytes

SHA-256:
576f72b12fc83105b543dbee0adbed1b27445147932223ce88f7db3fe2f85818
 

Observed identifiers

 
Assembly name:
Lazarus

YARA:
Loki rule match

Manifest resource:
Lazarus.Properties.Resources.resources
yBMsd.resources
Unicode-named resource
 

16. Current Assessment

The analyzed sample is a multi-stage .NET loader employing layered deception, embedded payload storage, runtime-aware obfuscation, custom string decoding, and anti-analysis logic.

The first stage is relatively straightforward once the embedded bitmap mechanism is identified.

The second stage is significantly more sophisticated.

Its most notable characteristic is not the use of advanced cryptography, but the deliberate coupling of:

  • Execution context
  • Assembly identity
  • Metadata
  • Caller information
  • Runtime state
  • Obfuscated resource naming
  • Stateful string decoding

This means conventional static analysis can repeatedly produce plausible but incorrect results unless the analyst validates each transformation against the original IL/runtime behavior.

The sample therefore represents a good example of a broader malware-analysis lesson:

Obfuscation does not have to make code unreadable. It only needs to make your assumptions unreliable.

At the time of this report, the analysis has reached the custom string-decoding resource and its associated state machine. The final payload and complete behavioral capability set remain under investigation.

And, unfortunately, the sample is still sitting there like:

“Are you done?”

No.

Not remotely. 💀


Analyst Notes

A few conclusions have intentionally not been made here.

The internal assembly name Lazarus has not been treated as proof of Lazarus Group attribution. Likewise, the YARA Loki match is treated as a detection rather than definitive family attribution.

Likewise, native interop and process-manipulation capabilities are described only to the extent supported by the recovered code; exact API-level behavior remains subject to further reconstruction.

That distinction matters. The deeper the analysis gets, the more tempting it becomes to fill gaps with what the malware probably does.

That’s precisely the trap this sample is designed to create.