Anatomy of a Mirai Variant: Full Source Code Recovery of an IoT Botnet
TL;DR
We recovered the complete source code of a Mirai-variant IoT botnet -- a C-based bot client and a Go-based command-and-control server -- from an exposed build environment. The recovered source includes 10 DDoS attack vectors (TCP/UDP/GRE/HTTP), a telnet brute-forcer with 62 hardcoded credential pairs targeting IoT devices, a competing-malware killer module, and a multi-architecture cross-compilation pipeline targeting ARM, MIPS, PowerPC, SPARC, SH4, m68k, and x86. Below is a line-by-line technical analysis of the recovered codebase.
Bot Architecture
The bot client is written in C99 and follows the canonical Mirai architecture: a single-threaded select() event loop in the parent process, with forked child processes for the scanner, killer, and individual attacks.
Initialization Sequence (bot/main.c)
On execution, the bot performs the following in order:
- Self-deletion --
unlink(args[0])removes its own binary from disk immediately. - Watchdog disarm -- Opens
/dev/watchdogor/dev/misc/watchdogand issuesioctl(wfd, 0x80045704, &one)to disable hardware watchdog timers that would otherwise reboot the device on process hang. - Anti-debug trap -- Installs
SIGTRAPhandler viasignal(SIGTRAP, &anti_gdb_entry). Theanti_gdb_entryfunction swaps a function pointer (resolve_func) from a dummy address to the realresolve_cnc_addr. Under GDB, SIGTRAP is intercepted by the debugger, so the function pointer is never set and the bot never resolves its C2 -- a simple but effective anti-analysis gate. - Table obfuscation unlock --
unlock_tbl_if_nodebug()is wrapped in a layer of obfuscation: byte-swapping a hardcoded string (./dvrHelper), building an array of function pointers, and using an arithmetic fold over argv to index into the array and calltable_init(). This is the function that populates all obfuscated string constants. - Single-instance enforcement -- Binds a TCP socket to
127.0.0.1:48101. If the port is already bound, it connects to the existing instance (triggering that instance's self-kill viaaccept()onfd_ctrl), then callskiller_kill_by_port()to forcibly terminate the old process and recurses. - Process name randomization -- Overwrites
argv[0]and callsprctl(PR_SET_NAME, ...)with random alphanumeric strings to hide frompsoutput. - Daemonization --
fork(),setsid(), closes STDIN/STDOUT/STDERR. - Module initialization --
attack_init(),killer_init(), and conditionallyscanner_init()(gated behindMIRAI_TELNETdefine).
C2 Protocol
The bot communicates with the CNC over a raw TCP socket. The protocol is minimal:
- Registration: On connect, the bot sends
\x00\x00\x00\x01followed by a 1-byte length and an optional source identifier string. - Keepalive: Every 60 seconds (6 iterations of a 10-second
select()timeout), the bot sends a 2-byte zero-length frame. - Command reception: The CNC sends a 2-byte big-endian length prefix followed by the command payload. A zero-length frame is a ping. Non-zero payloads are passed directly to
attack_parse(). - Reconnection: On disconnect, the bot sleeps for a random 1-10 second interval and re-resolves the CNC domain via DNS before reconnecting.
The C2 address is resolved at runtime from an obfuscated domain stored in the string table. The initial srv_addr is set to a fake/placeholder IP (FAKE_CNC_ADDR = 45.137.70.27) and port 23, which is overwritten once resolve_cnc_addr() runs after the anti-debug gate fires.
String Table Obfuscation (bot/table.c)
All sensitive strings are XOR-obfuscated with a 4-byte key derived from 0xdeadbeef:
uint32_t table_key = 0xdeadbeef;
The toggle_obf() function XORs each byte of a table entry against all four key bytes sequentially:
val->val[i] ^= k1; // 0xef
val->val[i] ^= k2; // 0xbe
val->val[i] ^= k3; // 0xad
val->val[i] ^= k4; // 0xde
This is equivalent to XORing with 0xef ^ 0xbe ^ 0xad ^ 0xde = 0x22. Strings are unlocked (decrypted) before use and immediately re-locked (re-encrypted) after, so they never persist in cleartext in memory for longer than necessary. The table stores 52 entries including the CNC domain, scanner credentials, attack strings, HTTP headers, user agents, and paths used by the killer module.
Attack Modules
The bot registers 10 attack vectors at initialization:
| ID | Name | Function | Protocol Layer |
|---|---|---|---|
| 0 | udp | attack_udp_generic | UDP flood |
| 1 | vse | attack_udp_vse | Valve Source Engine query flood |
| 2 | dns | attack_udp_dns | DNS water torture |
| 3 | syn | attack_tcp_syn | TCP SYN flood |
| 4 | ack | attack_tcp_ack | TCP ACK flood |
| 5 | stomp | attack_tcp_stomp | TCP STOMP (established conn flood) |
| 6 | greip | attack_gre_ip | GRE-encapsulated IP flood |
| 7 | greeth | attack_gre_eth | GRE-encapsulated Ethernet flood |
| 9 | udpplain | attack_udp_plain | Optimized high-PPS UDP flood |
| 10 | http | attack_app_http | HTTP layer-7 flood |
Each attack is dispatched via double-fork: the first fork() detaches from the main process, and the second creates a timer child that sleep(duration) then kill(getppid(), 9) to enforce the attack duration limit. Up to 8 attacks can run concurrently (ATTACK_CONCURRENT_MAX).
TCP SYN Flood (bot/attack_tcp.c)
The SYN flood constructs raw IP+TCP packets via SOCK_RAW with IP_HDRINCL. It accepts fine-grained options: TOS, TTL, identification, don't-fragment bit, source/destination ports, sequence/ack numbers, and arbitrary TCP flag combinations (URG, ACK, PSH, RST, SYN, FIN). The source flag allows specifying a spoofed source IP, with 0xFFFFFFFF indicating random per-packet spoofing.
HTTP Flood (bot/attack_app.c)
The HTTP flood module maintains up to 256 concurrent connections (1000 in debug mode) and implements a multi-state connection machine with 12 states -- from initial connect through HTTPS negotiation, header send/receive, body processing, cookie tracking (up to 5 cookies), and anti-DDoS detection. It specifically checks for DOSarrest and Cloudflare-nginx server headers to identify protected targets. Five hardcoded Chrome/Safari user-agent strings from the 2016 era are rotated.
Command Wire Format
Attack commands are serialized by the CNC as:
[2 bytes: total length, big-endian]
[4 bytes: duration in seconds, big-endian]
[1 byte: attack type ID]
[1 byte: target count]
[4 bytes: target IP prefix] [1 byte: netmask] (repeated per target)
[1 byte: flag count]
[1 byte: flag key] [1 byte: value length] [N bytes: value] (repeated per flag)
Maximum duration is enforced at 3600 seconds (1 hour). Maximum 255 targets and 255 flags per command. Total payload capped at 4096 bytes.
Scanner Module (bot/scanner.c)
The scanner forks into its own process and runs a telnet brute-forcer against random IPv4 addresses. It uses raw TCP SYN scanning on port 23 with SOCK_RAW + IP_HDRINCL for connection initiation, maintaining up to 128 concurrent connections at 160 packets per second.
Credential Table
62 username/password pairs are hardcoded, each with a weight value that determines selection probability. The highest-weighted credentials (most commonly found on IoT devices) are tried first:
| Weight | Username | Password | Target Device Type |
|---|---|---|---|
| 10 | root | xc3511 | XiongMai DVRs/IP cameras |
| 9 | root | vizxv | Dahua DVRs |
| 8 | root | admin | Generic embedded Linux |
| 7 | admin | admin | Generic routers/cameras |
| 6 | root | 888888 | Dahua DVRs |
| 5 | root | xmhdipc | XiongMai IP cameras |
| 5 | root | default | Various IoT |
| 5 | root | juantech | JuanTech IP cameras |
| 5 | root | 123456 | Generic |
| 5 | root | 54321 | Generic |
| 5 | support | support | Huawei/ZTE routers |
| 4 | root | (empty) | Misconfigured devices |
| 4 | admin | password | Generic |
| 4 | root | root | Generic embedded |
| 4 | root | anko | Anko devices |
The remaining 47 entries at weights 1-3 cover additional defaults: Zte521, hi3518 (HiSilicon SoCs), 7ujMko0vizxv and 7ujMko0admin (Dahua backdoor credentials), dreambox (satellite receivers), realtek (Realtek-based routers), ubnt (Ubiquiti), smcadmin (SMC routers), and others.
Brute-Force State Machine
Each connection progresses through a telnet negotiation state machine:
SC_CLOSED -> SC_CONNECTING -> SC_HANDLE_IACS -> SC_WAITING_USERNAME ->
SC_WAITING_PASSWORD -> SC_WAITING_PASSWD_RESP -> SC_WAITING_ENABLE_RESP ->
SC_WAITING_SYSTEM_RESP -> SC_WAITING_SHELL_RESP -> SC_WAITING_SH_RESP ->
SC_WAITING_TOKEN_RESP
After authentication, the scanner sends shell, enable, system, and sh commands to attempt to drop into a raw shell -- accounting for different embedded Linux CLI environments (BusyBox, Cisco-style enable prompts, etc.). It then executes /bin/busybox MIRAI and checks for the string applet not found to verify it has a working BusyBox shell. The ncorrect string is checked to fast-detect invalid passwords without waiting for a full timeout.
Successful logins are reported to a separate scan reporting server (domain and port stored in the string table as report.changeme.com:48101).
Killer Module (bot/killer.c)
The killer runs as a forked child process and serves three functions:
1. Port Rebinding
Kills services on ports 23 (telnet), 22 (SSH), and 80 (HTTP) via compile-time flags (KILLER_REBIND_TELNET, KILLER_REBIND_SSH, KILLER_REBIND_HTTP), then binds to those ports itself. This prevents the legitimate services from restarting and blocks remote administration of the compromised device. The build script enables KILLER_REBIND_SSH for all release architectures, ensuring SSH lockout.
The kill mechanism is precise: it parses /proc/net/tcp to find the inode associated with a listening port, then walks /proc/*/fd/ to find which PID holds that inode, and issues kill(pid, 9).
2. Competing Malware Elimination
Continuously scans /proc/ for new processes (tracking the highest-seen PID to avoid rescanning). For each process:
- Checks if the binary's realpath contains
.anime(a known Bashlite/QBOT artifact) -- if so,unlink()the binary andkill -9the process. - Checks if the binary has been deleted from disk (
open(realpath, O_RDONLY)fails) -- if so, kills it. This targets malware that deletes itself after execution (ironic, given this bot does the same). - Performs memory scanning of
/proc/$pid/exeagainst signatures for known competing botnets:- QBOT/Bashlite variants (3 signature patterns)
- UPX-packed binaries (detects UPX magic bytes)
- Zollard worm
- Remaiten (Linux/Remaiten ELF bot)
The killer also zeroes its own path buffers after each scan iteration to prevent other malware's memory scanners from finding its strings.
CNC Server (cnc/)
The CNC is written in Go and exposes three network interfaces:
Port 23 -- Dual-Purpose Listener (cnc/main.go)
A single TCP listener on port 23 handles both bot check-ins and admin telnet sessions. The initialHandler discriminates by reading the first bytes: if they match the \x00\x00\x00\x01 bot registration magic, the connection is handed to Bot.Handle(). Otherwise, it is routed to Admin.Handle() as an operator session.
Port 101 -- API Listener
A separate TCP listener accepts API-key authenticated attack commands. The wire format is API_KEY|COMMAND\n. This enables programmatic attack launching from external tools or reseller panels.
Admin Panel (cnc/admin.go)
The admin interface is a telnet-based CLI with Cyrillic prompts ("пользователь" for username, "пароль" for password), indicating a Russian-speaking operator. After login, it displays theatrical "hijacking" messages:
[+] DDOS | Succesfully hijacked connection
[+] DDOS | Masking connection from utmp+wtmp...
[+] DDOS | Hiding from netstat...
[+] DDOS | Removing all traces of LD_PRELOAD...
[+] DDOS | Wiping env libc.poison.so.1
The admin prompt shows a live bot count in the terminal title: \033]0;%d Bots Connected | %s\007.
Admin commands include:
- Attack dispatch:
<attack_type> <targets> <duration> [flags](e.g.,syn 192.168.1.0/24 300 dport=80) - Bot count prefix:
-N <command>limits the attack to N bots - Category targeting:
@category <command>targets bots by source identifier (admin-only) adduser: Creates new operator accounts with configurable bot limits, duration caps, and cooldownsbotcount: Shows bot fleet distribution by architecture/source
Attack Orchestration (cnc/attack.go)
The CNC parses attack commands using go-shellwords for shell-like argument splitting. Each attack type has a defined set of allowed flags. The source flag (spoofed IP) is restricted to admin users only (admin == 0 && flagInfo.flagID == 25). Attack commands support CIDR notation for targets with up to 255 target prefixes per command.
Database Layer (cnc/database.go)
MySQL-backed with the following schema inferred from queries:
userstable:username,password(plaintext),max_bots,admin,last_paid,intvl,wrc,cooldown,duration_limit,api_keyhistorytable:user_id,time_sent,duration,command,max_botswhitelisttable:prefix,netmask
The login query includes a subscription check: wrc = 0 OR (UNIX_TIMESTAMP() - last_paid < intvl * 24 * 60 * 60). This confirms the botnet operates as a DDoS-for-hire service with paid access and expiring subscriptions.
Attack rate-limiting is enforced per-user: time_sent + duration + cooldown > UNIX_TIMESTAMP() prevents overlapping attacks. A whitelist table prevents attacks against protected prefixes (likely the operator's own infrastructure or paying customers).
Passwords are stored and compared in plaintext. Database credentials are hardcoded: root:password@127.0.0.1/mirai.
Build Infrastructure (build.sh)
The build script cross-compiles for 10 architectures:
| Toolchain | Output Binary | Notes |
|---|---|---|
i586-gcc | mirai.x86 | x86 (static) |
mips-gcc | mirai.mips | MIPS big-endian (static) |
mipsel-gcc | mirai.mpsl | MIPS little-endian (static) |
armv4l-gcc | mirai.arm | ARMv4 (static) |
armv5l-gcc | mirai.arm5n | ARMv5 (dynamic -- no -static flag) |
armv6l-gcc | mirai.arm7 | ARMv6 (static) |
powerpc-gcc | mirai.ppc | PowerPC (static) |
sparc-gcc | mirai.spc | SPARC (static) |
m68k-gcc | mirai.m68k | Motorola 68k (static) |
sh4-gcc | mirai.sh4 | SuperH SH4 (static) |
Each binary is aggressively stripped with --strip-unneeded and removal of .note, .comment, .eh_frame, .jcr, .got.plt, and ABI tag sections to minimize file size. Compiler flags include -O3 -fomit-frame-pointer -fdata-sections -ffunction-sections -Wl,--gc-sections for dead code elimination.
Two build variants are produced: mirai.* (with MIRAI_TELNET and KILLER_REBIND_SSH) and miraint.* (without scanner or killer rebinding -- likely a lightweight variant for already-compromised hosts).
The build also compiles supporting Go tools: the CNC server and scanListen (a scan results collector).
IOCs
Hardcoded Network Indicators
| Indicator | Type | Context |
|---|---|---|
45.137.70.27 | IPv4 | Fake/placeholder CNC address (FAKE_CNC_ADDR) |
cnc.changeme.com | Domain | CNC domain (obfuscated in string table) |
report.changeme.com | Domain | Scan results reporting server |
| TCP/23 | Port | CNC communication + admin telnet |
| TCP/101 | Port | CNC API listener |
| TCP/48101 | Port | Single-instance mutex + scan report listener |
Default Credential Pairs (Top 20 by Weight)
root:xc3511 root:vizxv root:admin
admin:admin root:888888 root:xmhdipc
root:default root:juantech root:123456
root:54321 support:support root:(empty)
admin:password root:root root:12345
user:user admin:(empty) root:pass
admin:admin1234 root:1111
Database Credentials
MySQL: root:password@127.0.0.1/mirai
Process Artifacts
| Indicator | Context |
|---|---|
/dev/watchdog or /dev/misc/watchdog | Watchdog disarm |
Port 48101 bound on 127.0.0.1 | Single-instance mutex |
./dvrHelper | Expected binary name (verified by obfuscated check) |
.anime in binary path | Competing malware signature |
| Random 12-24 char alphanumeric process name | prctl(PR_SET_NAME, ...) |
Compile-Time Defines
MIRAI_TELNET -- Enables scanner module
MIRAI_SSH -- (Unused in recovered source)
KILLER_REBIND_TELNET -- Kills and rebinds tcp/23
KILLER_REBIND_SSH -- Kills and rebinds tcp/22
KILLER_REBIND_HTTP -- Kills and rebinds tcp/80
Memory Scan Signatures (Competing Malware)
The killer module scans process memory for these obfuscated strings to identify and kill competing botnets:
- QBOT/Bashlite report string
- QBOT HTTP flood string
- QBOT duplicate instance string
- UPX packer magic bytes
- Zollard worm identifier
- Remaiten bot identifier
Conclusion
This source dump represents a fully operational Mirai variant with incremental improvements over the original 2016 leak: a Go-based CNC (replacing the original C implementation), an HTTP API for programmatic access, subscription-based access control with cooldown enforcement, and anti-DDoS-mitigation awareness in the HTTP flood module. The Cyrillic admin interface and subscription model confirm this variant was operated as a Russian-language DDoS-for-hire service. The 62-entry credential table covers the most common IoT default passwords across major manufacturers including Dahua, XiongMai, HiSilicon, Huawei, ZTE, Ubiquiti, and Realtek, making it effective against a broad swathe of consumer and enterprise IoT devices.
The changeme.com placeholder domains suggest this was a template/builder kit intended for redistribution rather than a single operator's deployment -- buyers would substitute their own C2 infrastructure before compilation.