ES EN
Index

Mirai · Chapter 17

Fresh out of the oven

A quiet Sunday, my new-binary alarm went off after days of silence. The first thought was the best one there is: something fresh. What followed was a rollercoaster — an old acquaintance, a false positive that nearly fooled me, and finally a critter that wasn't in any public repository, caught less than thirty hours after it was born.

Sunday looked quiet when my new-binary alarm went off. And it surprised me, because it had been silent for days. So the first thought was the best one anybody with a trap set can have —something fresh— and I got to work.

A warning before we start, because this chapter isn't what it looks like. I'm not here to explain how Mirai works: I did that in chapter 3 and chapter 4, and repeating it is boring. This is something else, shorter and faster: how you tell that something is genuinely new. Complete with the two trips along the way, which are the part you learn from.

01The hunch broke twice

The alarm didn't bring one thing: it brought three. And all three called for the same boring question.

1

An old acquaintance — but look who's carrying it

I recognised the first two straight away: Trinity, the ADB miner from chapters 10 and 13. Same hashes, same cycle, same wallet. Nothing new… until I looked at where they were coming from.

Four visits in eight days, and four different IPs: three from China and one from South Korea, all of them on home or mobile networks, none on hosting, none repeating. That isn't an operator re-uploading his critter from a server: those are infected devices pushing it to the next one. Trinity is still alive and spreading six years on, and there's no delivery server here to take down — it moves from victim to victim over the debug cable itself.

Hold on to that. At the end of the chapter it runs headlong into the other one.

Inaccurate · being checked What follows, struck through, doesn't hold up as written, and I don't yet know how far it goes. I'm leaving it visible while I check: deleting it would be worse than leaving it badly flagged. Once I have the verdict this becomes an edit note like any other.
2

The panel that wasn't a panel

The third piece was different and it looked promising: a modern-looking login page, in Russian, "OpenWrt Remote Hub", with a username, a password and a captcha. A credential-stealing panel! I could already see myself pulling that thread. On top of that a different IP had brought it —from Finland, not from where Trinity came— so I filed it as a separate campaign.

Until I asked the boring question: what exactly is this?

And it turned out it wasn't even malware. It was the login screen of a real open-source project, without a single modification, identical to the one in its repository. So why had my trap saved it as if it were a payload? Because the attacker ran wget against that server, the server answered HTTP 401 with its login page as the body of the error, and wget, without -f, happily saves the body of an error. The chain delivered nothing. The six "samples" were six error messages.

The scare, and why I don't name the projectI was one slip away from cataloguing an honest developer's front page as malware. That's why I don't name it here: it has no need to appear in a chapter about botnets, not even to be cleared. And you don't need its name to recognise it — the header its server returns is enough. A file your trap saves isn't necessarily what the attacker meant to deliver — before analysing, and above all before cataloguing, look at which HTTP response produced that capture.
3

And what was left wasn't what I thought either

With the noise ruled out, what remained did smell different: a server in Vietnam handing out multi-architecture binaries. My first reflex was to file it with Trinity —they landed the same day, through the same port— and I was wrong again.

Because that dropper had not a single Trinity artefact: no APK, no pm install, no trinity binary, no wallet, no pool. What it had was the canonical pattern of a Mirai-school IoT loader. And the four IPs that were actually carrying Trinity never touched that box.

The same vaccine, twice in one dayThe only thing linking the three was port 5555. And that isn't a relationship: it's the base rate. In eight days, 99 different IPs hammered that port on my trap. If "they both go to 5555" isn't enough to join Finland to Vietnam, it isn't enough to join Vietnam to Trinity either. The same boring question —where does this come down from?— saved me twice in the same afternoon.

02I went and got it

And here I have to come clean about something. The trap didn't capture those binaries. It saved the three download scripts —the same one in three flavours: wget, busybox wget and curl— but not the twelve files those scripts were going to fetch: adbhoney records the URLs it sees in the command and doesn't chase the curl that lives inside the downloaded script.

So I had the addresses and I didn't have the critter. It smelled fresh and I had to check, so I went and got the twelve myself. Nothing more: I didn't list the directory, didn't try credentials, didn't force any paths. Download them, set them read-only, and look. None of them was ever run.

03Mirai with a surname: Condi

Twelve static ELF files, twelve architectures, all uploaded in the same second. And the operator made one delicious slip: of the twelve, one didn't get its symbols removed.

You don't have to take my word for that detail. It's in the sizes, and the sizes are published:

the ARM family, by size
condi.arm    125,504 B
condi.arm5   125,504 B
condi.arm6   139,004 B
condi.arm7   173,966 B   # 35 KB extra, for the same program

Those 35 KB are the symbol table. And inside it, in the clear, are the names from Mirai's leaked source: table_key, attack_tcp_syn, killer_init, resolve_cnc_addr. Even the compiler path gives the lineage away: the Aboriginal Linux cross-compilers, which are exactly the ones shipped with the Mirai build that leaked. One mistake in 1 of 12 and the binary's anonymity is over.

