Malware Analysis: Discord Code Injection

I haven’t forgotten about this blog, I promise! I’ve just been busy! But as of this week, I’m officially on winter break, and my break started off with a bang. Today, we do some malware analysis!

Malware Analysis: Discord Code Injection

Merry winter break everyone! Hope you’ve all been well! I know I said earlier that I would try to post at least once a week but uh… As we can all see, that didn’t happen. I promise it’s not because I forgot about the blog. I’ve written 10 or so entries that haven’t been published yet due to multiple reasons: permission concerns, content not meeting my standards, etc. etc. I’m still working hard, but right now it’s mostly behind the scenes. Hope you all understand. :)

But for today, we talk Discord malware! As the post excerpt states, my winter break started off really exciting with multiple scammers contacting me and members of my server in an attempt to hack me and my friends’ discord accounts. Luckily only one person was affected, and I was able to put mitigation methods in place on my server before the problem got worse. However, in this process, I managed to get my grubby hands on one of the more popular malwares making the rounds, and decided to go on the offensive. Here’s how I did it.

First Contact

It started when my friend Jake, the old president of Florida State Esports, contacted me out of the blue. He asked if I could test a game for him, and said it was for his IT class. (In retrospect, I should have remembered that Jake has since graduated from FSU and is no longer a student, but at the moment it slipped my mind.)

Jake texting me out of the blue was not unusual. He and I used to talk a lot during my high school days when I was the president of my high school’s esports program, and even after he left the presidency, he would text me occasionally when he saw me playing a game he really liked. I hadn’t spoken with him in a while, so maybe he was just trying to catch up. Maybe he genuinely needed my help.

The scammer’s messages. The account used to look different, it has since had its pfp and username changed several times.

That said, his request didn’t demand I treat it with the utmost trust, so I did as I was asked but with several safeguards in place:

  1. The file would be run within a sandbox
  2. The file would be run with a freshly made throwaway account
  3. The throwaway would be made with fake PII

I proceeded with this plan. The exe was 88 megs, which set off a small alarm in my head, but I persisted. After setting up the VM, I ran the exe and nothing happened. I asked Jake about what was up, if there were any other hoops I needed to set up in my VM to make it work, assuming it genuinely was a project he needed help with. It was there that I got my biggest red flag:

Red flag.

No game interacts with Discord like that. I have enough experience with the Discord API to know that the only thing that can really interface with discord on a user’s computer is RPC, and access to the RPC API is so locked down that only certain whitelisted app IDs have access to it. Obtaining access to said API requires approval by the dev team themselves, an honor very few applications get. The odds of Jake’s “class” getting approval to work on a project with that much power surrounding it is slim to none. This was a virus.

I told the scammer that I was having client issues, closed the DM, and blocked the account. After that, I deleted the VM and proceeded to enter damage control mode.

Byebye!

I could easily see how someone could fall for that. Hell, I almost did. Thank god my IT spidey-sense kicked in and gave me the forethought to sandbox it. This could have been catastrophic. I immediately told all of my roommates about it, all of my servers, and a bunch of Jake and I’s mutuals as well. My roommate, Daniel, told me that an announcement in one of his servers already warned him about this specific hack, so at least people were already aware.

I’ll take this opportunity to make something very clear for all readers. Scams like these, especially with the pending integration of NFTs and Web3 into the discord platform, are on the rise big time. (Not happy with that btw, but I’ll save my NFT rant for another day.) Do not download any exe that you do not trust with the entirety of your being. If someone contacts you out of the blue asking for help, just like Jake did to me, do not run that file in an uncontrolled environment, let alone at all.

Also on that note, just yesterday morning another scam link was posted to my personal discord server, and I had to clean that up too. Please remember: The only valid Discord URLs are discord.com, discordapp.com and discord.gift! If you ever receive a “free discord nitro” link from anyone, even if it’s your own mother, that does not match those domains exactly, it is a scam. Case sensitivity doesn’t matter, but be wary that many scammers will register links that look very close to their originals. (i.e. The scam in my server had the “i” in “gift” replaced with a lowercase “l”.)

I did some more poking around my host PC and felt comfortable that the malware didn’t leak out of the VM, but just in case I reinstalled my discord client, changed my account’s password, and left it for the night.

Starting the Offense

The following morning, I woke up with a vengeance. I remembered that I still had a link to the exe on my discord, and figured it would be a GREAT chance to some malware analysis. I quickly found the link, in case the link that the scammer sent me was set to expire after a certain period of time, and tried downloading the exe again. Surely enough, the download was still live, so I downloaded the file, immediately changed the file suffix so I wouldn’t accidentally run it, and put it in a quarantined sector of my computer.

I checked the hash of the file against virustotal, and only 3 vendors detected that it contained malicious code. However, of more useful information, the details states that the file was a bundled version of the node.js runtime.

File Details on VirusTotal

This meant that the malware was written in node.js, which means reversing it shouldn’t be too hard. Later that day when I found time, I loaded the exe into my hex editor and saw that sure enough, there was uncompressed javascript in this exe.

