Mirai Botnet Series - Part 3 of 6
| Part | Title | Status |
|---|---|---|
| 1 | From Zero to Botnet: What Is a Botnet and Why Should You Care? | |
| 2 | How Mirai Actually Infects a Device | |
| 3 — You are here | I Opened Mirai’s Source Code — Here Is What I Found | |
| 4 | I Read Mirai’s Scanner Code — Here Is How It Infects Millions | |
| 5 | I Read the Brain of Mirai — How the C2 Server Actually Works | |
| 6 | Mirai’s Weapons — How 600,000 Cameras Brought Down the Internet |
← Previous: Part 2: How Mirai Actually Infects a Device | Next → Part 4: I Read Mirai’s Scanner Code
(A beginner reads malware for the first time)
Blog 3 of 7 in the Mirai Botnet Series
When I first opened Mirai’s source code, I did not know where to start.
There are 20+ files. Thousands of lines of C code. Function names that mean nothing at first glance.
So I did what any beginner should do.
I started with main.c.
Every C program starts execution from main(). Whatever the program does first that is what matters most. And what Mirai does first tells you everything about how the attacker thinks.
Let me walk you through it line by line. The way I wish someone had walked me through it.
First — Get The Source Code Yourself
Do not just read about it. Open it.
git clone https://github.com/jgamblin/Mirai-Source-Code
cd Mirai-Source-Code/mirai/bot/
ls
You will see this:
Every file has a job:
main.c → entry point, starts everything, connects to C2
scanner.c → finds vulnerable devices, cracks default credentials
killer.c → removes competing malware, owns the device's RAM
attack.c → executes DDoS flooding when C2 gives the order
table.c → stores encrypted strings and config
We are starting with main.c. Open it:
cat main.c
Part 1 — The Very First Thing Mirai Does
Look at the first few lines inside main():
int main(int argc, char **args)
{
// Delete self
unlink(args[0]);
Before connecting to C2.
Before scanning anything.
Before doing literally anything else.
Mirai deletes itself.
When I first read this I thought wait, how does it keep running if it deleted itself?
Here is the answer. And this is something you need to burn into your memory.
Running a program = loaded into RAM
File on disk = just storage
These are TWO completely separate things.
Think of it like this. You open a PDF on your laptop. The PDF is loaded into RAM and displaying on your screen. Now you delete the PDF file from your Downloads folder. Does the PDF disappear from your screen?
No. It is still open. Still running. Still in RAM.
Same thing here.
Step 1: Mirai binary uploaded to /tmp/mirai.arm (on disk)
Step 2: Execution starts → loaded into RAM
Step 3: unlink() deletes /tmp/mirai.arm from disk
Step 4: Everything continues running from RAM perfectly
So why delete first? Why not wait until later?
Because the moment Mirai starts running is the most dangerous moment for the attacker. If an investigator checks the filesystem RIGHT NOW they find the file. Evidence. Trace back to the attacker.
Delete it immediately. Before anything can go wrong.
Investigator arrives, checks /tmp/ → nothing there
Antivirus scans the disk → nothing there
Device owner looks for suspicious files → nothing there
But Mirai is still running perfectly in RAM.
Invisible. Clean. No evidence.
One line of code. Evidence gone forever.
Part 2 — Disable The Watchdog
Right after self-deletion:
// Prevent watchdog from rebooting device
if ((wfd = open("/dev/watchdog", 2)) != -1 ||
(wfd = open("/dev/misc/watchdog", 2)) != -1)
{
int one = 1;
ioctl(wfd, 0x80045704, &one);
close(wfd);
}
What is a watchdog?
Cheap IoT devices cameras, routers, DVRs have a hardware timer called a watchdog. Its job is simple: if the software stops responding or crashes, reboot the device automatically. This keeps the device alive even when software breaks.
Under normal conditions, this is useful. The device crashes, watchdog reboots it, device comes back online.
But for Mirai, this is a problem.
Mirai is about to do very heavy work. Scanning millions of IP addresses. Tracking hundreds of simultaneous connections. Cracking credentials. All of this on a device with 32MB or 64MB of RAM.
Watchdog NOT disabled:
Mirai starts heavy scanning
Device gets stressed
Watchdog timer expires
Device REBOOTS
Mirai gone from RAM
All scanning work lost
C2 connection dropped
Everything gone
vs:
Watchdog disabled:
Mirai scans freely
Heavy work, no problem
Device never reboots
Scanning runs forever
C2 stays connected
Attack uninterrupted
So the order is deliberate:
1. Delete evidence → no trace on disk
2. Disable watchdog → no interruptions
3. Everything else
Clean up. Secure the environment. Then work.
Part 3 — Hide In Plain Sight
Next comes something I found fascinating:
// Hide argv0
name_buf_len = ((rand_next() % 4) + 3) * 4;
rand_alphastr(name_buf, name_buf_len);
util_strcpy(args[0], name_buf);
// Hide process name
name_buf_len = ((rand_next() % 6) + 3) * 4;
rand_alphastr(name_buf, name_buf_len);
prctl(PR_SET_NAME, name_buf);
rand_alphastr() generates a random string. Something like xkqpfmra or tnjvwqkp.
Mirai replaces its own process name with this random garbage.
Why? Because on any Linux device, you can see all running processes:
ps aux
Without hiding:
PID NAME
1234 mirai.arm ← suspicious, investigator notices this immediately
With hiding:
PID NAME
1234 xkqpfmra ← looks like a normal system process
Different random string every time. No pattern. No way to search for it by name.
The malware is running. Right there in the process list. But nobody knows what they are looking at.
Part 4 — Only One Mirai Runs At A Time
ensure_single_instance();
This function makes sure only ONE Mirai process runs on the device at any time.
The reason is simple. These IoT devices are weak. A cheap camera might have 32MB of RAM total. Mirai’s scanner tracks hundreds of simultaneous connections. That alone eats significant memory.
If two Mirai processes run at the same time:
Process 1: scanning → using RAM
Process 2: scanning → using RAM
Camera's own services → also using RAM
Total RAM: 32MB
Result: RAM exhausted → device crashes → reboots → Mirai gone
So how does it enforce this? Clever trick:
// Try to bind to a specific port
bind(fd_ctrl, ..., SINGLE_INSTANCE_PORT)
The first Mirai claims a specific port on the device. Like planting a flag.
First Mirai: claims port → success → "I am the only one"
Second Mirai: tries same port → FAILS
→ "someone already here"
→ kills the old instance
→ claims the port itself
→ becomes the new single instance
New version always wins. Old version always dies. Device stays stable.
Part 5 — Loading The Three Modules
After securing the environment, Mirai loads its three main modules:
attack_init();
killer_init();
scanner_init();
Three lines. But the ORDER matters.
attack_init() — Prepare The Weapons
Think of a soldier preparing weapons before a mission. He does not know when the order will come. But when it does, he needs to be ready immediately.
attack_init() loads all DDoS attack capabilities into RAM. Sets up the structures needed for UDP flooding, TCP flooding, HTTP flooding. Everything ready and waiting.
When C2 later sends “attack 8.8.8.8” the attack module fires immediately. No preparation delay.
killer_init() — Own The Device
This runs BEFORE the scanner. Deliberately.
Mirai is not the only malware that targets IoT devices. When it lands on a device, there might already be other malware running. Other botnets. Other miners. All consuming the same limited RAM.
killer_init() removes all of them. Competing malware gone. RAM freed up. Device resources belong entirely to Mirai now.
Before killer_init():
RAM: [Mirai] [other malware] [other botnet] [camera services]
Total: nearly full
After killer_init():
RAM: [Mirai] [camera services]
Total: plenty of space for scanning
scanner_init() — Start Recruiting
Only after the environment is secure, the weapons are loaded, and competing malware is removed THEN the scanner starts.
It begins scanning random IP addresses across the internet. Looking for the next vulnerable device. The next default credential. The next victim to add to the botnet.
Part 6 — The Main Loop
After initialization, Mirai enters an infinite loop:
while (TRUE)
{
// Stay connected to C2
// Receive attack commands
// Execute them
}
This loop never exits. The bot just sits there, connected to the C2 server, waiting.
Every 10 seconds it sends a small ping:
if (pings++ % 6 == 0)
send(fd_serv, &len, sizeof(len), MSG_NOSIGNAL);
Like a heartbeat. “I’m still alive. Still connected. Still ready.”
When the C2 sends an attack command, the loop receives it and passes it to attack_parse(). The attack begins.
The Complete main.c Startup Sequence
Let’s put it all together:
Mirai binary uploaded and executed
│
▼
1. unlink()
Delete self from disk
Evidence gone before anything else
│
▼
2. Disable watchdog
Open /dev/watchdog
Send disable signal
No reboots allowed
│
▼
3. Hide process name
Replace name with random string
Invisible in process list
│
▼
4. ensure_single_instance()
Claim control port
Kill any previous instance
Only one Mirai runs
│
▼
5. attack_init()
Load DDoS weapons into RAM
Ready for C2 orders
│
▼
6. killer_init()
Remove competing malware
Own the device's RAM
│
▼
7. scanner_init()
Start scanning for new victims
Botnet grows itself
│
▼
8. while(TRUE) loop
Stay connected to C2
Wait for attack commands
Execute them
Forever
Eight steps. Each one deliberate. Each one protecting the infection.
What I Learned From main.c
Reading this code taught me something bigger than just Mirai.
The attacker thought like a forensic investigator. He asked himself “if someone finds this device, what will they look for?”
They will check the filesystem → delete the file first
They will check running processes → randomize the process name
They will look for suspicious ports → use a common looking port
They will check for known malware → randomize behavior
Every defensive technique that investigators use the attacker anticipated it and coded around it.
This is black box thinking at its finest. Not reading a manual. Just poking at how things work, understanding the system deeply, and exploiting that understanding.
That mindset is what makes Mirai’s code so elegant. Not the complexity. The simplicity. Every line does exactly what it needs to do and nothing more.
What We Learned
unlink() = delete file from disk while process stays in RAM
watchdog = hardware timer that reboots device on crash
/dev/watchdog = the file Mirai opens to disable the timer
ensure_single_instance = claim a port as a flag, kill old instances
attack_init() = load DDoS capabilities before they are needed
killer_init() = remove competing malware, own the RAM
scanner_init() = begin scanning for new victims
while(TRUE) = infinite loop, stays connected to C2 forever
In Blog 4, we go deeper into the scanner. How Mirai generates random IP addresses. Which IP ranges it deliberately avoids. And the exact 63 default credentials hardcoded into its source.
The infection engine is next.
Resources
Previous: Blog 1 - From Zero to Botnet: What Is a Botnet and Why Should You Care?
Previous: Blog 2 - How Mirai Infection Works
What’s Coming in Blog 4
We only looked at main.c today.
But the real heart of Mirai is scanner.c.
This is where Mirai generates random IPs, skips
certain address ranges deliberately, runs the
credential brute force, and manages hundreds of
simultaneous connections.
That deserves its own deep dive.
Blog 4: Inside scanner.c — The Infection Engine
See you there.



