ES EN
Index

Mirai · Chapter 4

Mirai laid bare: 16 methods and a one-byte XOR

The bot from the last chapter kept its config encrypted and its orders under lock and key. Ghidra spills the lot: the decryption (a laughable XOR), the botnet's real name, its C2 protocol and its full arsenal — without handing out weapons.

In Chapter 3 I caught a multi-architecture Mirai and walked off with all twelve binaries. Their strings already showed the C2 in the clear, but the rest of the configuration and every one of its orders were encrypted. Time to crack it open with Ghidra.

We pull out who the bot calls and what it's capable of so we can detect it and block it. The mechanics of the attacks — how each flood is fired — are left out: we catalogue the weapon, we don't hand it over.

01The encryption: a single byte

The XorDDoS from Chapter 2 used a 16-byte XOR key and we had to decompile the routine to find it. This one is in another league — a lower one. Just looking at the encrypted strings gives the trick away: they all end in T (byte 0x54).

the clue
# encrypted string exactly as it comes out of the binary:
?5$$50;7=5z:1 T

# XOR each byte with 0x54:
?5$$50;7=5z:1 T  ⊕ 0x54  =  kappadocia.net

The config carries null terminators (\0) at the end of each string, and 0 XOR key = key. That repeated T was the \0 giving the key away: 0x54. A single-byte XOR. I didn't even need to fire up the decompiler — it decrypts straight from strings.

A note, because it may jar: in the previous chapter that same domain was already showing up in plain sight in strings. What we didn't know then is that it also lives in here, inside the encrypted table — and it's this copy, the one the critter actually reads at startup, that we've just cracked open.

02The config laid bare

I apply XOR 0x54 to the whole string table and it comes out clean. And with it, the critter's identity:

decrypted config (XOR 0x54)
# identity
milnetv4            # botnet name/version
.anime             # tell-tale marker of the Mirai lineage

# C2
kappadocia.net · 141.98.10.50

# behavior
/dev/watchdog · /dev/misc/watchdog     # disables the watchdog
/proc/ /exe /maps /proc/net/tcp /proc/net/route
/etc/resolv.conf · nameserver          # resolves the C2
bash

# attack
Source Engine Query    # the template for method 1 (VSE)
assword                # no P: catches "Password:" and "password:" alike

The signature is unmistakably Mirai: the .anime string (marker of a known lineage) and the attack on /dev/watchdog — which disables the device's automatic reboot so it can't clean itself. The sweep of /proc lets it kill rival botnets and work out its own IP. The variant calls itself milnetv4.

A note I didn't make at the time, and that I'm correcting here: the string table has two halves, and not everything the critter carries inside is encrypted. sshd, dropbear and /bin/sh live in the plaintext part; what's above goes through the XOR routine. bash is the only one that shows up in both. It isn't an oversight by the author: they're two different mechanisms — what exec() and the process-killing module consume directly doesn't need decrypting. And it reinforces the note from the previous section, because kappadocia.net is precisely one of the things that sits in both.

03The C2 protocol

With the config in the clear, I decompiled the main loop in Ghidra. The bot connects to the C2, registers itself (architecture + milnetv4 + .anime) and sends a periodic heartbeat. Orders arrive with a length prefix:

C2 order format
[ 2 bytes big-endian = length ][ payload ]
        └ discarded if the length > 0x400

# the first 4 bytes decide the type:
0xFE  →  kill ALL attacks
0xFD  →  kill ONE attack by its id
other  →  ATTACK order

The attack order carries the duration, the vector id (less than 16), the target list (each one as IP + network prefix) and a list of options (port, size…). For each attack the bot does a fork() and stores the child process in a table — that's how it keeps up to 15 attacks running at once and can kill them individually with the 0xFD order. It's Mirai's architecture, piece by piece.

And there's the reason the two control orders come in different sizes. The order is a four-byte word, so 0xFE — "kill them all" — takes four and needs nothing else. 0xFD takes eight because it carries the identifier of the attack to be stopped stuck on behind it. The asymmetry isn't a quirk of the format: it's that one of the two orders has to say which one.

04The arsenal: 16 methods

The bot registers its attack methods in a {function, id} table. I recovered all 16 entries and classified each by the type of socket it opens and the headers it crafts. I present it as a defensive inventory — what it can do, so it can be recognized — without going into how any of them is executed:

IDAttack type
0UDP flood (raw)
1UDP flood, small packet (games/VSE)
2DNS flood
3TCP flood (raw)
4TCP flood (ACK/PSH)
5TCP flood (STOMP)
6GRE flood (GRE-IP)
7GRE flood (GRE-ETH)
8UDP-PLAIN flood
9STD flood
10TCP flood (configurable flags)
11UDP flood, multi-socket
12TCP with data
13TCP connection flood
14Reverse shell to the C2 — not a flood
15HTTP flood (layer 7)

A textbook catalogue: UDP (raw and plain), DNS, GRE, several flavours of TCP and a layer-7 HTTP flood. The hard evidence comes from the code itself — the type of socket and the protocol it asks for leave no room for doubt. Twelve architectures, one and the same arsenal: the table is identical across every binary, proof that they come from a single source. And then there's number 14, which doesn't fit that catalogue at all.

The first fifteen all do the same thing in different wrapping: throw packets at somebody. Number 14 opens a reverse shell: it starts a fresh session, wires standard input, output and errors to a socket, and launches a command interpreter. That floods nobody — that's someone sitting down at the machine.