Some random Javascript file in the exe, currently looking at address 0x3DCCBC0

Knowing that the code was there in plaintext piqued my interest. This section of the exe seemed to contain all of this app’s files concatenated after one another. The question now became “How do I extract the code from the exe, and how do I determine what code does what?”

Unpacking the Code

I remembered from my Node.js days that the most popular framework to make native apps out of Node.js was electron, but figured that because this virus didn’t have a GUI, that it wasn’t compiled using that. (This information would be relevant later because Discord uses Electron as one of its desktop client’s major backbones, but for now I put this knowledge on the back burner.) I also remembered from my BF-Yield project in freshman year that there were frameworks I could use that would compile high level languages (in BF-Yield’s case, Python) down to a single exe for distribution.

This would explain the huge file bloat. Node.js is infamous for having unholy amounts of dependencies for projects and 88MB sounds about par for the course. If this was the case, that meant that somewhere within this file existed a mapping of sectors within this file to whatever filename they should have on a traditional desktop runtime.

I started by looking for common suffixes. This is javascript, so .js and .json are a good chance.

Damn. Over a thousand hits.

I could have changed my query to look for something else, but after looking at the list for a few seconds I immediately found something of interest.

Found the mapping, and something else...

This looked like a file mapping that correlates each file name with an array of two integers. But of more importance was the name of the dict everything was held within. It had a reference to something called nexe. While doing research on Node.js packaging I saw that Nexe was one of the options people can use. This gave me a solid lead that this malware was compiled using this framework, and that gave me a new keyword to google with.

It wasn’t long after that I found a link to a Github Issue of someone asking how to decompile a Nexe package. There, an absolutely wonderful contributor to the project gave me the answer I needed:

Thank god for legitimately helpful members of FOSS projects.

Using this information, I was able to figure out how to parse this file mapping and get a ballpark estimate of where the actual malware was. 99% of the text glob were the contents of files in the node_modules folder that I didn’t really care about, but there was one file that seemed interesting: 2sEx5uHuU9Y9.js. The first file in the listing, and the only file not located within node_modules. It had to be our entry point.

Using the estimates given to me from the mapping, I found the code.

This looks awfully obfuscated and suspicious...

I copied the entirety of the file into a beautifier, and then into a text editor, and began figuring out how the thing ticked.

Solving the Puzzle

This thing sure was obfuscated. Every variable (minus certain function names that ended up being big hints) had their names replaced with random hex, and all strings were encoded using a central string array, rather than being in plaintext. The developer wasn’t going to make this easy, but I was determined.

After poking around for a bit, I discovered that all the functions within this project start by copying a global function pointer _0x569885 to its local scope, and this local version would be called in every case where a string should be. Each function then uses this new pointer to establish a lookup table within itself that contains all strings that it needs to reference. URLs, dict keys, and more were all encoded using this method, so my first step in cracking this code was figuring out how this function worked.

Look at all these calls to `_0x569885`!

In the first line of the file, var _0x569885 was set to the value of _0x32f2. When I searched for _0x32f2’s definition, it pulled me to a function.

`keyconverter_implemented`, aka `_0x32f2`. (I renamed a few vars to make it easier to read)

The function made a call to another function _0x5f6e (getLookupArr) which contains a giant list of strings and some obfuscated code that just returns said list. Back to this function, if you stare at it long enough, you might realize that second argument of this function doesn’t matter, and that this function essentially just returns the value at index arg1 minus 298.

When I copied this code into a sandbox file and tried getting values with it, something didn’t seem right. I tried using my code to get the values of function InjectNotify’s lookup table, and something looked wrong…

This doesn't look right...

Those should be proper strings, but it looks like obfuscated data still. This probably meant that the main array has some modification done to it before the rest of the payload uses it. So… back to the drawing board!

I found this function at the start of the file that looked interesting, so let’s stare at it too.

This looks interesting... (Once again, deobfuscated a bit for reading.)

It’s a function definition that immediately calls itself with the main array constructor function as its first argument. It could very well be modifying the main list.

Looking into it further, you can see that it does some iteration, using a while (true). Each iteration it performs some long and convoluted calculation using the key converter function, then checks to see if the value calculated is equal to the second argument. If the argument and calculation don’t match, it takes the first value in the main list and moves it to the end, essentially shifting everything in the list left by one… like a deque!

This gave me an idea. If all we need to do to make the main list function properly is find out the amount we need to shift the deque by… We can brute force this! If I looped over the main list and kept shifting the deque until I get the main lookup function to return a known value, I can crack the offset!

My comparison was pretty easy. Importing the url lib must look like require('url') but in the code it uses require(keyconverter_implemented(0x158)). Knowing this, let’s write code to shift the deque until keyconverter_implemented(0x158) returns 'url', and print that shift amount! Brute force time!

// get tarr
var tarr = getLookupArr();

// establish keyconv
function mykeyconv(a1) {
    return tarr[a1 - 298];
}

