Anatomy of a fake folder: an NSIS miner dropper, 191 decoded instructions, and a sandbox report that read it backwards
A teardown of a Windows cryptominer dropper whose entire exploit is a folder icon: what the installer script actually does instruction by instruction, a wallet the packer hid from every file on disk, why the mining could never have worked, and why the automated verdict described a different piece of malware.
TL;DR
- A quarantined 4.5 MB sample came with a sandbox verdict of malicious, threat score 100/100, 223 signatures, 101 MITRE techniques and a top-level "Spyware" incident.
- The technique list was wrong in both directions. It claimed Email Collection,
Screen Capture, Clipboard Data, OS Credential Dumping, Process Hollowing and
Timestomp, none of which the sample does, while never assigning
T1496 Resource Hijacking to a file it had itself tagged
miner, and never assigning T1091 to a worm whose drive-root copies it had recorded. - Reality: an NSIS 2.46 installer that disguises itself as a
Windows folder. It carries the stock Explorer folder icon, a version
block whose
FileDescriptionis literallyFolderand whoseProductNameisImages folder (x86-x64), and it propagates asimages.scrto every drive root from C: to Z:. A.scris a PE that Windows executes on double-click, with its extension hidden by default. - Extraction tools unpack an NSIS archive's files but not its logic, and
here the logic was the whole story. I parsed the format directly and decoded all
191 script instructions, the string table and every branch target. The
reconstruction is self-validating: its branch graph predicts exactly 12
HTTP requests with
test8twice and notest9/test10, and the capture contains exactly that. - Two persistence mechanisms (a Run value named
Coinand a Startupimage.lnk), a 30-second stall, a wallet chosen from the dropped file's own timestamp, and the wallet config deleted after use. - The three payloads are VMProtect-packed, so nothing on disk names them.
Process memory does: Claymore's CryptoNote Miner, built on the CryptoNote
reference
simpleminersource, with a hardcoded dev-fee wallet that exists in no file at all and a string that readsSOMETHING WRONG WITH DEVFEE CONNECTION. STOP MINING. - The mining never happened and could not happen. 54 stratum packets, every one a SYN, zero SYN-ACK: the pool sat behind Cloudflare, which does not proxy TCP 3333. CryptoNight v0 has been dead to Monero since RandomX in November 2019. Revenue: 0 XMR.
- What is not dead: the worm, both persistence mechanisms, and a dormant download-and-execute stage that takes its URL from a config key. One line in a text file turns an obsolete miner into a live loader.
If you take one thing from this: a sandbox tells you what the malware did on one run; the script tells you what it can do. Decompile the script.
Background: the fake-folder worm
This lure is old, unglamorous, and still works. The mechanics:
- Take a PE and give it the extension
.scr. Windows treats a screensaver as an executable and runs it on double-click. - Set its icon to the standard Explorer folder graphic and name it something a folder would plausibly be called.
- Write it to the root of every drive: fixed disks, USB sticks, mapped network shares.
- Because "hide extensions for known file types" is on by default, the victim sees an item named images with a folder icon, and nothing else.
There is no exploit and no dropped second file at the point of infection. The user's own double-click is the delivery mechanism, and the code runs at their privilege. It spreads by being copied onto shared storage rather than by exploiting anything, which means an air-gapped network is not immune and a patched one is not either.
What makes this specimen worth a teardown is the care taken over the deception, and the fact that the automated verdict got the malware's identity, its capability list and its impact all wrong at once, in a way that a five-minute look at the script would have settled.
The two things, side by side
The entire attack rests on a victim not being able to tell these apart in Explorer. On the left, a folder. On the right, a 4.5 MB executable.
RT_ICON id 1, one of ten sizes in icon group 103, stored as a literal PNG
stream so the extracted file is byte-identical to the embedded resource (28,181 bytes,
MD5 b744c2b55648408de209c6180ef48e5e)That is not an approximation of the Windows folder icon; it is the Windows folder icon, supplied at all ten sizes from 256×256/32bpp down to 16×16/4bpp so that no Explorer view falls back to a generic executable glyph. Whatever icon size the victim's view mode asks for, the answer is a folder.
Why the fake works, line by line
| A real folder | images.scr | |
|---|---|---|
| What Explorer shows | Folder icon, name, no extension | Identical. Folder icon at every size, name images, extension hidden by default |
| Properties → Details | File folder | FileDescription = Folder,
ProductName = Images folder (x86-x64),
Comments = FOLDER |
| On double-click | Opens in Explorer | Windows executes it, then the script opens an empty folder in Explorer anyway |
| Where you find it | Anywhere | The root of every drive C: through Z:, including removable media and mapped shares |
| Signature | n/a | Unsigned, but branded: impersonation by metadata rather than by certificate |
| Visible UI | A folder window | A folder window. Nothing else — no wizard, no progress bar, no UAC prompt |
Three design choices deserve a callout.
- The metadata lies in the same direction as the icon. Setting
FileDescriptiontoFoldermeans that the one place a cautious user might look, right-click → Properties, agrees with the lie.OriginalFilenameandInternalNameare deliberately omitted: either would have had to name a real file and contradict theimages.scr/image.exerenaming. - The decoy folder is the masterstroke. When the script detects it was
launched as
images.scr, it creates the directory%TEMP%\images.scrand opens it withSW_SHOWNORMAL. The victim clicked a folder and a folder window opened. The expected outcome occurred, so nothing feels wrong, and the empty contents read as an empty folder rather than as an infection. - The silence is provable, not assumed. The inflated NSIS common header
begins
aa 00 00 00: flags0x000000AA, which setsCH_FLAGS_SILENT,CH_FLAGS_DETAILS_NEVERSHOWandCH_FLAGS_AUTO_CLOSE. The header'sPAGESblock has a count of zero. This is aSilentInstallbuild with no installer pages at all; the six embedded dialog templates are inert stock exehead dead weight that is never instantiated.
The naming is coherent across the whole chain, which is what holds the disguise together:
install directory Images, dropped executable image.exe, propagated
copy images.scr, startup shortcut image.lnk. Every artifact
reinforces a benign "pictures" theme.
The verdict that started it, and why it was wrong
The sample arrived with a CrowdStrike Falcon Sandbox report: malicious, threat score 100, 5 processes, 19 extracted files, 223 signatures (172 informational, 45 at threat level 1, 6 at level 2), 101 MITRE ATT&CK techniques, and six top-level incidents, the first of which is Spyware.
That 101-technique list is heuristic, derived largely from imported-API presence, and it is wrong in both directions at once:
| Report said | Actually |
|---|---|
| Incident: Spyware | There is no keylogging, screenshot, clipboard, credential-store or exfiltration path anywhere in the 191-instruction script. The only outbound data is a fixed-URL GET whose response is discarded unread. |
| Email Collection, Screen Capture, Clipboard Data, OS Credential Dumping, Steal Web Session Cookie | None supported by any artifact. These are API-presence guesses. |
| Process Hollowing, Thread Execution Hijacking, PE Injection | Unsubstantiated. Two level-2 signatures ("creates a process in suspended mode", "writes data to a remote process") are best explained by VMProtect's own unpacking stub, which routinely trips these heuristics. Left unresolved rather than claimed. |
| T1496 Resource Hijacking: never assigned | The sandbox tagged the file miner and recorded both miners' command
lines, then did not assign the technique that describes mining. |
| T1091 Removable Media: never assigned | It recorded Copy to C:\images.scr and
Copy to X:\images.scr in its own detail log, and did not connect them
to the technique. |
Reproducing 101 techniques would misrepresent the threat, so the report I ended up writing carries 15, each tied to a specific artifact. The failure mode is worth naming: a long technique list looks like thoroughness, and its length is what makes the two genuinely load-bearing omissions invisible.
To see what the sample actually is, you have to stop reading the detonation and read the installer.
Setting the scene: an eleven-year-old sample in a 2026 sandbox
Context makes this case legible. Five independent lines of evidence date the build:
| Evidence | Value |
|---|---|
| PE compile timestamp, both CPU miners | 2014-07-23 (10:09:45 and 09:54:38) |
| PE compile timestamp, GPU miner | 2015-02-02 05:34:01 |
| Version block | LegalCopyright: Copyright © 2014 |
Algorithm name in tmp.ini |
cryptonote, i.e. CryptoNight v0 |
| Toolchain | NSIS 2.46 with Inetc plugin 1.0.4.4 |
The detonation is dated 2026-01-07 16:03:44 UTC with
origin: quarantine: a resurfaced artifact, not a live campaign. One wrinkle
worth recording because it will bite anyone building a timeline from the capture file: the
pcap's frame clock reads 2026-01-08 00:04:03, but the server's own
Date: headers read Wed, 07 Jan 2026 16:04:21 GMT. The
pcap clock is +8 hours relative to UTC. All seven beacons land inside
16:04:21–16:04:29, so the whole install chain completed in roughly ten
seconds; the remaining eighteen minutes of capture are two miners failing to connect.
Recovering the script
7z will happily unpack an NSIS archive and hand you eight files. It will not
hand you the install logic, and in this sample the logic is the payload. So I parsed the
container format:
# the firstheader sits at the end of the 87,552-byte exehead stub
off = 87552
flags, sig = struct.unpack('<II', d[off:off+8]) # flags=2, sig=0xDEADBEEF
nsinst = d[off+8:off+20] # b'NullsoftInst'
len_hdr, _ = struct.unpack('<II', d[off+20:off+28]) # 8758
szw = struct.unpack('<I', d[off+28:off+32])[0]# 0x8000085D -> compressed, 2141 bytes
out = zlib.decompressobj(-15).decompress(d[off+32:], len_hdr*4)
# -> inflated 8758 bytes, an exact match for len_hdr
The inflated header carries an eight-entry block table, which tells you where everything lives:
the block tablePAGES offset= 300 num=0 <- zero pages: a silent install
SECTIONS offset= 300 num=1
ENTRIES offset= 1348 num=191 <- 191 instructions, 28 bytes each
STRINGS offset= 6696 <- 1900 bytes of strings
LANGTABLES offset= 8596 num=1
CTLCOLORS offset= 8758 num=0
Each instruction record is a 4-byte opcode plus six 4-byte parameters. Resolving the
parameters against the string table needs NSIS's escape codes:
0xFD introduces a variable reference as a 7-bit byte pair,
0xFE a shell-folder reference as a CSIDL pair
(0x1A/0x23 → $APPDATA), and 0xFF a language
string. Miss that and every path in the script reads as mojibake.
The last piece is control flow. NSIS stores a jump target as
instruction_index + 1, with 0 meaning "no jump",
so decoding the jump parameters of every Goto, StrCmp,
IntCmp and IfFileExists yields the graph:
#27 IfFileExists $R0\image.exe YES-> #38 # skips the test9 beacon block
#38 StrCmp $EXEFILE "image.exe" EQ-> #53 # skips relaunch, decoy AND test10
#40 StrCmp $EXEFILE "images.scr" NE-> #43 # Abort
#115 IntCmp <len [section2] file> EQ/LT-> #124 # skips the 2nd-stage downloader
#149 StrCmp $R5 "Win64" EQ-> #152 # 64-bit: CPU miner, falls into GPU
#151 Goto #154 # 32-bit: skips the GPU miner entirely
The execution: two passes, 191 instructions
The chain runs in two passes. The process the victim double-clicks does the install and then relaunches a hidden copy of itself; that hidden copy does everything else.
Walk-through: what each step does, and why
Pass A, #0–8: beacon "install started." What: extract
inetc.dll to $PLUGINSDIR and call
inetc::get /SILENT http://testswork[.]ru/test8.txt temp.txt, sleep 2 s, delete
temp.txt. Why it matters: the response is never read, which tells you
immediately that this is not a config fetch. More on that below.
#9–20: choose an install directory. What: try five
locations in order until one works — %APPDATA%\Images,
C:\Images, D:\Images, %USERPROFILE%\Images,
%TEMP%\Images. Why it matters: a detection or a cleanup script keyed
to %APPDATA%\Images alone misses four of the five. In this run the first
candidate won.
#21–26, #38–39: self-copy and hidden relaunch.
What: CopyFiles $EXEPATH → $R0\image.exe, then, because its own
filename is not image.exe, ExecShell open $R0\image.exe SW_HIDE.
How we know the second process is this: the sandbox's process list shows
image.exe with the command line SW_HIDE and a SHA-256
identical to the input sample. Every PE carved from that process's memory
turns out to be the stock NSIS System.dll plugin. There is no hidden stage
here; image.exe is the dropper wearing a different name.
#40–42: the decoy. Covered above, and reachable
only in Pass A, i.e. only when the file really is named
images.scr. A victim who double-clicks the fake folder on a USB stick gets the
empty-folder illusion; the same binary downloaded under any other name shows nothing at
all.
Pass B, #53–59: drop the payloads. Six files into $R0:
NsCpuCNMiner32.exe, NsCpuCNMiner64.exe,
NsGpuCNMiner.exe, Data.bin, pools.txt,
tmp.ini.
#60: Sleep 30000. Why: a thirty-second stall between
dropping the payloads and executing them. Cheap, dependency-free, and effective against any
automated analysis with a short timeout. The sandbox recorded one
image.exe thread sleeping 525 times.
#70–103: pick a wallet from a file timestamp. What:
GetFileTime $R0\image.exe feeds integer arithmetic that yields an index; the
script then reads tmp.ini line by line and slices out that pool entry on the
'-o and -p delimiters. Why it matters:
the choice is not C2-driven, and because the dropped file's mtime is fixed
once written, it is sticky per host across reboots. Victims spread
pseudo-randomly but stably across twenty payout addresses.
#113–124: the second stage that did not fire. What:
ReadINIStr pulls tmp.ini [section2] file; had it held a URL, the
script would fetch it to %TEMP%\temp.exe and run it SW_HIDE.
Why it didn't: the key is empty in this build, so #115 branches past the whole
block. Confirmed independently: the string temp.exe appears
zero times in the sandbox report. Why you should care anyway:
this is a fully intact download-and-execute primitive whose destination is a single line in
a text file.
#125–140: architecture detection. Via the NSIS System plugin:
kernel32::GetCurrentProcess() then
kernel32::IsWow64Process(is,*i.s) → Win64 or
Win32.
#149–153: launch the miners. On 64-bit,
NsCpuCNMiner64.exe and NsGpuCNMiner.exe, each with
-dbg -1 -o stratum+tcp://mine.moneropool[.]com:3333 -u <wallet> -p x. On
32-bit, only NsCpuCNMiner32.exe — the branch at #151 jumps clean over the
GPU miner. Impact assessments have to distinguish the two: a 32-bit victim gets one miner, a
64-bit victim gets its CPU and its GPU pegged simultaneously.
#154 and #156: persist, twice.
HKCU\Software\Microsoft\Windows\CurrentVersion\Run value
Coin = $R0\$EXEFILE, and a shortcut
image.lnk in the Start Menu Startup folder pointing at the same file
(dropped, SHA-256 589172b2fae9782cee660bc71395242bb6d7dfd1e7251665954be392c4ba9180).
Two mechanisms, either of which alone re-establishes the infection on logon. Both are
HKCU and user-level: every manifest in the chain requests
asInvoker, and there is no UAC bypass or elevation attempt anywhere.
#155: destroy the evidence. DeleteFile $R0\tmp.ini. The file
holding all twenty wallets is removed the moment one has been chosen.
#166–174: spread. Walk the letter table
+CDEFGHIJKLMNOPQRSTUVWXYZ and issue
CopyFiles $EXEPATH → <X>:\images.scr for each, pausing 2 s between
drives. The sandbox's NSIS detail log preserved Copy to C:\images.scr and
Copy to X:\images.scr, and its API log shows direct volume access to
D: and Z: plus \PIPE\wkssvc and
\PIPE\DAV RPC SERVICE, which is mapped-drive and WebDAV resolution.
The reconstruction validates itself
This is the part that makes the whole teardown trustworthy rather than plausible. The
branch graph predicts precisely which beacons can fire: test8 twice, once per
pass, and test11, test12, test13,
test14 once each. That is six logical fetches, each attempted on port 80 and
then on port 443, for twelve HTTP requests.
The sandbox report contains exactly twelve, with test8
appearing twice, and no request at all for test9 or
test10. Prediction and packet capture agree completely, which is about as good
as static-versus-dynamic corroboration gets.
The beacon funnel, read correctly
Seven URLs are woven through the chain, one per install stage:
| # | URL | Fires after | Observed |
|---|---|---|---|
| 1 | /test8.txt | process start (both passes) | 2×, 301 → 404 |
| 2 | /test9.txt | unreachable branch | never |
| 3 | /test10.txt | unreachable branch | never |
| 4 | /test11.txt | payloads dropped + 30 s stall | 301 → 404 |
| 5 | /test12.txt | wallet selected | 301 → 404 |
| 6 | /test13.txt | architecture detected | 301 → 404 |
| 7 | /test14.txt | miners running + persistence set | 301 → 404 |
Every one downloads to temp.txt and is then
deleted without being parsed. They carry no victim data and receive no
configuration. They are a conversion funnel: the operator counting how many
infections survive each stage, instrumented exactly as a growth team would instrument a
signup flow. It is the same pattern as the ?event=pasted callback in
the ClickFix teardown, eleven years
earlier and with a text file instead of a JSON API.
All seven carry User-Agent: NSIS_Inetc (Mozilla), the Inetc plugin default,
and that turns out to be the only thing network detection caught: Suricata produced
exactly one alert across the whole capture,
ET USER_AGENTS Observed Suspicious UA (NSIS_Inetc (Mozilla)), SID
2011227. One alert, on the least interesting property of the least interesting
request.
The host was alive; the files were gone
Every beacon got a two-hop failure: port 80 returned a
301 Moved Permanently from nginx with
Location: https://testswork[.]ru/test<N>.txt, and the HTTPS leg then
returned 404 Not Found. The sandbox saved these bodies; all five 301 pages are
byte-identical at 162 bytes, because they are all the same nginx error page.
It would be wrong to call the C2 "dead" in January 2026, and the distinction matters. The
host was alive, maintained and instrumented: a valid Let's Encrypt certificate
roughly 89 days old, a per-request W3C traceparent header, and an edge
identifier X-ID-FE: fr5-hw-edge-gpig-gc53. What had disappeared were the
/test<N>.txt files. The site was serving normally; the malware's drop
files had been removed from its web root. That is exactly the behaviour of a
compromised legitimate site that has since been cleaned.
The malware neither notices nor cares, because it never inspects a status code. Which is why the install completed in full despite total C2 failure.
The wallets, and the twenty-first one
tmp.ini holds twenty entries, pool0 through
pool19, each a complete miner argument string pointing at the same
pool with a different wallet. I validated all twenty properly rather than by
regex: base58-decode to 69 bytes, check the network byte (0x12, Monero mainnet
standard), and recompute the Keccak-256 checksum over the first 65 bytes, cross-checking a
pure-Python Keccak against PyCryptodome.
20 / 20 valid, 20 distinct public spend keys, 20 distinct public view
keys, no shared prefix beyond two characters. Genuinely independent wallets, not derived
subaddresses. The one selected at runtime was pool17.
Why twenty against one pool? The arithmetic explains it. A five-thousand-host campaign at an assumed 75 H/s per host is about 375 kH/s. Against a 2014-era Monero network of roughly 15 MH/s that is ~2.5% of global hashrate arriving at a single payout address, conspicuous enough for a public pool to notice and ban. Divided twenty ways it is ~18.75 kH/s per address, which looks like an ordinary small mining farm. The wallet count buys camouflage from the pool operator and resilience if individual addresses are banned. (Those hashrate figures are stated assumptions, not measurements.)
A wallet that exists in no file
Sweeping all nine files with an anchored base58 pattern in both ASCII and UTF-16 returns
zero wallet hits outside tmp.ini itself. Searching the process memory
of the running CPU miner returns something else:
47mr7jYTroxQMwdKoPQuJoc9Vs9S9qCUAL6Ek4qyNFWJdqgBZRn4RYY2QjQfqEMJZVWPscupSgaqmUn1dpdUTC4fQsu3yjN
# 95 chars, 69 raw bytes, network byte 0x12
# keccak-256 checksum computes to a3d5ee09, matching the address
# random base58 passes that check with probability ~2^-32
It is resident in six memory regions of
NsCpuCNMiner64.exe across all three dump generations, and at offset
0x2b6b0 of the PAGE_READWRITE region based at
0x2A45E000 it sits as a NUL-terminated C string in a zero-padded fixed
buffer: an allocated string field, not incidental high-entropy data. It appears in
no file on disk at all, because the miners are VMProtect-packed and their
string data only materialises once the packer decrypts it at runtime.
| PID | Process | Distinct wallets in memory | Which |
|---|---|---|---|
7312 | NsCpuCNMiner64.exe | 2 | pool17 (×25) and 47mr7jYT… (×6) |
8160 | image.exe | 1 | pool17 only (×8) |
7020 | NsGpuCNMiner.exe | 0 | none, consistent with an immediate exit on a GPU-less host |
Only pool17 was loaded from the config, which confirms a single selection
rather than the whole file being parsed. But the CPU miner carries an extra address of its
own, and the next section says what it is for.
The payloads: VMProtect, Claymore, and a dev fee that stops mining
All three miners are VMProtect-packed. Each carries .vmp0 and
.vmp1 sections marked read/write/execute, while the original
.text, .rdata and .data have a
raw size of zero: present in the section table, empty on disk. Only 13–14
imports survive. The three miners carry no version block and no icon at all
— VMProtect discarded their resources, which is why the operator had to give them
descriptive filenames instead.
Two developer artifacts survive the packing in plaintext, and they are the strongest attribution evidence in the sample:
PDB pathsNsCpuCNMiner64.exe E:\CryptoNight\bitmonero-master\src\miner\x64\CPU-Release\Crypto.pdb
NsGpuCNMiner.exe E:\CryptoNight\bitmonero-master\src\miner\x64\POOL\Crypto.pdb
Not repacked public binaries: compiled by the operator from a
bitmonero-master source tree on drive E:, under Visual Studio
build configurations named CPU-Release and POOL.
The packer is beaten by the memory dumps, not by unpacking
Everything VMProtect hides on disk is sitting in plaintext in the running process. From
the unpacked string table in PID 7312, region 0x2A419000:
0x1b010 47mr7jYTroxQMwdKoPQuJoc9Vs9S9qCUAL6Ek4qyNFWJdqgBZRn4RYY2QjQfqEMJZVWPscupSgaqmUn1dpdUTC4fQsu3yjN
0x1bd00 DevFee:
0x1c050 SOMETHING WRONG WITH DEVFEE CONNECTION. STOP MINING.
0x1c25d Claymore CryptoNote %s Miner v%s Beta
simpleminer.cpp simpleminer/0.1
vasrashpil@gmail.com
%s%s - SHARE FOUND (target %d) - (%s %d of %d)
{"method": "login", "params": {"login": "%s", "pass": "%s", "agent": "cpuminer-multi/0.1"
stratum+tcp://mine.moneropool.com:3333 # 26 occurrences
That banner identifies the payload as Claymore's CryptoNote Miner, a
closed-source Monero miner of the 2014–2016 period, rather than a generic repack. The
presence of simpleminer.cpp and the agent string simpleminer/0.1
shows it was built on the CryptoNote reference simpleminer
code, which is exactly what lives in src/miner/ of the bitmonero source tree,
reconciling perfectly with that PDB path. Both agent strings are sent in the clear in the
stratum login request, which makes them excellent network signatures. The table
also yields a contact address, vasrashpil@gmail.com; I report it as an artifact
and draw no conclusion about whose it is.
And it settles the wallet question. The literal DevFee: sits
0xCF0 bytes after that address, with
SOMETHING WRONG WITH DEVFEE CONNECTION. STOP MINING. just beyond it. All four
artifacts within about 4.7 KB of one another in the same string table. The twenty-first
wallet is the dev-fee payout address compiled into the miner. It is
arguably a better pivot than the twenty configured ones, because it belongs to whoever
built the miner rather than whoever ran this campaign, and can therefore
link operations that share tooling but nothing else.
That error string also reveals a behaviour worth knowing: this miner stops mining entirely if it cannot reach its dev-fee pool. On any host where the dev-fee destination is unreachable, which today it certainly is, the miner terminates its own workload.
And it explains -dbg -1
The flag looks like "enable debugging", and I was ready to leave it undetermined rather
than guess at packed code. The unpacked option table resolves it: -dbg
takes a value (the miner carries the error
Missed option value after -dbg option), and the value supplied is
-1, which disables the log file. The literal
_log.txt exists in the unpacked .rdata, yet no formatted
*_log.txt filename appears in any of the 54 dumps and _log.txt
appears zero times in the sandbox report. So -dbg -1 is the operator
switching logging off, and it is why no miner console output survives
anywhere.
Data.bin: what I can and cannot say
78,642 bytes, Shannon entropy 7.9804 bits/byte with all 256 byte values
present, header a9 02 4f be matching no known container magic. I ruled out the
easy explanations rather than guessing: the length is
not block-aligned (78,642 mod 16 = 2), there is no ECB
structure (2 repeated 16-byte blocks out of 4,915), and no single-byte
XOR key in 0x01–0xFF yields MZ, PK,
ELF, __kernel, .version, gzip, bzip2 or xz magic at
any offset. Conclusion: a compressed or stream-encrypted opaque blob.
The hypothesis, labelled as one, is that it is the encrypted OpenCL
compute kernel for the GPU miner: NsGpuCNMiner.exe is the only payload that
imports opencl.dll, the only one that would need an external device-code blob,
and the script drops Data.bin beside it while never reading it itself. Runtime
evidence confirms the loader was never exercised: the GPU miner never loaded
opencl.dll and never opened Data.bin,
and its process memory holds no wallet at all. The hypothesis stands untested.
Network reality: nothing mined
Filtering the capture to the stratum ports returns 54 packets, every one a SYN or a
retransmitted SYN to 188.114.96.3:3333. There is
not a single SYN-ACK.
$ tshark -r capture.pcap -Y 'tcp.port==3333 || tcp.port==1111'
344 931.662 192.168.0.2 -> 188.114.96.3 TCP 49768 -> 3333 [SYN]
345 931.662 192.168.0.2 -> 188.114.96.3 TCP 49769 -> 3333 [SYN]
350 934.663 192.168.0.2 -> 188.114.96.3 TCP [TCP Retransmission] 49768 -> 3333 [SYN]
351 934.663 192.168.0.2 -> 188.114.96.3 TCP [TCP Retransmission] 49769 -> 3333 [SYN]
... 54 packets total, zero SYN-ACK, zero stratum bytes exchanged
The cause is infrastructure, not sandboxing. mine.moneropool[.]com resolved to
188.114.96.3 and 188.114.97.3, Cloudflare edge addresses. Cloudflare's
proxy serves HTTP/HTTPS ports only; TCP 3333 is not proxied, so the stratum port is simply
unreachable through the edge. No login, no job template, no submitted share.
Note the SYNs arriving in pairs on adjacent source ports (49768+49769, 49782+49783, 49788+49789, 49794+49795), each pair retried about three times with a fresh pair every ~20 s. That is the CPU miner and the GPU miner independently retrying, which matches the reconstruction's simultaneous launch of both. Small detail, but it is independent confirmation that #152 and #153 both ran.
So the corrected impact: the dropper completed its entire local kill chain — self-copy, payload drop, both persistence mechanisms, drive-root propagation — while both of its network channels failed completely, because it never checks whether any network operation succeeded. Revenue was zero. Persistence and propagation were total.
Separating malware traffic from sandbox noise
Only two DNS names in the capture are attributable to the malware:
testswork[.]ru → 81.28.12.12, and mine.moneropool[.]com
→ the Cloudflare pair. Everything else — login.live.com,
client.wns.windows.com, dns.msftncsi.com,
www.msftconnecttest.com, win1710.ipv6.microsoft.com,
maps.windows.com and the associated 20.190.160.x / 172.187.86.x /
23.200.147.154 flows — is ordinary Windows 10 telemetry and connectivity checking from
the analysis VM. It reads as suspicious volume in a report and
must not be promoted to indicators.
Campaign infrastructure
| Host | Role | At detonation | Today (2026-09-10) |
|---|---|---|---|
testswork[.]ru |
beacon / telemetry | 81.28.12.12, nginx on :80 redirecting to :443 | unregistered |
mine.moneropool[.]com |
stratum pool (public, all 20 wallets) | 188.114.96.3 / .97.3 Cloudflare | 65.108.204.181 Hetzner, 80/443 closed |
xmr.hashinvest[.]net |
failover pool, port 1111 | not contacted | no A record |
monero.crypto-pool[.]fr |
failover pool | not contacted | no A record (apex still up) |
mine.cryptoescrow[.]eu |
failover pool | not contacted | no A record |
A trap to avoid: moneropool[.]com and
crypto-pool[.]fr were legitimate public Monero pools. They are victims
of abuse here, not attacker assets, and labelling them C2 in a report is both wrong and
actively unhelpful to anyone consuming it.
Two configuration files, two different consumers, which looks like a contradiction until
you check: tmp.ini lists only mine.moneropool[.]com:3333 across
all twenty entries, while pools.txt lists four pools. The
NSIS script never reads pools.txt — it appears nowhere in
the recovered instruction stream except as a filename to extract — whereas the
miner binary does, as its own failover list. In this detonation the
failover was never exercised, because the miners never got far enough to fail over.
Investigating the operator
What can be responsibly inferred about who ran this? The short version: this is commodity, financially-motivated e-crime built on off-the-shelf tooling, and the operator is not identifiable from what we hold. The reasoning, with confidence stated:
1. The telemetry host looks compromised, not registered
This is the finding most likely to be reported incorrectly. The domain's archived history
does not look like attacker infrastructure. Wayback holds captures from 2011
of a small Russian site about employment questionnaires, with pages such as
anketi_pri_prieme_na_rabotu_ovd.htm, matching the name
("tests work"), and then from 2024–2026 a Russian clickbait
content farm with a named author byline and categories raznoe and
rejting. At detonation it served a valid, properly renewed Let's Encrypt
certificate.
A fifteen-year-old site with real content and maintained TLS is a poor fit for purpose-registered malware infrastructure and a good fit for a compromised or otherwise abused host used as a dead-drop. The beacon paths are consistent with files dropped into a web root. Confidence: medium-high.
It is now unregistered, which retires it as an active threat and creates a different one: anyone can re-register it, including to re-point a still-installed population of this worm. A lapsed C2 domain in a family that persists across reboots is a standing risk and a candidate for defensive registration.
2. Public pools mean there is no C2 to seize
The operator ran no mining infrastructure of their own: no private pool, no proxy, just twenty wallets pointed at public pools. That buys zero infrastructure cost and traffic that blends with legitimate mining, at the price of exposing the wallets to public pool statistics and to bans. It also means there is nothing to take down. Confidence: high, this is deliberate and characteristic of commodity mining crews rather than of anyone who needs interactive control.
3. The tooling is bought, not built
Claymore's CryptoNote Miner was a widely-distributed closed-source product;
simpleminer is CryptoNote reference code; VMProtect is a commercial packer;
NSIS is a mainstream installer. The only artifact that is plausibly the operator's own work
is the dropper script. That is the profile of somebody assembling
commodity parts, and the dev-fee address means even the miner's author was taking a
cut of their revenue.
4. What does not support attribution
- The
.rudomain does not make the operator Russian. It is a cheap TLD and, on the evidence above, probably not even theirs. Registrar and TLD geography tell you where it was cheapest to buy or easiest to compromise, not where anybody sits. - The Cloudflare IPs tell you nothing. Every stratum address we resolved is shared anycast. Geolocating it returns Cloudflare's datacenters.
- The email address in the miner is the miner author's surface, not the operator's. It ships inside a product that many unrelated operators ran. Treating it as this campaign's owner would be a category error.
- The
E:\build path belongs to whoever compiled the miner, which the dev-fee mechanism strongly suggests is not the person who ran this campaign.
5. Attribution verdict
Operator: unknown. Ecosystem: commodity CryptoNight mining trojans of the
2014–2016 era, distributed in NSIS wrappers with fake-folder .scr lures,
a pattern reused by many unrelated crews. The campaign-specific parts — the dropper
logic, the beacon URL scheme, the Coin registry value, the folder-lure
branding, the twenty-wallet set — are the only things that cluster this sample, and
they cluster it with a technique, not with a group.
The honest headline: we can classify the ecosystem with reasonable confidence and the operator not at all. The highest-value next step is not more infrastructure analysis but blockchain research against the twenty-one addresses, which would size the campaign's actual take. I deliberately did not attempt it: it needs pool-statistics and explorer work that is a separate exercise from reversing the binary.
Detection engineering: detect the script, not the detonation
The single Suricata alert fired on a User-Agent string. Useful, and about as shallow as a detection can be: the operator changes one literal and it is gone. What they cannot cheaply change is the shape of the behaviour, because it is dictated by what the malware is for.
So the detections worth building, most durable first:
Any process created with stratum+tcp:// on its command
line. This single rule catches essentially every commodity mining trojan
regardless of family or vintage, and has almost no legitimate counterpart on a corporate
endpoint.
Supporting signals, in rough order of durability:
- A
.scrfile executing from a drive root,%TEMP%or%APPDATA%. Legitimate screensavers live in%SystemRoot%\System32. - Appearance of
images.scr, or any executable, at the root of a removable or mapped drive. - A registry
Runvalue namedCoin, or anyRunvalue whose data points into anImagesdirectory. - A
.lnkcreated in the Startup folder pointing into%APPDATA%. - Sustained simultaneous CPU and GPU saturation by a non-interactive process.
One scoping point matters for this family specifically, and it generalises to anything
VMProtect-packed: the strings that identify the payload do not exist in the
files. File-scanning YARA has to anchor on the PDB path and the
.vmp0/.vmp1 section layout, while the product banner, the dev-fee
strings and the dev-fee wallet are reachable only by scanning memory. A
rule built from the memory strings and deployed as a file signature will never fire, and
will look like coverage while providing none. Mark the scope in the rule.
The other discipline is negative: a rule that anchors on NSIS structure alone is a
false-positive machine, because every legitimate Nullsoft installer matches it. My ruleset
anchors instead on pool0="-o stratum+tcp://, the
E:\CryptoNight\bitmonero-master build path, and the payload filenames in the
NSIS string table — and I checked it, rather than assuming: zero false
positives across 854 system binaries and every non-sample PE on the analysis host,
with correct hits on the sample set.
Remediation, in this order
- Capture before you kill. Take a memory image of every
*CNMiner*.exeprocess and record its command line.tmp.iniis already deleted, so the process is the only place the configured wallet still exists, and process memory is the only place the dev-fee address is visible at all. A file collection alone loses both. - Terminate
image.exeand the miners. - Delete the
CoinRun value and the Startupimage.lnk. - Remove the install directory, checking all five candidate locations.
- Sweep every drive root for
images.scr, C: through Z:, including removable media and mapped shares. Skipping this reinfects the host and everyone else with access to those shares. This is the step most likely to be missed. - Hunt laterally: any file server or USB device that ever held
images.scris a distribution point. - Check for
%TEMP%\temp.exe. Its presence means the second stage did fire on that host and an unknown payload ran. Escalate.
Hard lessons
- I got one wrong, and the way I got it wrong is the lesson. I initially
reported that the sandbox had missed the
CoinRun key, on the strength of a case-sensitive search returning zero hits. It had not missed it. Falcon Sandbox upper-cases registry paths, key names and value names in its signature descriptions, so the record is stored asCOIN, present three times, twice in threat-level-1 signatures, with the complete tuple.- Fix: search sandbox reports for registry artifacts case-insensitively, or you will conclude that observed behaviour was never observed. A zero-hit grep is evidence about your grep before it is evidence about the malware.
- Worth noting: the correction made the finding stronger. Statically reconstructed instruction #154 turned out to be corroborated at runtime on path, value name and data. An error that resolves in your favour is still an error, and the published version has to say so.
- Unpack the logic, not just the files. Every capability the detonation did not exercise — the two unreachable beacon URLs, the second-stage downloader, the five-path install fallback, the wallet-selection algorithm, the full C:–Z: enumeration — lives in a code path that either never ran or was not recorded. Where a sample's logic is a compiled script (NSIS, AutoIt, Inno, any installer framework), decompiling it is not extra rigour; it is the only way to enumerate what the malware has rather than what it happened to use.
- "Old and broken" is a statement about one capability, not about the
sample. Three properties survive the mining being dead: it spreads to network
shares, so one infected laptop seeds file servers; it re-establishes itself on every logon
from two locations; and its second-stage loader needs one line in a text file to deliver
current malware. A host still running this has an unattended, user-level, auto-starting
remote-code-execution channel.
- What this argues for: triage capability-by-capability rather than verdict-by-verdict. "Miner, obsolete, low priority" and "self-propagating loader with intact persistence" are the same file.
- A long technique list is not thoroughness, and its length hides its
gaps. 101 techniques including Email Collection and Process Hollowing, while
omitting the two that define the sample. The volume is what makes the omissions
invisible.
- Fix: require an artifact citation per technique before it enters a report. Anything that cannot cite one is a hypothesis, and belongs in a section labelled as such. 15 cited techniques are worth more than 101 uncited ones.
- Never promote a CDN or anycast IP to a blocklist.
188.114.96.3appears in the sandbox's own IoC export as a malicious IP. It is shared Cloudflare edge space. Blocking it breaks a large part of the web and does nothing to the pool, whose real origin remains hidden.- Fix: mandatory fleet-prevalence check before any indicator enters a blocklist. Block campaign domains by name; treat every IP in a sandbox report as context until proven otherwise.
- Sandbox noise reads as campaign traffic. The capture is mostly Windows
telemetry:
login.live.com,client.wns.windows.com,msftconnecttestand friends. Two names out of the set belong to the malware. Fix: establish the analysis VM's own baseline before attributing any flow to the sample. - Memory beats static unpacking, for a whole class of malware. The
product name, the dev-fee wallet, the JSON-RPC templates, the agent strings and the
meaning of
-dbg -1were all unreadable in the files and trivially readable in the process. I did not defeat VMProtect and did not need to.- What this argues for: when a sample is commercially packed, ask for process dumps before investing in unpacking. Then write your detections for the surface you can actually see.
- Say "unknown" out loud. A
.rubeacon domain and Hong-Kong- or Russia-adjacent tooling invite a confident geographic claim, and there is nothing here to support one; the domain probably was not even the operator's. Writing "operator: unknown" is a finding, and it is what stops a phantom actor from being inherited by the next analyst as established fact.
What went well: the branch-graph reconstruction predicted the exact HTTP request count before I looked at the capture, which is the strongest form of validation available for this kind of work. Every hash in the report was recomputed from the files rather than copied from the sandbox, which is how the case-sensitivity error got caught at all. And the detection content was tested against a benign corpus rather than shipped on the assumption that specific-looking strings are specific.
Indicators of compromise
Block the beacon domain by name. Treat every IP here as context only: the stratum addresses are shared Cloudflare anycast and blocking them will cause an outage.
| Type | Value | Note |
|---|---|---|
| SHA-256 | 8bee95131ae47d9a5e3c8cccceaaad7e5567eac66ae7c0d875c9a57d3fc7acef |
The dropper. Also image.exe and images.scr: same bytes,
three names. |
| SHA-256 | a0eba3fda0d7b22a5d694105ec700df7c7012ddc4ae611c3071ef858e2c69f08 |
NsCpuCNMiner32.exe, VMProtect, 2014-07-23. |
| SHA-256 | d0326f0ddce4c00f93682e3a6f55a3125f6387e959e9ed6c5e5584e78e737078 |
NsCpuCNMiner64.exe, the build that ran. |
| SHA-256 | 7a2a860bb344526e8546acd172522b4d276a4647f43dd4720281d40e390b283e |
NsGpuCNMiner.exe, imports opencl.dll. |
| SHA-256 | 932055827d87637ba7e11565f22dda3f09cc9457769788d94413e96cf346a6e4 |
Data.bin, entropy 7.9804, unidentified blob. |
| SHA-256 | 85ac75d560d635313a88fd2cafdbbee84ec9a4263d40b138e1f0c52e3dbdca36 |
tmp.ini, the 20-wallet config. Deleted at runtime. |
| SHA-256 | 589172b2fae9782cee660bc71395242bb6d7dfd1e7251665954be392c4ba9180 |
image.lnk, the Startup persistence shortcut. |
| Domain | testswork[.]ru |
Beacon host. Now unregistered; likely a compromised legitimate site. |
| URL | http://testswork[.]ru/test{8,9,10,11,12,13,14}.txt |
Seven stage beacons. test9/test10 are dead code, never
contacted, and appear in no public report. |
| User-Agent | NSIS_Inetc (Mozilla) |
Inetc plugin default. The only thing Suricata caught (ET SID
2011227). |
| Registry | HKCU\Software\Microsoft\Windows\CurrentVersion\Run → value
Coin |
Persistence 1 of 2. Data: %APPDATA%\Images\image.exe.
Search case-insensitively. |
| Path | %APPDATA%\Images\ |
Install dir. Fallbacks: C:\Images, D:\Images,
%USERPROFILE%\Images, %TEMP%\Images. |
| Path | <drive>:\images.scr |
Propagation, every root C: to Z:, incl. removable and mapped. |
| Path | %TEMP%\images.scr |
The decoy. A directory, not a file. |
| Path | %TEMP%\temp.exe |
Second-stage target. Dormant here; its presence means the stage fired. |
| PDB | E:\CryptoNight\bitmonero-master\src\miner\x64\{CPU-Release,POOL}\Crypto.pdb |
Best retro-hunt pivot for sibling builds. |
| XMR wallet | 43tjagd2e8d4GXzYn5xmysYmDnLbvvZSHFPbMWtg4Cs1DLwztfENYbNBz8Y8fmuhpCXFHDzXUWn2QZwhswsNtgzTM8v899K |
pool17, the one selected at runtime. All 20 in the full report; all
checksum-verified. |
| XMR wallet | 47mr7jYTroxQMwdKoPQuJoc9Vs9S9qCUAL6Ek4qyNFWJdqgBZRn4RYY2QjQfqEMJZVWPscupSgaqmUn1dpdUTC4fQsu3yjN |
Dev fee, memory-only. In no file on disk. Belongs to the miner's author, not necessarily this operator. |
| Memory string | Claymore CryptoNote %s Miner v%s Beta,
SOMETHING WRONG WITH DEVFEE CONNECTION. STOP MINING.,
simpleminer.cpp |
Memory scanning only. Zero hits against any file: VMProtect hides them until runtime. |
| IP (DO NOT BLOCK) | 188.114.96.3, 188.114.97.3 |
Cloudflare anycast, shared with the open web. Present in the sandbox's own IoC export. Context only. |
| IP (context) | 81.28.12.12, 65.108.204.181 |
Beacon host at detonation; pool host as of 2026-09-10 (Hetzner, ports closed). |
| Not an IoC | nsis.sf.net/nsis_error |
The benign Nullsoft "installer corrupted" help page, referenced by every NSIS stub. Exported as a URL indicator by the sandbox. |
Behavioral / host artifacts
- Miner command line:
-dbg -1 -o stratum+tcp://<pool>:3333 -u <95-char base58> -p x(-dbg -1= logging off). - Config shape:
pool0="-o stratum+tcp://... -u <wallet> -p x"throughpool19, plus[Section10] name=cryptonote. - Second-stage config key:
tmp.ini [Section2] file=— empty in this build. - Process shape:
*.scr→image.exe(SW_HIDE, same SHA-256) → two miners with identical stratum arguments. - PE shape:
.vmp0+.vmp1RWX,.textraw size 0, 13–14 imports, no version block, no icon. - Stratum agent strings, sent in cleartext:
cpuminer-multi/0.1,simpleminer/0.1.
Takeaways
- The detonation is one path through the program; the script is the program. Six capabilities in this sample never executed, including a download-and-execute stage. Decompile the installer.
- Triage capabilities, not verdicts. "Obsolete miner" and "self-propagating loader with intact persistence and a dormant RCE channel" are the same file. Only one of those gets remediated properly.
- A commercial packer moves the evidence, it does not delete it. Product name, dev-fee wallet, agent strings and flag semantics all came out of process memory without touching VMProtect. Ask for dumps.
- Count your techniques against your artifacts. 101 heuristic techniques missed the two that define the malware. A citation requirement would have caught it.
- Case sensitivity is a finding-killer. One capital letter in a sandbox report cost me a wrong claim in a published draft. A zero-hit search says something about the search first.
- State non-attribution as non-attribution. The ecosystem is knowable, the operator is not, and the beacon domain was probably somebody else's website. Writing down which is which is what keeps a phantom actor out of the next analyst's baseline.