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.
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.
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.
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.
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:
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 programThose 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 there —killer_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.
/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/haltIt 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.
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.
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:
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 laterAnd 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.
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:
| Type | Value |
|---|---|
| C2 and delivery server | cc.nhancute[.]site : 47925 → 160.250.181.124 (VPSRE, Vietnam, AS150895) |
| Attacking IP (ADB) | 160.250.181.123 — the neighbour; sweeps first, loads after |
| Domain registration | 2026-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 marker | top1hbt (top1hbt.arm … top1hbt.x86_64, what each bot re-serves) |
| Self-identification | /var/Condi · condi2 %s:%d · webserv |
| Config key | table_key = 0x6d53d2c2 → effective 1-byte XOR 0x2e |
| Bot↔C2 protocol | header 66 99 66 + length (2 B) + payload (ping, condi2 webserv:<port>) |
| Bot httpd fingerprint | Server: Apache (false) · client User-Agent: Update v1.0 · random high port |
| Anti-reboot | unlinks 16 paths: {/sbin, /usr/sbin, /bin, /usr/bin} × {reboot, shutdown, poweroff, halt} |
| Amputated modules | no scanner_* · killer_init (76 B) and killer_kill (48 B) gutted — the symbols are there, Mirai's logic isn't |
| Certificate | nhancute.site + *.nhancute.site · Google Trust Services WE1 · CT 2026-08-29 07:54:11 UTC |
| Vector | Open ADB (tcp/5555). No CVE: it's misconfiguration, not a vulnerability |
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