// bruteforce
console.log(tarr.length)
for (i = 0; i < tarr.length; i++){
    var val = mykeyconv(0x158)
    if (val == "url"){
        console.log("offset:", i)
        return;
    }
    tarr.push(tarr.shift());
}
Here we go!

I tried this with a couple other known library imports just to make sure duplicate occurrences of a package name in the main list didn’t give me a false positive, and they all returned 468. So now to get actual values, we start by shifting the deque 468 times, and afterwards all subsequent references to the main list should now point to the right place! Now we can try our original InjectNotify look up conversion again!

💡
Editor’s Note: Beyond this point we discover a lot of information about the project that can be used to identify it. Because I decided to publish this report publicly, and I don’t want to give this project or its creator a notoriety boost, I’ve redacted all developer PII and repo names from here on out. Hope you understand. :)
This looks more like it... Also looks like dev can't spell!

We now have access to all the strings within this virus. So let’s see what we can find. For shits and giggles I put the Inject function’s lookup through my converter and…

What's this?

Is that… A GitHub username I see? Better yet, is that a repository I see?

Yes. Yes it is.

Searching the Remnants

I immediately downloaded the repository as a zip, in case the repo gets taken down for whatever reason. A find this big is too huge to give up. We now have the source code of the virus we’ve been looking at, so lets see what we can find…

Deobfuscated source code!

It seems this project is split into two parts: the injector that we just reversed, and the injection payload that is patched onto a running Windows Discord client. What does this payload do?

A password logger...

But wait, there’s more!

A CREDIT CARD LOGGER!

The injected payload works by hooking itself to certain functions that handle calls to the Discord API. By doing so, it can intercept and log/exfiltrate any important values before this private information gets encrypted by SSL and sent over the internet to its actual destination. It posts all info to a Discord webhook, probably on the perpetrator’s own server. The payload hooks into every major function on the client: login, email change, password change, credit card addition, etc. And because the injector modifies Discord’s main app.asar file directly, the changes persist until you completely uninstall and reinstall the infected Discord client.

Wrapping Up

This is where my story ends. I considered flooding the webhook with false yet valid-looking data to slow these scammers down, but as soon as I saw that credit cards were involved, I decided to report it to the higher-ups. I immediately sent the exe I analyzed, the zip of the repo, and all related links to Discord and am actively awaiting a response. I’ll update this post when I get word back.

I’ll take this chance to reiterate what I said earlier. Do not download any exe that you do not trust with the entirety of your being. The only valid Discord URLs are discord.com, discordapp.com and discord.gift! If you ever receive a gift link from anyone that does not match those domains exactly, it is a scam.

Discord scams are real. Especially now that we know that this hack isn’t just stealing tokens, but rather modding clients and stealing credit card info, clicking stray links isn’t a joke anymore. This gets way worse when you consider that NFTs are potentially going to be linked to the platform, scams like these are going to be extremely prevalent. (Sidenote: Still not a fan of NFTs, but this is still a major security risk just waiting to get worse, so I’ll put my opinions aside for a second.)

Be safe. Really.


Following Up & Final Thoughts

It’s now the next evening and I have gotten a response from Discord basically saying that they don’t care and that they’re closing my ticket.

I don’t blame them. I sent the report to them via their bug report portal out of a concern for confidentiality, and typically social engineering and phishing attempts fall out of scope for that platform. The engineers who read my report probably recognized this immediately and closed my ticket without a second thought. A forward to the Trust and Safety team would have been nice, but it’s whatever.

I have also since found out that this software now has a website, written in Express.js, that will generate a custom version of the malware for any script kiddie with too much time on their hands to use. There’s even a premium version that steals even more info from victims! With easier accessibility comes more users, so I’m willing to bet that this program is only going to get more popular as time goes on.

With this in mind, I believe public disclosure is the next best plan of action.

The first step in this new plan is publishing this write-up and sending it around to any parties who I think may be interested. Afterwards, I’ll probably also work on a bot to help scan links in all of my servers and remove any suspicious ones automatically, but even that isn’t a permanent solution now that Discord’s shifting their API to only support their new slash commands instead. Maybe a BetterDiscord plugin would be better suited for this, but that also limits my outreach to techies who don’t mind breaking a few rules to mod their desktop clients. Not to mention it does nothing for mobile users.

As you can probably tell, I’m a bit frustrated with how Discord’s choosing to run their platform at the moment, but then again I’m only one user out of millions. So for now, I’ll just focus on keeping my circles safe, while looking into more secure solutions to potentially implement and/or migrate to. (Matrix, anyone?)

If there’s any takeaway you get from this write-up, let it be the following:

  • Don’t be naive like I was. Be suspicious of any links anyone sends you.
  • Take any/all precautionary measures when dealing with any untrusted data on your machine.
  • These scams are only going to get worse, so it’s better you familiarize with popular vectors of attack right now, and prepare yourself for the future.

That’s all for now. I’ll see you all in the next post. :)