Breaking Down Discord Account Takeover from Low-level Perspective
Malware Analysis · Published 23 Jul 2026 · 16 minute read
A runtime investigation of process hollowing, self-healing hosts, RC4-protected C2 traffic, anti-VM checks, and Discord credential theft.
Overview of the fake Discord Nitro account-takeover investigation
Discord has often been targeted by threat actors to perform malicious activities, such as taking over victim's account to spread fake Crypto campaign, Malicious phishing link, or any cyber threat that uses Discord as C2 communication. This platform was chosen by gamers, crypto players, and developers because it has strong social trust, easy account-to-account communication, and a rich API ecosystem that enables automation.
But behind those advantages, Attackers could exploit victim's accounts to spread phishing links, fake giveaways, malicious OAuth applications, and malware through Discord servers or DMs to spread campaigns rapidly. Threat actors would also abuse Discord's infrastructure, such as webhooks, bots, and private channels, as low-cost command-and-control (C2) and data exfiltration channels.
This happens because of the benefits of Discord itself, where attackers could gain reliable cloud infrastructure, encrypted HTTPS traffic, and widespread-use for free which can make malicious communications appear similar to normal user activity.
So here we are. This is the third time I perform Malware Analysis in my self-research project: Crime & Punishment. This post was created to satisfy my curiosity about how this could happen at the lowest level perspective. I found a sample similar to this incident from MalwareBazaar. Enjoy the show :)
Initial Recon
| Property | Value |
|---|---|
| Filename | discord-nitro.exe |
| Size | 102,400 bytes |
| Type | PE32+ GUI executable, x86-64 |
| Sections | .text, .rdata, .data, .pdata, .reloc |
| Entry point RVA | 0x136B0 |
| MD5 | 7866E055D6135C7FCBEBC7BBDB8266D5 |
| SHA-256 | 9115C69843820F1A93B137BF6EAB805294D61BFEE04B971B7DF01DE2C927A12B |
| Signature | Not signed |
Different from the previous content, this blog will talk more about dynamic analysis rather than just static. I used x64dbg, Process Monitor, Wireshark, HollowsHunter, ProcDump, Rizin, Frida, and Python scripts during analysis. The point was to try to explore important findings through runtime execution.
Okay, the suspected file was named discord-nitro.exe and was presented as part of fake Discord Nitro subscription. When the binary file is executed, no app window appears on the screen. However a few seconds later, a CMD window blinked and quickly disappears, and then Calculator.exe was suddenly opened on the screen even though I didn't launch the program.
Fake Discord Nitro sample spawning Calculator during initial recon
The original import table was boring. It contained C/C++ runtime functions and a small set of harmless-looking dll such as kernel32.dll. Even though it lack of sensitive imports, didn't mean the capabilities were absent. I still suspect the binary file as malicious executable since previouslt there is a CMD window and Calculator.exe suddenly blinked on my screen.
At this point, Recon step is useless because you won't get any meaningful clue during analysis. In short, you also won't be able to find insightful results when performing Static analysis because all functions and decompiled code was obfuscated. I thought the malware hides its operational APIs behind runtime resolution, so I need live execution to learn what it actually does.
Malware Evasion 1: Process Hollowing
I started Process Monitor and a packet capture before launching the executable. During the first run, discord-nitro.exe exited after only a few seconds, but it created a new Windows process which initially made it look like a malware loader. The important clue was the behavior attached to them after the loader disappeared. The first useful observation was timing. I filtered the useless stuffs until i got this process list:
Analysis evidence: initial process
discord-nitro.exe is our suspected malware while svchost.exe, dispdiag.exe, and dllhost.exe are a legitimate Windows process. But why did I still highlight those processes in the screenshot above if it's legitimate? It's because all of them have corellation. On the image above, 2 seconds after discord-nitro.exe was disconnected to an IP, there is TCP connection trying to connect to the same IP by the dispdiag.exe. But actually since I have applied some filters, it should be in miliseconds after discord-nitro.exe process has terminated (not 2 seconds).
Why does it matters? Isn't that just a coincidence? You might be thinking if those are only noisy processes. I scanned the exact live PIDs with HollowsHunter, which uses PE-sieve for memory-to-disk comparison. It was confirmed that svchost.exe, dispdiag.exe, and dllhost.exe are the hollowing target.
Analysis evidence: hollows hunter
This was the first direct evidence that the PEB-connected main images had been replaced. Hollows Hunter classified the process's primary image as a different PE from the executable named in the PEB.
Malware Evasion 2: Self-healing Mechanism
For the context, Self-healing here means the malware can automatically respawn or restore itself after being terminated. In the previous step, we know that dispdiag.exe is one of the process-hollowing target. But actually, if we try to kill the dispdiag.exe process, it will terminated, but then spawns another hollowed-process with a different name.
Each time a new hollowed target was spawned, It will executes the same behavior as if it's launched from a single app. Looking at the GIF below, the author of this malware is quite smart. Every time the malware was terminated, they always choose a legitimate Windows system file as the next hollowing target so that the malware always hidden under a trusted or legitimate process. The first verified primary host was dispdiag.exe; the later verified host was nslookup.exe, nbstat.exe, and then a randomly named executable. The malware used hidden PowerShell loaders, Explorer, and Runtime Broker to create replacement processes.
Analysis evidence: self healing
To confirm if this finding is not just a false-positive, and to validate whether each process-hollowing target is actually a single binary that have the same behavior, I tried to dump the process during runtime while comparing them using PE Bear. Since the hollowing target will change, I run Hollows Hunter again to identify the next suspected process. The result is now isoburn.exe and then SndVol.exe.
Analysis evidence: hollowing target comparison
The reconstructured PE binary and its metadata were byte-identical. Both reconstructed main image had:
To wrap up this section, discord-nitro.exe uses variable primary process-image replacement targets. dispdiag.exe, isoburn.exe, and SndVol.exe were confirmed as the hollowed target through PE-Sieve and runtime payload recovery. Earlier I mentioned about svchost.exe and dllhost.exe which based on my analysis, they appear to be the secondary replacement hosts.
String Discovery
It would be too strong to say that the malware is undetectable due to self-healing mechanism since we have already know the pattern. The previous findings was useful to determine our next steps. The fact that discord-nitro.exe is only acts as a loader while the main payload could be in the hollowed process, means that we can dump the hollowed target to analyze it further.
Earlier I said that static analysis won't help you to get any insightful findings. Well it partially correct since the first time I saw the malware, I thought it was the main payload. It turns out to be just a loader where the real payload would be in the hollowed process. So now our next step is to perform static analysis of the hollowed target, starting from exploring known strings. This time, the hollowed target is changed to mspaint.exe with PID 3032.
You can use FLOSS against the hollowed process to do this, but I prefer to dump the hollowed target using procdump while searching known strings with strings64.
Analysis evidence: strings
We got a bunch of useful clue here. The dumped process of hollowed target is confirmed to be the main payload. It contained the returned account, guild, and invite data stored in the local storage of different web browsers. /users/@me exposed account ID, username/global name, email, phone, locale, MFA state, and premium state. The guild response contained ownership metadata, and the invite request returned invite information for an owned guild. Billing subscriptions were empty since my account is not premium
Moreover, now we can see that luckyware.cy seems to be the attacker's C2 endpoint. Based on the URL pattern, the malware seems to have capability for doing screen capture, uploading victim files, and interecting to attacker's bot.
Actually, there are more results rather than just Discord URLs, Local storage, and C2 endpoints, because I just filtered out the output using grep. But does the malware really use those C2 endpoints and local storage path for stealing data? or is it just hard-coded without actually hitting it? Currently the C2 endpoint was revealed. However, since we don't know what data are being sent to the attacker, I need to continue the analysis at network-layer.
Breaking the C2 Codec and Reverse RC4 Encryption
Analysis evidence: wireshark
Looking at the Wireshark traffic, there are several hits to https://luckyware.cy. All request are encrypted over TLS so we can't see the real payload in a plaintext. But I found interesting finding when exploring Wireshark packets. The User-Agent field was filled with Base64 value which carries 56 decoded bytes without any HTTP request body. The request and response shared a four-byte session prefix and rotated together during my observation.
Even though the value was base64-encoded, we won't see a readable string if you decode it. Those traffic appears after the hollowed process was executed. So far, we have dumped the hollowed process in the previous step, but still haven't touched it yet. Since we now have the dumped hollowing process, we can import it on Ghidra for performing static analysis.
Analysis evidence: binarystring
Inside FUN001316b0, Ghidra decompilation shows:
1. Call time64 and build the rolling decimal key
2. A key-scheduling loop at RVA 0x31EC0 to 0x32030 that swaps entries based on key bytes.
3. A second swap/XOR loop over the message buffer (RVA 0x32050–0x320C6)
4. CryptBinaryToStringA(..., 0x40000001, ...) after encryption.
5. CryptStringToBinaryA(..., 1, ...) before response decryption.
Those operations are RC4 structure. Flag 0x40000001 is Base64 with no CR/LF. At CryptBinaryToStringA, the flags were 0x40000001, which is CRYPTSTRINGBASE64 | CRYPTSTRINGNOCRLF. In short, what we see in the previous Wireshark traffic is actually a decoded version of plaintext which follows:
The exact key input is assembled from configuration/identity fields in that same function, but it varies by session.
Analysis evidence: Decrypt RC4
For positive Unix timestamps, the rolling key calculation simplified to:
The recorded capture used:
I validated the result byte-for-byte against all five request/response pairs. The requests decrypted to:
The two short responses decoded to:
The large responses contained three additional malware stages:
| Stage | Size | SHA-256 |
|---|---:|---|
| PE64 stub | 720,896 | 4FBB21606DEFA6B48C06FB9A3A0D13075DEC0846298443BD74A2E58212DAB495 |
| x64 Donut shellcode, handler v003 | 103,116 | 2C197A5AA1CC6AB9F04B5F9CD384D583141658859F1EFC0F7726CB3C6536BA5D |
| PE64 handler v002 | 83,456 | 9A401F42467DB35D1F014625768642E87467F1123C2F86C5FADF7ADA358BE96F |
Malware Evasion 3: Anti-VM and How to Defeat It
This is the most difficult malware defense I've ever face during analysis. The malware has an Anti-VM gate which makes it very difficult for me to debug in x64dbg. Actually, the analysis of this malware was already considered to be completed since the previous session. This was because based on the static analysis results, I was quite satisfied with the findings so I thought this result was good enough (actually im avoiding this malware defense lol).
Analysis evidence: anti vm1
The dynamic analysis I performed previously was just opening Wireshark and dumping hollowed process which ends up to static analysis again. However, static analysis doesn't truly show the program's impact and execution in real time, and I still haven't discovered the malware's capabilities. For example, based on the victim's background, their account was hacked and they suddenly sent messages that didn't originate from them.
I'm still curious about that. Static analysis hasn't revealed anything about that case yet. But if I rush to continue with debugging in x64dbg, I will face this damn Anti-VM. So what would I choose? face the fear and start rushing :(
To be noted, I probably won't go into too much detail about how to bypass Anti-VM because during the process, I did a lot of trial and error so there are many steps that are not documented. But you can still see the original binary and the patched one in this local analysis folder.
Analysis evidence: anti vm2
During static analysis, the environment checker at RVA 0x48BE0 collects DXGI adapter information, physical RAM, and usernames. The build explicitly compares the username vboxuser and also reacts to Microsoft's software-rendering adapter.
The hardest part of this analysis was getting a captured hollowed image to execute as a standalone PE. In this case, the hollowed sample is either mspaint.exe or tabcal.exe. First I would place a one-shot breakpoint at the PE entry point and high-signal breakpoints on termination and exception paths. There are 2 very different situations:
1. Failure before the PE entry point: loader, relocation, or reconstructed-dump problem
2. Early return after user code : possible environment or anti-analysis decision
Analysis evidence: anti vm3
The sample reproduced the first case. It terminated before hitting Anti-VM gate (at RVA 0x48BE0) or any of the user-code breakpoints. This happens because of double relocation. The file contains 270 IMAGERELBASEDDIR64 targets that already held the captured process relocation. Windows relocated them again when mapping the reconstructed PE at a new base, producing a pre-entry 0xC0000005. The required normalization was:
This repair changes captured loader state. Next, I loaded the normalized analysis image at base 0x2F0000 and restored the original nine gate bytes in memory:
The live disassembly became:
Analysis evidence: anti vm4
Result after running it:
This time I've reached the anti-VM gate. However, execution later faulted inside the anti-VM code and terminated with 0xC0000005 exception. The exception was caused by additional captured process state such as stale cached API pointers, CRT initialization state, and initialized static guards.
For the decisive run, I used the fully dump-state-repaired image. I restored only the original anti-VM bytes in memory before execution. The file on disk was not changed.
I kinda forgot about the next steps. But as I remember, I patched the conditional branch at 0x6B5A4:
Analysis evidence: anti vm5
| RVA | Original bytes | Patched bytes | Purpose |
|---:|---|---|---|
| 0x6B59D | E8 3E D6 FD FF | B0 01 90 90 90 | Replace the environment-check call with mov al,1 |
| 0x6B5A4 | 74 07 | EB 07 | Force the accepted-environment branch |
This pair does bypass the environment decision. Immediately afterward, code at RVAs 0x6B700–0x6B72B waits for a launcher-owned IPC record. Looking at the decompiled code below, The while loop sleeps for 50 milliseconds while ready remains zero. Because the file was reconstructed from a hollowed child, the parent/watchdog process that normally writes the record was missing.
Analysis evidence: anti vm6
I repaired the standalone replay with six more control-flow changes:
| RVA | Original bytes | Patched bytes | Effect |
|---:|---|---|---|
| 0x6B59D | E8 3E D6 FD FF | B0 01 90 90 90 | Replace anti-VM checker call with mov al,1 |
| 0x6B5A4 | 74 07 | EB 07 | Always take the accepted-environment branch |
| 0x6B72B | 74 D3 | 90 90 | Leave the missing-launcher IPC wait |
| 0x6B72D | 48 8B 1D 84 45 04 00 | 48 8D 1D CC 57 04 00 | Point RBX to the embedded IPC record |
| 0x6BDC3 | 0F 85 0F 02 00 00 | E9 10 02 00 00 90 | Force the normal mutex-owner path |
| 0x6C25E | 0F 85 D4 07 00 00 | 90 90 90 90 90 90 | Do not suppress the C2 bootstrap |
There was one more problem specific to process dumps. The image contained API addresses, CRT state, and local-static guards from the old process. I reset the CRT/static initialization state and zeroed 63 stale DLL pointers so the malware's own hash resolver would populate valid addresses for the current boot. Without those repairs, the access violations looked like anti-debugging but were actually replay defects.
Discord Credential Theft & Account Takeover
Once the Anti-VM was bypassed successfully, the first interesting things to explore was exactly the one suggested by the previous findings from String Discovery. Looking at the image below, the requested access was read-only, the share mode was 7, and the operation was CreateFileA. This is direct evidence at runtime, which proves my previous assumption that this is not just an unused path.
Analysis evidence: local state
Local State matters because Discord is an Electron application built on Chromium. Its JSON contains the Base64-wrapped oscrypt.encryptedkey. The collector removes the DPAPI prefix, calls CryptUnprotectData, and obtains the per-profile master key. In my replay, the DPAPI call at RVA 0x2F9CE succeeded and returned a 32-byte key.
Analysis evidence: leveldb and me
Recovering a token-shaped string is not enough. LevelDB can contain stale and invalid values, so the malware validates each candidate against Discord itself. The broader trace on the image above shows a token validation through GET /api/v9/users/@me. This is the boundary where the malware receives the account-validation result.
The controller thread continues to operate while the Discord collector runs. Actually, there are also another HttpOpenRequestA call for POST /index.php to luckyware.cy during the later debugging session. This confirms that the C2 poller was still active alongside the credential workflow.
Analysis evidence: linkedin2
x64dbg is also captured the billing/subscription and payment-source endpoints, the SerpentDiscord/1.0 User-Agent, and personal account JSON. The unfamiliar logged-in device from Sweden shows the real account-takeover impact. To summarize The end-to-end execution model I could support with this runtime evidence might be similar to: