< Back to blog
high🤖Botnet
investigatedMarch 7, 2026publishedMarch 7, 2026

Anatomy of a Mirai Variant: Full Source Code Recovery of an IoT Botnet

#botnet#c2#iot

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:

  1. Self-deletion -- unlink(args[0]) removes its own binary from disk immediately.
  2. Watchdog disarm -- Opens /dev/watchdog or /dev/misc/watchdog and issues ioctl(wfd, 0x80045704, &one) to disable hardware watchdog timers that would otherwise reboot the device on process hang.
  3. Anti-debug trap -- Installs SIGTRAP handler via signal(SIGTRAP, &anti_gdb_entry). The anti_gdb_entry function swaps a function pointer (resolve_func) from a dummy address to the real resolve_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.
  4. 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 call table_init(). This is the function that populates all obfuscated string constants.
  5. 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 via accept() on fd_ctrl), then calls killer_kill_by_port() to forcibly terminate the old process and recurses.
  6. Process name randomization -- Overwrites argv[0] and calls prctl(PR_SET_NAME, ...) with random alphanumeric strings to hide from ps output.
  7. Daemonization -- fork(), setsid(), closes STDIN/STDOUT/STDERR.
  8. Module initialization -- attack_init(), killer_init(), and conditionally scanner_init() (gated behind MIRAI_TELNET define).

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\x01 followed 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:

IDNameFunctionProtocol Layer
0udpattack_udp_genericUDP flood
1vseattack_udp_vseValve Source Engine query flood
2dnsattack_udp_dnsDNS water torture
3synattack_tcp_synTCP SYN flood
4ackattack_tcp_ackTCP ACK flood
5stompattack_tcp_stompTCP STOMP (established conn flood)
6greipattack_gre_ipGRE-encapsulated IP flood
7greethattack_gre_ethGRE-encapsulated Ethernet flood
9udpplainattack_udp_plainOptimized high-PPS UDP flood
10httpattack_app_httpHTTP 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:

WeightUsernamePasswordTarget Device Type
10rootxc3511XiongMai DVRs/IP cameras
9rootvizxvDahua DVRs
8rootadminGeneric embedded Linux
7adminadminGeneric routers/cameras
6root888888Dahua DVRs
5rootxmhdipcXiongMai IP cameras
5rootdefaultVarious IoT
5rootjuantechJuanTech IP cameras
5root123456Generic
5root54321Generic
5supportsupportHuawei/ZTE routers
4root(empty)Misconfigured devices
4adminpasswordGeneric
4rootrootGeneric embedded
4rootankoAnko 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 and kill -9 the 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/exe against 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 cooldowns
  • botcount: 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:

  • users table: username, password (plaintext), max_bots, admin, last_paid, intvl, wrc, cooldown, duration_limit, api_key
  • history table: user_id, time_sent, duration, command, max_bots
  • whitelist table: 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:

ToolchainOutput BinaryNotes
i586-gccmirai.x86x86 (static)
mips-gccmirai.mipsMIPS big-endian (static)
mipsel-gccmirai.mpslMIPS little-endian (static)
armv4l-gccmirai.armARMv4 (static)
armv5l-gccmirai.arm5nARMv5 (dynamic -- no -static flag)
armv6l-gccmirai.arm7ARMv6 (static)
powerpc-gccmirai.ppcPowerPC (static)
sparc-gccmirai.spcSPARC (static)
m68k-gccmirai.m68kMotorola 68k (static)
sh4-gccmirai.sh4SuperH 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

IndicatorTypeContext
45.137.70.27IPv4Fake/placeholder CNC address (FAKE_CNC_ADDR)
cnc.changeme.comDomainCNC domain (obfuscated in string table)
report.changeme.comDomainScan results reporting server
TCP/23PortCNC communication + admin telnet
TCP/101PortCNC API listener
TCP/48101PortSingle-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

IndicatorContext
/dev/watchdog or /dev/misc/watchdogWatchdog disarm
Port 48101 bound on 127.0.0.1Single-instance mutex
./dvrHelperExpected binary name (verified by obfuscated check)
.anime in binary pathCompeting malware signature
Random 12-24 char alphanumeric process nameprctl(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.

Share: