Author: Danyal Rana
SHA256: 89f6c08fa045d14eb73a0992145a724bc34d531d7af3dcc4901a1b7c7833aede
MD5: 3d639a627186e77730ff2d1294df49a3
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:
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.
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 × 48Each 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
Sheet1along 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 designThe application therefore attempts to present itself as a legitimate Paint/autosave utility.
It is not.
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.
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 : Format32bppArgbThe 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: 106617The resulting data is transformed using a derived XOR byte:
XOR byte: 0x70After transformation, the stream is gzip-compressed data.
The reconstructed output was:
Size : 138240 bytes
Type : PE32 .NET executableSHA-256:
b293342e115dc3903b73459d45d1b958d83c604c271479b975bf22049ecb2466This represents the transition from the deceptive first-stage executable into the substantially more complex second stage.
LazarusThe recovered assembly identifies itself as:
Assembly: Lazarus
Version : 0.0.0.0It 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.
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 resolutionThis became essential for reconstructing the string engine.
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 → countEach entry is a value type containing:
Int32
StringSpecifically:
0x0400003A → integer identifier
0x0400003B → decoded stringThe 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:
0x06000054which 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 plaintextThis is a useful design choice for both performance and obfuscation: the expensive decoder only has to run once per string identifier.
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+200AThe runtime then calls:
GetManifestResourceStream(...)against that generated name.
The actual resource was identified and extracted directly from the assembly.
Size:
3349 bytesSHA-256:
bd1dbc31ebd4f3457b9049e913e6ecaedf98cc29e0bf1e50561d1ac0f326fcbcThis provides a concrete example of the malware hiding the storage location and the data format simultaneously.
The resource begins with a 16-byte header:
fe 3f 40 53 f6 c2 9d 36
ec 59 ec 6f 85 cc 0b 90The 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 bytesSHA-256 of the transformed data:
576f72b12fc83105b543dbee0adbed1b27445147932223ce88f7db3fe2f85818The 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.
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
RuntimeMethodHandleThe 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:
2918384That 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.
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 contextand performs a custom 64-bit mixing routine.
The analysis reproduced the following fingerprint:
0x6F78EF8B027F45C5The 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.
The string decoder uses several derived integer states.
Among the recovered intermediate values are:
num11 = 0x1CE44278
num10 = 0x1AB5EEB1
num15 = 0x00808D2D
num16 = 0x7C2E0C12The 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
↓
stringA 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.
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 valueThe 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.
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.
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 dataEach 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.
Filename: intentionally omitted
SHA-256:
89f6c08fa045d14eb73a0992145a724bc34d531d7af3dcc4901a1b7c7833aedeSize: 6656 bytes
SHA-256:
8cc8f6d82d589059867f2e321594d59b97759027b508b8d3640ecc8b838a513dSize: 138240 bytes
SHA-256:
b293342e115dc3903b73459d45d1b958d83c604c271479b975bf22049ecb2466Size: 3349 bytes
SHA-256:
bd1dbc31ebd4f3457b9049e913e6ecaedf98cc29e0bf1e50561d1ac0f326fcbcSize: 3333 bytes
SHA-256:
576f72b12fc83105b543dbee0adbed1b27445147932223ce88f7db3fe2f85818Assembly name:
Lazarus
YARA:
Loki rule match
Manifest resource:
Lazarus.Properties.Resources.resources
yBMsd.resources
Unicode-named resourceThe 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:
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. 💀
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.