But it isn't plain Mirai. It has a surname, and it says so itself: in the clear, in all twelve, are /var/Condi and condi2. It's Condi, a Mirai fork whose source was published in 2023 — documented back then by FortiGuard and Akamai. This particular campaign's marker is top1hbt, the exact equivalent of chapter 4's milnetv4.

What they took out

The Condi FortiGuard documents hunts for its own victims: it carries a scanner and exploits a TP-Link router flaw. This one has no scanner — not a single symbol from that part is left. They amputated it and feed it from outside, through the open ADB on 5555. It exploits no vulnerability: it walks in through a door that shouldn't be open.

And that isn't the only amputation. Mirai carries a killer: a module that hunts down rival processes and closes the device's telnet and SSH so nobody else can get in behind it. Here the names are still therekiller_init, killer_kill— so at a glance it looks present. It isn't: they're 76 and 48 bytes. One does a fork() and leaves the child spinning in a sleep(5) loop; the other sends a signal and that's it. Mirai's logic is nowhere to be found. A function name is not a function, and an unstripped binary hands you the analysis — but it also invites you to trust the label.

Scanner gone, killer gone. What's left is a Condi slimmed down to three things: talk to its command server, attack, and replicate over HTTP. It neither hunts for victims nor fights for the machine — it's handed one, and it doesn't mind sharing.

And what they put in

The public Condi deletes eight shutdown binaries, all eight under /usr/. This one carries sixteen: the same four commands, but across four paths.

the 16 paths, in .rodata
/sbin/reboot     /usr/sbin/reboot     /bin/reboot     /usr/bin/reboot
/sbin/shutdown   /usr/sbin/shutdown   /bin/shutdown   /usr/bin/shutdown
/sbin/poweroff   /usr/sbin/poweroff   /bin/poweroff   /usr/bin/poweroff
/sbin/halt       /usr/sbin/halt       /bin/halt       /usr/bin/halt

It isn't that it's "more thorough": it's that it's adapted to the terrain. On the Android boxes and embedded devices it reaches over ADB, /usr often doesn't even exist. Scanner out, new paths in — both point the same way.

And it isn't enough for the strings to be there: you have to see what it does with them. They live in main, not in the evicted killer, and the mechanism is the same on ARM and on x86-64 — it copies the sixteen onto the stack and unrolls sixteen calls in a row, one per path. The instruction is unlink.

What that means for whoever owns the deviceIt doesn't intercept the shutdown: it deletes the files. All sixteen, off the disk. The joke cuts both ways, and it's a nasty one: if the device loses power, Condi goes with it —it doesn't survive a reboot, like all Mirai— but its owner no longer has anything to shut it down cleanly with, and that doesn't fix itself. It needs a reinstall. It doesn't just use your machine: it breaks your off switch.

The dumb trick that hides the C2

The config is XOR-encrypted, like all Mirai. The binary's key is four bytes, 0x6d53d2c2, but Mirai XORs each byte with all four, so the effective key is a single one: 0x6d^0x53^0xd2^0xc2 = 0x2e.

And 0x2e happens to be the code for a full stop. So when the C2 domain is encrypted its dots turn into zero bytes, and the name ends up broken into pieces that strings reads as junk two, eight and four letters long. Nobody chose that key: it fell out of the four bytes on its own. There's no cunning here — there's luck.

Decrypted, the command server is cc.nhancute[.]site, port 47925 (that one isn't in the table: it's baked into the code). And the pretty detail: that domain resolves to the same box in Vietnam that hands out the binaries. The delivery server is the C2.

04Four botches and a third of the network crippled

The binary is designed not to depend on that box: every infected bot raises its own HTTP server —on a random high port, lying with a Server: Apache that isn't Apache— downloads the binaries from the seed and serves them to the next one. The boast it carries inside starts, literally, with "Self Rep".

Flawless design. Execution, less so:

  • The swapped labels. The file the dropper calls sh4 is actually SPARC, and the one it calls spc is Renesas SH. They're the wrong way round.
  • An architecture that isn't on the list. The internal name array has eleven entries, not twelve: spc is missing.
  • And the loop stops early. Of those eleven, the replicating code only walks eight — and not through a slip in one place: the download is written twice, unrolled where the server starts up and in a loop inside main. Both stop at eight. The limit is set in duplicate.

Add it up: four of the twelve architectures never replicate from a bot. PowerPC, 68k, x86 and SPARC can only be served from Vietnam. A critter built so that taking down the seed wouldn't hurt… that depends on the seed for a third of its targets.

05How do you date something that isn't anywhere?

And now the part that earns the title. This critter isn't uncatalogued because it's stealthy: it's uncatalogued because it had just been born. But proving that has a trick to it, and the trick is knowing which clock measures what.

The first thing I did was check the feeds. And they came back silent: GreyNoise had nothing on the delivery box, passive DNS was empty, urlscan zero, Shodan no information. Four silences. The temptation is to call bingo.

Two of those silences are worth nothingGreyNoise measures scanning, and that box doesn't scan: it serves files. It would be just as silent whether it was a day old or a year old. (Its neighbour, the one that did attack my trap, does show up flagged as malicious.) And passive DNS is empty because it serves over a raw IP, with no domain — there's nothing to record. That silence is function, not freshness. I'd have loved to bank four clocks; I have two.

