services research about contact ↗

The nvidia-smi Line That Hid a Botnet's Wallet

A too-permissive sudo shim, a sealed detonation lab, and four rounds of fighting fake C2 to pull one cryptomining crew's Zephyr wallet out of a $6 honeypot.

Here's a dumb reason a piece of malware wouldn't cough up its crypto wallet: my fake GPUs weren't convincing enough.

The thing had already done the hard part. Brute-forced the login, escalated, dropped its toolkit, set up persistence. Then it ran nvidia-smi -q, looked at what came back, decided my "server" had no GPUs worth using, and stopped cold. It told its operator GPUs: No and never started mining.

That was a problem, because the wallet address lives in exactly one place: the login packet the miner sends its pool, which it only sends once it believes it's on a host worth mining. Not in the binary. Not served by the C2. A couple of missing lines of fake nvidia-smi output were the whole difference between "caught some cryptojacking malware, neat" and pulling the actual wallet these people mine into.

Spoiler up front, because I hate writeups that make you wait for it. The crew is Diicot, sometimes tracked as Mexals, a Romanian SSH-cryptojacking operation. The wallet is a Zephyr (ZEPH) address, mined with XMRig. I got it out of a fully sealed lab detonation without ever sending them a mining share or a single byte to their exfil endpoint, and that restraint is most of the point.

The family that kept walking away

I run a high-interaction SSH honeypot on a throwaway Contabo box. Cowrie sits out front, the usual fake shell that logs every keystroke and hands attackers a harmless sandbox. For a short allowlist of the interesting ones, there's a real containerized shell behind it with egress locked down, so I can watch an actual intrusion run instead of reconstructing it from typed commands.

Most SSH honeypot traffic is noise: Mirai, generic brute-forcers, coin miners that fire three commands and leave. But one group kept nagging at me. They'd show up, poke around, and leave without dropping anything. When I went back through the logs I counted more than 120 sessions from them. Zero payloads, every single time.

Then I noticed what they checked right before bailing. This crew, coming out of 91.92.40.0/24, gates everything on sudo. The first thing they run is roughly:

echo '<guessed-password>' | sudo -S sh -c '<recon>'

And real sudo did exactly what it's supposed to. It rejected the guessed password, printed its prompt to stderr, exited non-zero. Their controller saw that, decided the account was useless, and quit. My "high-interaction" trap was too honest to catch them.

So I made it dishonest. I swapped sudo for a shim that emulates passwordless sudo, but only for the specific ways this campaign invokes it — it consumes their credential prompt and passes the wrapped command straight through, so from the controller's side the box looks like a sloppy host where their stolen creds just work. (Matching every invocation shape they throw is fiddlier than it sounds, and a mismatch is its own tell; the shim ships with a test suite to keep it honest about being dishonest.)

Next time 91.92.40.x came knocking, the shim said yes. They deployed. First drop was Diicot.

Let me be straight about what's new here and what isn't. Diicot isn't a discovery. Bitdefender, Cado, and others have written about this crew's SSH-cryptojacking for years, and if you've read those, the playbook won't shock you. What I've got that's worth something is current: this month's binaries, this month's infrastructure, this month's live wallet, and a method that gets you from "some stranger ran a binary on my box" to "here's their wallet" without hand-waving.

A Go binary in three layers of wrapping paper

The first drop was a 15.6 MB ELF sitting in /tmp, named IQITtfbr. Static triage takes thirty seconds and tells you how bad it's going to be:

file IQITtfbr        # ELF 64-bit, statically linked, no section header
readelf -h IQITtfbr  # no section header → packed, basically always
strings -n6 IQITtfbr | grep -iE 'UPX|upx!|go1\.|golang|garble'

Three layers.

UPX 4.23 on the outside. Easy. upx -d gave back a 21 MB binary because the magic bytes were intact.

Go underneath. Normally that's good news, since Go binaries strip into readable runtime paths and analysis gets easy.

Except they'd run it through garble, which scrambles the symbol names and, worse, encrypts the string literals so they only exist decrypted in memory at the moment they're used. That's the wall. The wallet, the pool, the C2 IPs, none of it shows up in strings, because none of it is plaintext until the program decrypts it right before it needs it.

You can't read your way to the answer. You have to run it and watch, which is of course the exact thing a honeypot analyst isn't supposed to do on a whim.

Detonating it (and the malware fighting back)

