# Malware Write-up: Analyzing a PDF-Disguised Multi-Stage .NET Loader

> **Disclaimer:** This post describes malware analysis in an isolated lab. Do not run samples on a daily-use machine. Hashes and IOCs are shared for research and defense.

## Introduction

I am an IT professional with 15 years of experience across software development — mobile, backend, front-end — and I currently work as a Software Architect. Still, since I was a kid I have been drawn to hacking, and for years I have studied low-level topics focused on security. I decided to start something like a research logbook of those investigations. This article was written by me; because I am Brazilian, I used AI to help rewrite it in English.

In this entry I walk through a sample from [MalwareBazaar](https://bazaar.abuse.ch/). It is obviously malware — that is why it is a *sample*. What made it interesting was the **social-engineering packaging**: a **PDF-looking icon** and bait naming (`pi 2.exe`), while the file itself is a **.NET executable** that unpacks through several stages, persists on the host, and tries to talk to a list of domains.

* * *

## Lab

*   **Guest:** FlareVM (Windows)
    
*   **Network:** host-only + **FakeNet-NG** (no real internet)
    
*   **Tools:** PEStudio, dnSpy, ExtremeDumper, Procmon, Wireshark / FakeNet pcaps, x32dbg, Ghidra, FLOSS / strings
    
*   **Sample (SHA-256):**
    

```text
af86d6a54fc8fb39239878a6b6e5a8f558ca0bba1bc85af7d5e076a59be18f67
```

*(Internal bait name:* `pi 2.exe`*, module name like* `Rm4cgtB`*.)*

* * *

## 1\. First look: PE metadata and the PDF disguise

In PEStudio / DIE:

*   PE / **.NET** assembly (PDF icon as disguise; internal name `pi 2.exe`, module `Rm4cgtB`)
    
*   High entropy (**7.189**) — packed / encrypted / obfuscated territory
    
*   Early hypothesis: **obfuscated loader**, not the final payload
    

![PEStudio indicators: size 1203712 bytes, entropy 7.189, .NET module Rm4cgtB](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/033e458a-3d4a-4a68-9da0-7eb0539046df.png align="center")

I opened it in **dnSpy**. In the entry point I first hit a **string reference** to an embedded path — built as `string.Join("/", "resources", "nasode.pptx")`, then read, reversed, decrypted, and loaded via `"L o a d"`:

![dnSpy entry point: Capnogramat gate, resources/nasode.pptx, Array.Reverse, R_c3g6zT4Bt, Assembly.Load](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/717ec231-25dc-442d-acb9-3de551bb4772.png align="center")

That was the breadcrumb — not yet the file itself. Searching for `nasode` led to the assembly resources (`Rm4cgtB.g.resources` → `resources/nasode.pptx`, ~509 KB). I dumped that stream. The “PPTX” is only a disguise; the bytes are the encrypted stage-2 blob:

![dnSpy: Rm4cgtB.g.resources → resources/nasode.pptx](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/ff51254b-7027-4875-b152-e437c306b511.png align="center")

Static flow of the loader after that:

1.  Anti-analysis gate (`"Capnogramat"`) → silent `Environment.Exit(0)` on some paths
    
2.  Reads embedded resource `resources/nasode.pptx` (**fake PPTX**)
    
3.  `Array.Reverse` on the bytes
    
4.  Decrypt via an obfuscated method (`R_c3g6zT4Bt`) → **AES-128-CBC**, with **Key = IV**
    
5.  `Assembly.Load` on the result (name split as `"L o a d"` to dodge simple string search)
    

### AES (stage-2)

Following `R_c3g6zT4Bt` in dnSpy showed plain AES with **Key = IV**:

![dnSpy: Aes.Create, Key = IV, TransformFinalBlock](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/04775fdd-48a8-416d-ba4a-16e2f06fb78e.png align="center")

The key material lives in a static string array (hex nibbles):

![dnSpy: Key/IV bytes 8C 38 48 1E … F4](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/34135c3b-ba33-424e-bdcc-84c66f48490c.png align="center")

```text
8C 38 48 1E BC 29 E9 BE EE 46 05 0B 09 F2 63 F4
```

Result: `Foliferi.dll` (.NET, ~521 KB).

```text
SHA-256 (stage2_dec / Foliferi):
E7982DA1E11D310CBD2D168784CB6E1D6A4CC84D70F83F2D430C4F920BF9B07E
```

* * *

## 2\. Stage-2: Foliferi + .NET Reactor

Opening the decrypted stage-2 in PEStudio: `Foliferi.dll`, .NET 32-bit DLL, still high entropy (**7.629**), suspicious future compile stamp:

![PEStudio: Foliferi.dll, entropy 7.629](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/53c71c0e-500f-4109-b4eb-be107cf5a42a.png align="center")

`Foliferi.dll` was protected with **Eziriz .NET Reactor** (trial / unregistered):

*   anti-debug (`Debugger.IsAttached`)
    
*   init in `.cctor`
    
*   heavy obfuscation (empty-looking methods, control-flow flattening)
    

![dnSpy: Debugger.IsAttached → Debugger Detected](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/e7d92a6d-b1d4-44f0-a511-7c1b44553c9b.png align="center")

![dnSpy: Eziriz .NET Reactor unregistered / trial string](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/abf40cb2-49ad-4474-94da-684c452c4699.png align="center")

I tried **NETReactorSlayer** → unpack was **incomplete**. So I switched strategy: **dynamic dumping** (ExtremeDumper) while the process was running — Foliferi shows up as a loaded module:

![ExtremeDumper: Foliferi modules in the live process](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/b44334be-1970-4486-8875-d433110387e9.png align="center")

**Takeaway:** static unpack of the protector does not always close the case; a **runtime dump** often yields more readable modules.

* * *

## 3\. Host behavior: what the malware does on the box

With FakeNet + Procmon (and later a fuller CSV of the original loader):

### Drop + persistence

The loader does not only “write the Run key” from its own process — it spawns `cmd.exe` with a delay via `ping`:

1.  **Persistence**
    

```text
cmd /c ping 127.0.0.1 -n 35 > nul && REG ADD "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /f /v "PI-0023455" /t REG_SZ /d "...\Desktop\PI-0023455.exe"
```

2.  **Copy + execute**
    

```text
cmd /c ping 127.0.0.1 -n 48 > nul && copy "<sample>.exe" "...\Desktop\PI-0023455.exe" && ping ... && "...\Desktop\PI-0023455.exe"
```

So:

*   Drop: `Desktop\PI-0023455.exe` (same hash as the sample in this family)
    
*   Persistence: `HKCU\...\Run\PI-0023455`
    

In System Informer the living process is already the Desktop copy (PDF icon, parent often *Non-existent* because the original loader exited):

![System Informer: PI-0023455.exe on Desktop](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/b4834d74-9069-4bf6-83f2-5ab11791f6aa.png align="center")

Procmon / registry confirmation of the Run key:

![Procmon: traffic around HKCU Run / PI-0023455](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/21f0eb95-7147-402e-8e9c-2ec4b85f87c4.png align="center")

![PowerShell: HKCU Run PI-0023455 → DesktopI-0023455.exe](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/efecdd32-e4d8-409f-989f-6c955f29bfcf.png align="center")

If you filter Procmon only on `PI-0023455.exe`, it is easy to **miss** the `REG ADD`. The key is written by `cmd`.

### The Reactor trial date trap

After a few days, the process would start and die immediately. Going back to notes: the Reactor trial checks a date window (~`2026-08-10` ± 14 days). Outside that window, the sample aborts early.

Lab fix:

*   Disable VirtualBox guest time sync (`GetHostTimeDisabled`)
    
*   Pin the guest date (e.g. `2026-08-15`)
    

**Lesson:** if the sample “does nothing”, check date, packer trial windows, and `Process Exit` / CPU time in Procmon — “five minutes of capture” ≠ “five minutes of living malware”.

* * *

## 4\. Network (FakeNet / pcap)

With the correct date and a valid capture:

*   Many **DNS** queries for `www.*` hosts (C2 / panel candidates)
    
*   **TCP 80/443** attempts → under FakeNet often only **ICMP unreachable**
    
*   **No useful HTTP** in that session (no full handshake → no request body)
    

Wireshark on the FakeNet pcap — early C2 candidates resolving via `169.254.0.33`, then failed connects:

![Wireshark: DNS www.aravaeducraft.com / www.bierhenkel-immo.com](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/9f94b143-c743-4f68-9087-cdd26e6df553.png align="center")

![Wireshark: DNS www.mmsp532.top (and related www.* queries)](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/cb9968ad-b6d9-4fd7-b33c-9f32abcc6a69.png align="center")

### Domain IOCs (candidates)

Defanged for publishing:

```text
www[.]aravaeducraft[.]com
www[.]auninternational[.]com
www[.]bangaranga[.]fashion
www[.]bierhenkel-immo[.]com
www[.]brswigacademy[.]com
www[.]dogivia[.]com
www[.]genki-healthcare[.]com
www[.]klynker[.]com
www[.]lovinej[.]info
www[.]mmsp532[.]top
www[.]novamed-beauty[.]ru
www[.]pv16[.]xyz
www[.]slot-oyunu[.]top
www[.]uvgullasedu[.]com
www[.]vcfxzm[.]com
www[.]wakomatsumoto[.]com
```

Possible noise / other context: `www[.]bradesco[.]com[.]br`, `www[.]vrbo[.]com`, `update[.]googleapis[.]com`.

Public reputation (examples):

*   `slot-oyunu[.]top` — vendor detections (e.g. Kaspersky/Fortinet as malware on VT)
    
*   `mmsp*.top` cluster — disposable infra / suspicious redirects in OSINT
    

Those domains were **not** in cleartext in the stage-1 loader (not even simple XOR) — they fit a later stage / runtime list.

* * *

## 5\. Stage-3: `bgK2rzW…dll` and in-memory execution

ExtremeDumper produced a more readable module, roughly:

```text
bgK2rzW57itpC1szi9tD7KX13LJ.dll
```

In dnSpy it shows up as a **dynamic / in-memory** assembly:

![dnSpy: bgK2… module with Dynamic/InMemory = Yes](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/bd759734-e34d-4b60-9977-d001de287b28.png align="center")

Typical init: `ChainResponder` `.cctor` → `ResponderBridge.RespondMonoResponder` (huge method with **control-flow flattening**).

### Chain until native execution

Inside the unpacker (obfuscated names → real APIs):

| Step | API / idea |
| --- | --- |
| Decrypt | AES / `CryptoStream` |
| Allocate | `VirtualAlloc` RWX (`MEM_COMMIT` + `PAGE_EXECUTE_READWRITE` / `0x40`) |
| Copy | `Marshal.Copy` → RWX buffer |
| Prepare | `GetDelegateForFunctionPointer` |
| Run | static delegate (`_ResponderThread`) |

Broken API strings (`"Virtual " + "Alloc"`) = classic anti-`strings`.

### Native debugging: trampolines and the JIT trap

With dnSpy + x32dbg + Ghidra:

1.  Dump of an RWX stub → prologue `55 8B EC`
    
2.  Not the “final malware”: a **trampoline/selector** (`CMP` + `JMP EAX`)
    
3.  Hop chain → larger functions in the same RWX region
    
4.  Many `CALL`s landed in `clr` / `mscorlib.ni` / `clrjit`
    

Breakpoint on `VirtualAlloc` in x32dbg (process `PI-0023455`), including RWX (`flProtect = 0x40`):

![x32dbg: breakpoint on kernel32.VirtualAlloc](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/13e7fc2f-85bb-4f84-b4ee-3333ecf97e6c.png align="center")

![x32dbg: VirtualAlloc with PAGE_EXECUTE_READWRITE (0x40)](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/a978c786-d878-41c5-807a-1c4031bf89a6.png align="center")

Dump taken to Ghidra — listing of the native stub / hops:

![Ghidra: listing of dumped native stage](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/baa9f77a-d3b1-4d37-b695-d4a60e388526.png align="center")

Conclusion of the native phase: a lot of the “interesting” RWX code was **JIT-compiled .NET**, not a classic third PE hidden on disk. Endless hopping there has low ROI.

**Key lesson:** when disassembly mostly calls into the CLR and uses tokens like `0x2000…`, you are probably in JIT — go back to behavior, strings from the unpacked managed stage, or network APIs.

* * *

## 6\. VirusTotal: validation

On VT the same hash shows up as `pi 2.exe`, with something like **53/71** engines and Behavior from sandboxes (CAPE, Zenbox, etc.):

![VirusTotal: pi 2.exe — 53/71, community score −11](https://cdn.hashnode.com/uploads/covers/6a64e9450fe8019fa8e19870/464f1e38-c67c-4ccc-8482-a5c4b222ebb3.png align="center")

Behavior confirmed what the lab already had:

*   `cmd` + `ping` + `REG ADD` Run / `PI-0023455`
    
*   copy to Desktop + execution
    
*   AES / .NET resources
    
*   tags like `long-sleeps`, obfuscation, anti-debug
    

Expected differences:

*   sandbox DNS ≠ full FakeNet domain list
    
*   VT environment noise (Google Update, font/decoy “memory patterns”)
    

A **negative Community Score** (e.g. −11) means the community voted the **file as malicious**, not that the report quality is “bad”.

* * *

## 7\. Classification (what we can claim)

**Observed**

*   Multi-stage .NET loader (AES resource → Foliferi/Reactor → in-memory unpacker)
    
*   Persistence (Desktop + Run via `cmd`)
    
*   DNS beaconing + web connect attempts (incomplete C2 under FakeNet)
    

**Not observed in this analysis**

*   Stable extra-stage download with full HTTP
    
*   Clear exfil/POST
    
*   Ransomware / mass file destruction
    
*   A 100% family attribution from VT alone (treat engine family names as hypotheses)
    

**Working classification:** .NET loader/downloader with persistence and contact to a domain list (likely C2).

* * *

## 8\. What I would do differently next time

1.  **VT / MalwareBazaar in minute zero** (context)
    
2.  **Procmon + FakeNet first**; deep RE later
    
3.  Include **children** (`cmd`) in Procmon filters
    
4.  Validate the pcap (open in Wireshark) **before** copying it out of the Share folder
    
5.  Stop earlier when native code is clearly **JIT**
    
6.  Write a checkpoint after every stage (hash, path, “what it does”)
    

* * *

## IOCs (summary)

| Type | Value |
| --- | --- |
| SHA-256 | `af86d6a54fc8fb39239878a6b6e5a8f558ca0bba1bc85af7d5e076a59be18f67` |
| Drop | `%USERPROFILE%\Desktop\PI-0023455.exe` |
| Persistence | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\PI-0023455` |
| Technique | `cmd` + `ping` delay + `REG ADD` + `copy` |
| Network | `www[.]…` list above (DNS); TCP 80/443 attempted |

VT (reference):  
https://www.virustotal.com/gui/file/af86d6a54fc8fb39239878a6b6e5a8f558ca0bba1bc85af7d5e076a59be18f67

* * *

## Closing

I started with a sample that *looked* like a document and ended up mapping a full .NET chain: AES resource decryption, Reactor + trial-date friction, persistence in Procmon, C2 candidates in FakeNet, and — the hard way — learning that not every `PUSH EBP` in RWX is the “final payload”.

If this write-up saves someone two weeks chasing JIT stubs, it already paid off.

* * *

*Based on my lab notes (Notion) and analysis on FlareVM. Written by me; AI helped with the English rewrite. Feedback welcome.*