And the best part is where it calls. Not some mystery address: the same place as always. It dials the very structure the bot fills in at start-up when it resolves its controller — the one that comes out of the XOR 0x54 from the previous section. Same IP, same port, same door. When the operator feels like it, the bot that's been chatting to its C2 all along opens it a /bin/sh down the same channel.

A classic Mirai is a cannon: you tell it who to shoot at, and it shoots. This one also brings a chair.

And if you've been following the numbers, there's something else to look at: the order in which they're registered. It goes: 0, 1, 2, 8, 3, 4, 5, 10, 6, 7, 9, 11, 12, 13, 14, 15.

One of them sits in a place that doesn't match its number: udpplain had its label changed without being moved from its slot in the queue. The tcpxmas in eighth place is something else — not a renumbered veteran, but a newcomer that took a number somebody else left free. That's what's visible, and it's what holds up the conclusion: this isn't a trimmed-down Mirai, it's one with a remapped and stretched attack table. Stretched at the top, too — which is where 11, 12, 13, 14 and 15 live.

The fingerprint, then, isn't in what's missing — nothing is. It's in the order, and in the fact that right at the top, where the original didn't reach, somebody added a chair.

I opened the leaked 2016 code. There the table is filled with ten methods, in this order: 0, 1, 2, 9, 3, 4, 5, 6, 7, 10. Strip the six additions from ours and that exact sequence is what's left. Two labels change: udpplain drops from 9 to 8 — a number nobody used in the original — and HTTP climbs from 10 to 15, though it still goes in last. The two numbers left loose, 9 and 10, go to new methods.

And the signature is a quirk. In the original, udpplain is registered fourth, out of order, on the pure whim of whoever wrote it in 2016. This thing, ten years later, still registers it fourth. That doesn't get reinvented by chance: it comes from there.

And there's one last thing in the inventory that isn't for attacking anyone either. The bot carries a module that, on start-up, closes thirty-two ports on the machine it has just walked into. Telnet, HTTP and FTP among them; a collection of backdoor and IRC ports used by rival botnets; and three very specific ones — 53413 on Netcore boxes, 37215 on Huawei HG532s and 52869 on Realteks. All three are well-known front doors into network gear.

The effect is that the device ends up harder to infect. By anyone else.

And the first on that list is the one that says most: 48101, the port Mirai uses as its single-instance lock — the one a Mirai opens so another copy of itself can't pile in on top. This one kills whatever is listening there. Which is to say: it goes after other Mirai.

Where I stopI know these vectors exist and how to recognize them in traffic; that's where I stay. Cataloguing the arsenal is defense; explaining how each flood is fired would be handing out weapons — and that's the line this log won't cross. Same goes for number 14: I'll tell you the reverse shell is there and what it does. The handshake that opens it, no.

05The thread that links back to the past

Ghidra showing Mirai's C2 resolution function, with the domain kappadocia.net and the fallback IP 141.98.10.50
The bug's plan B, in Ghidra. This function tries to resolve kappadocia.net over DNS; and if it comes back empty —domain down, DNS blocked, whatever— it goes straight to 141.98.10.50, which it carries written inside. Which means: taking its domain down doesn't disconnect it, because it knows the IP by heart. When DNS does answer and returns several addresses, it picks one at random: the decompiler ends that assignment with a % —the remainder of a division— which spreads the choice across the addresses returned. In the screenshot it sits right at the edge of the panel, half cut off. None of this shows up in strings; you have to read the code.

Passive OSINT on the C2, without touching the machine. kappadocia.net resolves to 141.98.10.50. And that's where the surprise lands:

The same neighbourhood as Chapter 2The C2 of the XorDDoS from Chapter 2 was 141.98.11.51. This Mirai calls 141.98.10.50. When I wrote this I said they shared the same 141.98.0.0/16 network, which is a very wide neighbourhood. It can be tightened a lot further.

That XorDDoS domain didn't always point at 11.51: in June it resolved to 141.98.10.115. Which means that for a stretch, both families had their command server in the same /24, 141.98.10.0/24. And all three addresses — the Mirai one and the two XorDDoS ones — sit in the same place: AS209605. Two different families, two separate captures, and not a corner of the internet: a corridor. It's exactly the kind of thread that only turns up when you save and compare everything you catch.

06Indicators (IOCs)

TypeValue
FamilyMirai · "milnetv4" variant · ".anime" marker
C2kappadocia.net · 141.98.10.50 (AS209605, same as the Ch. 2 C2)
Config encryptionXOR · 1-byte key 0x54
C2 protocollength-prefixed orders 2B BE · control 0xFE (4 B) / 0xFD (8 B, carrying the attack id)
Capability16 methods (15 DDoS + 1 reverse shell to its own C2) · up to 15 concurrent attacks
Ports it closes32, among them 48101 (Mirai's single-instance lock) · 23 · 80 · 21/20 · 53413 · 37215 · 52869
Status as of 11 September 2026kappadocia.net no longer resolves, and the delivery server from Chapter 3 has vanished. But 141.98.10.50 is still alive. It is, live, exactly what this chapter predicted three weeks earlier: taking its domain down doesn't disconnect it, because it carries the IP written inside.

And that closes out the Mirai family — from the hot chase to the arsenal on the table, all of it to understand it and be able to stop it. With a rhyme I wasn't expecting: back in Chapter 1, the XorDDoS renamed wget and curl so that no other botnet could download anything on that machine. This one closes thirty-two ports, Mirai's own lock included. Two families that look nothing alike doing the same thing: marking territory. And the victim's device left, incidentally, a little safer — for everyone except whoever is already inside.

To be continued — the bait's still on. When the next critter brings something new, there'll be a fifth chapter. 🍯

Comments