The good clock is a different one, and it's the one the attacker doesn't control: Certificate Transparency. When his domain got Cloudflare's automatic certificate, a dated entry appeared in a public log he can neither touch nor delete. Cross it with the domain's RDAP record and the story is very short indeed:

three clocks he doesn't own, the same morning
29 Aug 07:24:11 UTC  domain registered          # RDAP · GMO/Onamae
29 Aug 07:54:11 UTC  appears in the CT log      # crt.sh
29 Aug 09:50:30 UTC  the 12 binaries go up      # Last-Modified + ETag
30 Aug ~midday       it hits me                 # less than 30 h later
The certificate claims to be older than its own domainIf you look at that certificate's not_before field it says 06:52 — thirty-two minutes before the domain existed. It isn't an anomaly or a clue: certificate authorities backdate that field to absorb clock skew between servers. The honest "this appeared in public" time is the log entry's, 07:54. A good clock read wrong is a bad clock.
You'll have to forgive me one thingThis time I'm not giving the exact time of the attack. With something this newly born, whoever launched it has his own records of "who I hit at what second", and crossing those with a "it attacked me at such an hour" would hand him my trap on a plate. The times above are his infrastructure's, which are public; mine is rounded. The story loses nothing.

And the fact that clinches it

MalwareBazaar has been storing malware samples since 2020. When I searched the condi tag, there was one single sample, uploaded in May 2023 by somebody else: the one from the public source when it leaked. One, in three years and three months.

The twelve hashes from this campaign weren't there. Not there, and not anywhere else I looked. It wasn't hiding well: it was that nobody had had time to file it.

Now there are thirteen. Twelve of them I uploaded.

And I could date this because I kept the junkWhen I pulled the twelve binaries down I didn't just keep the files: I also kept the HTTP headers the server sent back, in a directory of their own. It looked like paperwork. Without them, that "under thirty hours" up there would be an impression of mine.

With them it's three clocks that don't talk to each other and say the same thing: the Last-Modified — identical to the second across all twelve files —, the date carried inside the ETag, and the size also carried inside the ETag, which matches the Content-Length and the file I have on disk. Three different ways of asking the same question, three times the same answer.

Measured age: 29.87 hours. It's a cheap habit — keeping what the server tells you in passing — and it turns a hunch into a number.

06Indicators (IOCs)

The twelve hashes are in the catalogue with their link to MalwareBazaar. The rest, for anyone who wants to recognise it or block it:

TypeValue
C2 and delivery servercc.nhancute[.]site : 47925 → 160.250.181.124 (VPSRE, Vietnam, AS150895)
Attacking IP (ADB)160.250.181.123 — the neighbour; sweeps first, loads after
Domain registration2026-08-29 07:24:11 UTC · GMO/Onamae · Cloudflare NS · no DNSSEC
Delivery paths/k7m2q9xa/ + 12 six-character names · droppers /b4k9zp.sh, /a7m2qx.sh, /c8r3nv.sh
Campaign markertop1hbt (top1hbt.armtop1hbt.x86_64, what each bot re-serves)
Self-identification/var/Condi · condi2 %s:%d · webserv
Config keytable_key = 0x6d53d2c2 → effective 1-byte XOR 0x2e
Bot↔C2 protocolheader 66 99 66 + length (2 B) + payload (ping, condi2 webserv:<port>)
Bot httpd fingerprintServer: Apache (false) · client User-Agent: Update v1.0 · random high port
Anti-rebootunlinks 16 paths: {/sbin, /usr/sbin, /bin, /usr/bin} × {reboot, shutdown, poweroff, halt}
Amputated modulesno scanner_* · killer_init (76 B) and killer_kill (48 B) gutted — the symbols are there, Mirai's logic isn't
Certificatenhancute.site + *.nhancute.site · Google Trust Services WE1 · CT 2026-08-29 07:54:11 UTC
VectorOpen ADB (tcp/5555). No CVE: it's misconfiguration, not a vulnerability
Status as of 11 September 2026 · it lasted thirteen dayscc.nhancute.site no longer resolves, neither it nor its root domain. It was registered on 29 August and was dead before 11 September: thirteen days of life.

And the division of roles I could only guess at above — one sweeps, the other loads — now has numbers on it:

· 160.250.181.123, the one that attacks: 491 reports, risk score 100 %.
· 160.250.181.124, the one holding the goods and acting as command server: 3 reports, 22 %.

The same tactic as KHserver, in another family and with the same lopsided numbers: the noisy one burns, the one that matters is protected.

And that "thirteen days" deserves a comparison, because it's the shortest this bait has produced. Condi died in thirteen days. RedTail was still coming down to my bait the day I write this, nearly three weeks after I devoted a chapter to it — the same five binaries, without recompiling. Two families, the same business underneath, and a difference in timescales that looks nothing alike.

And if you got here from a MalwareBazaar entry: this is what that entry can't give you — how it fell.

To be continued — the trap is still on. And this one, for once, didn't stay in my lab: it's where the people who build detections can use it. It made me happy, honestly. 🍯

Comments