Running live malware to the end is only fine if it truly can't reach anything. My detonation VM sits on an isolated Proxmox bridge with no physical uplink. The only route to the internet is a pfSense VM, and if I stop that VM the whole segment dead-ends. No firewall rules to get wrong. The pre-run check is stupid-simple: no default route, air-gap up, every known C2 sinkholed to loopback, and a curl to 1.1.1.1 from inside has to fail instantly. If it hangs instead of failing, stop. A hang means there's a route you didn't account for.

Sealed up, I ran it under strace. Diicot does not go quietly.

It kills the competition first. Before anything else it pkill -9s a whole list (python, SRBMiner, kswapd0, xMEu, and roughly a dozen other rival-miner process names) to evict the competition off the box. My sinkhole listeners were Python. It killed them. It also killed the QEMU guest agent I was driving the VM with, which was a fun surprise mid-run. Takeaway: don't write your fake-C2 listeners in anything on that kill list, and plan to lose remote control and recover artifacts offline. Stop the VM, qemu-nbd its disk read-only, read the loot from outside.

It self-wipes. The execution directory deletes itself on the way out, so you either capture somewhere else or you capture nothing.

And it backdoors everything. It changes the root SSH password to something it controls and, the useful part for attribution, drops its own key into every account's authorized_keys. The key comment is ElPatrono1337, a known Diicot handle. Persistence is cron: an @reboot job plus a once-a-minute one pointing at a random-named binary under /var/tmp.

The detonation coughed up the rest of the kit: a loader (diicot), an SSH brute-forcer (kuak), a spreader (cache), and that persisted miner-loader in /var/tmp. Still no wallet, though, because the miner still wouldn't start. It wanted GPUs.

Four rounds to the wallet

Here's where the real work is. The wallet lives in the miner's stratum login, the "credit my shares to this address" packet it sends the pool. It only sends that after it's convinced it's on a real, worthwhile machine and it reaches its pool. Faking all of that inside a sealed room took four passes.

Round 1: open the GPU gate. Diicot checks GPUs with nvidia-smi -q | grep "Product Name". My fake nvidia-smi only ever printed the summary table, the kind you get from a bare nvidia-smi with no flags. No -q detailed output, so the grep found nothing, the beacon reported GPUs: No, and the miner never fired. The fix was embarrassingly small: make the shim answer -q with four blocks of

Product Name                    : Tesla V100S-PCIE-32GB

and answer -L with the matching four-GPU list. That's the "one line" from the title, except it's four. With the detailed query answered, the beacon reported a 4× V100S rig and the miner decided I was worth its time.

Round 2: make the rest of the conversation land. A miner that starts still hangs if its C2 handshake times out. So I sinkholed each C2 to loopback and stood up responders that spoke the right shape back, non-Python this time for obvious reasons. The exfil PUT got a 204, the "what's my public IP" check got a believable public address, the gate POST got a 200. Enough that the malware thought its infrastructure was healthy and kept going.

Round 3: terminate the TLS the pool speaks. The miner talks to its pool over stratum-in-TLS on 443, so a plain listener just sees garbage. I terminated the TLS myself with openssl s_server and a junk cert, then logged the cleartext. One gotcha cost me a while: s_server needs stdin held open or it dies before the handshake finishes, so a bare </dev/null kills it. Pipe a sleep into it instead. The first decrypted message was the whole prize (trimmed for readability — the real packet also carries id/jsonrpc, a longer agent string, and XMRig's full algo list):

{"method":"login","params":{
  "login":"ZEPHYR35u3wKAs4jLTGfGD4yDsJc92M3yRr3gEHCmRutfnDwfwgZizqF8q7en82SRR6BiNDfukphgayqw5CU5Pue36PQCKHx1qd1v",
  "pass":"user",
  "agent":"XMRig/6.26.0 (Linux x86_64)"}}

There's the wallet, embedded in the miner, not served by the C2. Which is exactly why poking at the C2 server all day would never have produced it. You have to get the miner talking.

Round 4: confirm it's real, without playing along. A wallet sitting in a lab log is a claim, not proof. To check the pool was live and the address actively mined, I replayed that login against the real pool, but only over a VPN, from an exit I'd checked wasn't my home IP, and only to read the response. The pool came back with status: OK and started streaming live RandomX jobs at a current Zephyr block height. Two facts fell out of that: the wallet is being actively mined, and 85.11.167.190:443 is pulling double duty as both a plain-HTTP C2 gate and the TLS mining pool.

Then I stopped, which needs its own paragraph.

The line I don't cross

There's a version of this where you "go further." Submit shares to skew their pool, flood the exfil sink, go looking for a way onto their C2. I don't, and the restraint is the part of this work I'm proudest of.

The rule is simple: read their infrastructure, never write to it. Replaying a request to see what comes back is reading. Submitting a share, uploading to their sink, trying to pop their box, that's writing. That's touching or attacking someone else's system, and it's the exact offensive line a guy with a home lab has zero authorization to step over. Every time I touch anything live it's through a VPN, and I verify the exit isn't mine before I touch it, not after. The sealed lab and the masked replay aren't paranoia. They're the difference between research and just doing crimes with extra steps.

If a honeypot writeup ends with "and then I hacked them back," it stopped being a honeypot writeup.

What this is actually worth

A reality check, because inflated honeypot posts are their own genre.

I did not find Diicot. Known crew, years of public reporting, familiar TTPs: SSH brute-force spread, killing rival miners, cron persistence, XMRig, Romanian strings in the telemetry.

What's useful is three things. First, fresh IOCs: current binaries, current C2/pool/exfil IPs, a current live wallet, all shared out to the community feeds (samples on MalwareBazaar, C2 on ThreatFox, deployer IPs on AbuseIPDB). Second, a method you can reuse: capture, triage, unpack, sealed detonation, sinkhole-and-replay, feeds, with every gotcha that tried to derail me written down. Third, one genuinely funny lesson, which is that the quality of your lie is a hard ceiling on your intel. A single unimplemented flag in a fake nvidia-smi was the only thing between me and the wallet. Deception fails silent. The malware doesn't throw an error, it just declines to show you the good part, and you never find out what you missed.

What I can't tell you is how much this wallet has earned. Zephyr is a privacy coin, Monero-family, so balances and payment history aren't sitting on-chain the way they would be for a Bitcoin address. "Actively mined" I can prove. A dollar figure I can't, and I'd side-eye anyone who claims a precise one for a privacy-coin cryptojacking wallet.

If you run a honeypot, take these

  • Attackers gate on realism you never thought to fake. Sudo behavior. GPU detection. Core count. If a whole family keeps leaving without dropping, whatever they probed right before disconnecting is the thing you're faking badly. Log the last command before they hang up, not just the payload you were hoping for.
  • The sample will attack your sandbox. It's more than VM-detection. It's pkill sprees that take out your own tooling and self-wiping dirs that erase your evidence. Assume your instrumentation is a target and keep it off the malware's kill list.
  • The good secret is usually runtime-only. Garble-encrypted config and an embedded-in-the-miner wallet are the same lesson twice: static analysis lies to you by leaving things out. Budget for dynamic analysis, and budget for making the malware believe before it'll talk.
  • Set the ethics line before you're tempted. Sinkhole and observe. Read, don't write. Mask every live touch. Decide it now, while it's abstract, not at 2 a.m. when the pool is answering and it'd be so easy to submit one share.

IOCs — Diicot/Mexals, 2026-07-15

Samples are on MalwareBazaar (tags Diicot, Mexals); the C2/pool/exfil IPs are on ThreatFox.

Indicator Value
Loader IQITtfbr (UPX/Go/garble) 07b9702bc859227a658903d1b4cf85d9daed6e0c24e670332c83020cc0a37166
Spreader cache 640c817722a4cd22251fcff100fdba167b7dfff885f36b5f64f2d59c52514180
Dropped loader diicot ffe04bc05a56f78b1273876cf17ded8df1aa3da5a15deb17dce99a3e206eb705
SSH brute-forcer kuak 8f225e59f3b892c7ed440db4c913491f825ccd64792cb759b55563c1b1310ece
Persisted miner-loader (/var/tmp) 8c0f9343f5eeb0393a7d633cbb5fb819d531e35225496815cff8462e90db864b
Main C2 + Zephyr pool (dual role) 85.11.167.190:443
Secondary C2 (2nd Go instance) 5.178.96.15:222
Exfil (Discord-webhook style) 31.57.105.94:1337
Error telemetry (base64 GET /save-data?err=) 31.57.105.94:1418
Public-IP reflector 31.57.105.94:42
Wallet (Zephyr / ZEPH) ZEPHYR35u3wKAs4jLTGfGD4yDsJc92M3yRr3gEHCmRutfnDwfwgZizqF8q7en82SRR6BiNDfukphgayqw5CU5Pue36PQCKHx1qd1v
Miner XMRig/6.26.0, RandomX (rx/0)
Attribution SSH key comment ElPatrono1337

Done on infrastructure I own and run as a research honeypot. Every bit of live-infrastructure contact was read-only and VPN-masked: no mining shares submitted, nothing written to attacker endpoints. If you go do this, contain it properly and stay on the right side of the read/write line.

GOT A TARGET IN MIND?

tell us what keeps you up at night. we'll scope it together.

▸ GET IN TOUCH