Honeypot capture
mozi
A honeypot sensor network captured an ELF built for MIPS big-endian, and static analysis of its control channel recovered twelve endpoints, eleven of which carry an agreement verdict from a second, independent decompiler backend that resolved the same addresses. C2 is the channel an infected machine uses to reach its operator for instructions. The twelve break down as eight public BitTorrent DHT bootstrap nodes shared by every BitTorrent client, two hardcoded DNS resolvers, one public-IP discovery URL and one payload template, and three of the bootstrap nodes belong to the Transmission project, to Debian, and to BitTorrent Inc. This family, Mozi, runs its control channel over third-party infrastructure, and the binary holds no operator-owned address to seize.
The sample builds for two architectures, mips and mips-be, and the analysis treats that pairing as the signature of a campaign aimed at routers, cameras and other embedded devices rather than at servers. That hardware is permanently online and rarely patched or monitored. The sample behaves as a worm, copying itself to further machines with no user action — across a network or through exposed services — using a fourteen-record IoT exploit table. The sample is a peer-to-peer IoT bot that takes its tasking as an ECDSA-P384-signed, tag-delimited configuration published into the BitTorrent Mainline DHT, serves per-architecture /Mozi.[arch] payloads from the infecting peer, and carries HTTP-flood and raw-packet attack modules.
Table 1: a stripped, statically linked MIPS-I big-endian ELF inside a tampered UPX wrapper
File identity as read from the submitted sample. The unpacked SHA-256 is of the image produced by repairing the UPX header; every virtual address below refers to that image. ssdeep is a fuzzy hash that stays similar when a file is edited slightly, so near-identical variants match.
| SHA-256 | 2e4506802aedea2e6d53910dfb296323be6620ac08c4b799a879eace5923a7b6 |
| MD5 | fbe51695e97a45dc61967dc3241a37dc |
| SHA-1 | 1ed14334b5b71783cd6ec14b8a704fe48e600cf0 |
| ssdeep | 3072:/TNVO/QJHZcfFj4rwLQGTNO5VZLwHm7vuQTpZUyY6cot:7O/QJHZweEL/NOjCHm7FZZncI |
| File type | ELF 32-bit MSB executable, MIPS, MIPS-I, statically linked, stripped |
| Size | 132,876 bytes (packed) |
| Unpacked SHA-256 | 8f3a5bc6088b999d50bce0eef02c41860bc8ac5e63a2379508c20a1c188eb38d |
| Family | mozi, classification confidence 0.9 |
Static analysis of a packed, stripped binary
The sample was loaded as data throughout, with no recovered endpoint contacted or resolved. The analysis read strings and structure with strings and rabin2, anchoring every indicator to a file offset and corroborating it across both tools, and it disassembled and decompiled the executable sections headlessly in Binary Ninja, running a second decompiler backend as the consensus pass that produced the agreement verdicts reported below. The analysis is automatic and no individual analyst performed the work; the run as a whole is recorded at medium confidence. Because nothing was executed and no endpoint was resolved, the analysis did not establish the sample’s runtime timing, retry and post-execution behaviour, or which addresses the recovered endpoints resolve to now and whether they still answer on the recovered port.
The file is UPX-packed, banner UPX!, with the p_info header field zeroed so that upx -d aborts — “CantUnpackException: p_info corrupted” — while the runtime stub still self-unpacks normally. The analysis restored the true size from the intact UPX PackHeader trailer and ran upx -d on a copy. Entropy here measures how random the bytes are. The executable segment measures 7.8142 bits of entropy per byte, which is consistent with a packed or encrypted payload, since plain machine code does not compress to near-random. The ELF section headers are also stripped, so symbol- and section-based tooling has nothing to read while the loadable segments still run, and capability had to be recovered from syscall wrappers. Once unpacked, the sample does not resist static analysis. There is no ptrace self-debugging, no VM or sandbox detection, and no mprotect runtime code generation. FLOSS and capa produced no output at all for this sample.
The DHT control plane
The bot joins the public BitTorrent Mainline DHT as a full Kademlia node over UDP and uses KRPC get_peers/announce_peer as its command-and-control rendezvous. The bencoded KRPC parser sits at 0x4166ec and the builders at 0x418370 and 0x418b64. It issues ping, find_node, get_peers and announce_peer, and because the operator publishes the signed configuration on a rendezvous info_hash, bots retrieve it with get_peers. The implant bootstraps from eight hardcoded :6881 entries, a finding assessed at confidence 0.9 and mapped to ATT&CK T1071. The decompiler reads the first entry as a plain string:
'dht.transmissionbt.com:6881'— binja @0x492d74
and a second quote records the same string at 0x440574: [binja-va] 0x440574 dht.transmissionbt.com:6881.
Table 2: all eight bootstrap entries are public DHT nodes, agreed by both backends Recovered as strings anchored to the virtual addresses shown and confirmed in the second-backend consensus round. Defanged for safe reading.
| Bootstrap entry | Port | VA | Second backend |
|---|---|---|---|
dht[.]transmissionbt[.]com | 6881 | 0x492d74 | agreed |
router[.]bittorrent[.]com | 6881 | 0x492d78 | agreed |
router[.]utorrent[.]com | 6881 | 0x492d7c | agreed |
bttracker[.]debian[.]org | 6881 | 0x492d80 | agreed |
212[.]129[.]33[.]59 | 6881 | 0x492d84 | agreed |
82[.]221[.]103[.]244 | 6881 | 0x492d88 | agreed |
130[.]239[.]18[.]159 | 6881 | 0x492d8c | agreed |
87[.]98[.]162[.]88 | 6881 | 0x492d90 | agreed |
These eight hosts are shared by all BitTorrent users, so blocking them would be ineffective. The four remaining recovered endpoints are the two hardcoded resolvers, the public-IP discovery URL and the payload template. The resolvers are 8.8.8.8 and 114.114.114.114, reached by the sample’s own DNS client at 0x43f4e4 in place of /etc/resolv.conf; ipinfo.io/ip at 0x43f6cc serves public-IP discovery; and the payload template hxxp://%s:%d/Mozi.m at 0x48c581 has its host filled in at runtime with the infecting peer’s own address. In iocs.csv both resolvers appear as verified ipv4 rows, and the renderer labels their context “primary/fallback C2” from their endpoint role. Both addresses are DNS resolvers rather than C2; the consensus verdict covers the location of those strings at 0x43f4e4 and does not cover the C2 label.
The analysis did not trace the get_peers target derivation within budget, so the info_hash the operator publishes on is unknown, and c2_client.py can join the DHT and issue the sample’s own queries without locating the operator’s swarm. The live [cnc] controller address has the same status — it arrives only inside a signed config, and the binary carries no static default.
The signed configuration
The bot verifies every configuration with ECDSA over NIST P-384 before acting on it, a finding assessed at confidence 0.9 and mapped to ATT&CK T1573. The body is a 528-byte plaintext, tag-delimited document followed by a 96-byte signature — r‖s, 48 bytes each. The verifier at 0x40e5b4 checks it against the operator’s public key and the bot proceeds only when the gate at 0x419130 returns 1; the curve constants at 0x48b2c8 (p), 0x48b238 (n) and 0x48b268/0x48b298 (G) match secp384r1 byte for byte. The config is signed but not encrypted, so an interceptor who captures one config reads the [cnc] controller address, which attacks are enabled, and any update or download URL. reproduction/c2_dissect.py performs that decoding, and all four protocol test vectors replay through the dissector, including correct rejection of a forged signature.
The public key is 49 bytes, SEC1-compressed, reassembled from the swizzled X coordinate at 0x492d94 plus the parity byte at 0x492dc4:
03b6632976ac2f9e9438319b126973a92effad968d188b791b695f9dca8de27d639dc1684ed84b7d5f4cb38f68c12670eb
The operator’s private key signs any config the sample will act on, and that key is not in the binary and is computationally infeasible to recover, so the analysis could reproduce only the DHT-node side of the server. c2_emulator.py answers KRPC queries and frames a correct unsigned config, and the sample reaches that config and rejects it at 0x419140.
The bot reads sixteen bracket tags from the config, all of which the consensus round located. The ones with a recovered function are [cnc] (auxiliary TCP controller, 0x40a7e4), [atk] (attack enable, a one-character boolean compared against "1", feeding 0x40ff94 and 0x4259ec), [dip] (attack target), [ud] (self-update, 0x415e44), [dr] (download-and-run, 0x416318) and [hj] (web-hijack toggle, 0x41eef0, gating JavaScript blobs that rewrite victim web traffic through cookie clearing, meta-refresh redirects and WebKit click synthesis, at confidence 0.7). Payloads pulled by [ud] and [dr] are re-verified with the same key before execution. Seven tags — [ss], [sv], [rn], [hp], [idp], [nd], [count] — have readers that only store or compare the value, so their semantics stay open.
A 0x9e3779b9 constant in main looked like an XXTEA delta on an earlier pass, but the raw disassembly at 0x422ecc shows the call setting only two argument registers, so the analysis dismissed the constant as a MIPS-o32 HLIL artifact. Constant searches for AES, RC4, ChaCha and MD5 came back empty and .data entropy is 2.19, against 7.8142 bits per byte for the executable segment, and the config-authentication ECDSA is the sample’s only cryptography.
Self-propagation and the attack modules
The bot propagates from a fourteen-record exploit table at 0x48b33c, a finding assessed at confidence 0.9 and mapped to ATT&CK T1105. It covers GPON diag_Form (tcp/8080 and tcp/80), Realtek SDK miniigd (tcp/52869), Netgear setup.cgi (tcp/8080 and tcp/80), Huawei HG532 DeviceUpgrade (tcp/37215), TR-064/CWMP SetNTPServers (tcp/7574 and tcp/5555), D-Link HNAP1 (tcp/80), CrossWeb/JAWS cameras (tcp/81 and tcp/80), D-Link soap.cgi (tcp/49152), r7064 (tcp/8443) and Vacron DVR (tcp/8080). Each exploit body names its own drop path: /tmp/Mozi.m for the UPnP, HNAP1 and D-Link bodies, /var/Mozi.m for the UPnP AddPortMapping body, /tmp/huawei for HG532, /tmp/tr064 for TR-064, and /tmp/gpon8080 or /tmp/gpon80 depending on which GPON port was used.
The bot delivers payloads peer to peer, running its own HTTP server at 0x4212b4 that serves thirteen per-architecture names, /Mozi.{m,a,b,c,x,4,6,7,k,l,p,r,s}, so a freshly exploited device fetches from the peer that infected it, at confidence 0.85. The bot picks targets on the local network, sending SSDP M-SEARCH to 239.255.255.250:1900 and reading /proc/net/route, /proc/net/tcp and /proc/net/raw to pick an egress interface and find neighbours, and at startup it scrapes telnet-reachable peers from /proc/net/tcp into /tmp/.ips.
The analysis recovered the HTTP layer-7 flood at 0x43f5a4 — a spoofed Referer: http://baidu.com/... header and a Cloudflare-mimicking POST /cdn-cgi/ path, confidence 0.8. It did not trace the internals of the attack engine at 0x40ff94 (5,932 bytes) or the exploit/scanner engine at 0x4259ec (21,752 bytes), and it left the twenty setsockopt option constants unresolved, so whether the raw-socket paths spoof the source IP is open.
The correlation pass found router[.]utorrent[.]com in four other analysed captures, and the analysis treats that as evidence of a wider campaign rather than a one-off; router[.]utorrent[.]com is also one of the public bootstrap hosts listed above.
Device takeover and host artifacts
The supervisor function at 0x422974 takes control of the device before the bot does anything else. It daemonises, installs persistence, kills competitors, masquerades its process name, forks four workers — the P2P HTTP payload server, the attack engine, the exploit/scanner engine and the HTTP/web module — and then runs the DHT loop at 0x41a1e0 as its steady state. Every host command it issues funnels through one /bin/sh -c choke point at 0x436ef0, whose only execve caller is 0x4374d0; the analysis assesses that choke point at confidence 0.95 and maps it to ATT&CK T1059.004.
The implant writes its persistence into embedded-Linux boot paths, using /etc/init.d/S95baby.sh, a SysV init script with a deliberately innocuous name, plus /etc/rc.local, /etc/rc.d/rc.local and /etc/rcS.d/, and on OpenWrt it runs mount -o remount,rw /overlay / to get write access for /overlay/upper/etc/init.d (0x4152d0, confidence 0.85).
It then holds the hardware watchdog open so the device cannot reboot out of the infection, probing seven paths including /dev/watchdog, /dev/watchdog0, /dev/misc/watchdog and /dev/FTWDT101_watchdog. It installs iptables DROP rules on ports 22, 23, 2323, 7547, 35000, 50023 and 58000, which closes the management and exploitation ports behind it against both rivals and the owner, with an ACCEPT rule for its own runtime-chosen UDP port.
It repoints the TR-069 ACS management URL at 127.0.0.1 through cfgtool/sendcmd (0x43f8e8, confidence 0.8), severing the ISP’s ability to remotely remediate the router. It kills telnetd, utelnetd and scfgmgr, walks /proc/[pid]/exe to kill competing bots, and overwrites argv[0] with sshd when /usr/bin/python is present, or dropbear as the alternative, so the process list looks ordinary.
Table 3: the host artifacts cluster in /tmp, /etc/init.d and the boot scripts
Values recovered as strings at the virtual addresses shown and carried into iocs.csv; confidences are per-indicator assessments from the analysis.
| Kind | Value | Context | VA | Conf |
|---|---|---|---|---|
| file | /tmp/Mozi.m, /var/Mozi.m | dropped payload | 0x48f3f0 / 0x48c581 | 0.90 |
| file | /etc/init.d/S95baby.sh | SysV init persistence | 0x43fd10 | 0.85 |
| file | /tmp/.ips | harvested telnet-peer list | 0x422974 | 0.85 |
| file | /tmp/gpon8080, /tmp/huawei, /tmp/tr064 | per-exploit drop paths | 0x48b350 ff. | 0.85 |
| file | /dev/watchdog | held open against reboot | 0x4217a0 | 0.85 |
| process_name | sshd, dropbear | argv[0] masquerade | 0x422974 | 0.85 |
| file | /etc/rc.local | boot-script persistence | 0x43f864 | 0.80 |
| directory | /overlay/upper/etc/init.d | OpenWrt overlay persistence | 0x44008c | 0.80 |
The set also includes a single-instance lock at /var/run/baby, with the same /run + /baby pair probed under /var/, /tmp/ and /proc/. To run with full effect the implant needs root, for the iptables rules, the /etc/init.d and /overlay writes, the raw-socket attacks and the watchdog device. It also needs outbound udp/6881, an inbound and outbound runtime-chosen UDP port, udp/53 to its two resolvers, and tcp/80 for payload fetch, ipinfo.io and exploit delivery, and denying any one of those blocks it.
Artifacts
Indicators that remain usable
The propagation signature is the outbound GET /Mozi.[arch] HTTP/1.0 retrieval from the infecting peer, which the Suricata rule in detections/ keys on. The analysis recommends hunting /tmp/Mozi.m, /var/Mozi.m, /etc/init.d/S95baby.sh and /tmp/.ips across IoT and router estates; all four are rows in iocs.csv. The ECDSA public verification key survives repacking and IP churn and identifies whoever can sign tasking for this botnet. Given the shell choke point and the watchdog hold above, defenders should treat any infected device as fully attacker-controlled.
- Family
- mozi
- First seen
- October 24, 2026
- Vector
- Runs as a ddos-bot/worm; the entry vector is not established by static analysis
- Format
- 130 KB mips ELF 32-bit MSB executable
- VirusTotal
- 47/74 engines: trojan.mirai/mozi
- Tags
- backdoor · ddos-bot · elf · mips · udp · worm
- Sample
- By request. Email [email protected]
SHA-256
-
2e4506802aedea2e6d53910dfb296323be6620ac08c4b799a879eace5923a7b6as captured -
8f3a5bc6088b999d50bce0eef02c41860bc8ac5e63a2379508c20a1c188eb38dunpacked payload
Analysis performed using an automatic malware analysis pipeline using Binary Ninja