Honeypot capture
mirai
A honeypot sensor network captured an ELF binary for 32-bit ARM. Static analysis of it recovered two command-and-control endpoints across two hosts under one registered domain, cmd[.]5gf7jtfk0y[.]st and fb[.]5gf7jtfk0y[.]st. Command and control is the channel an infected machine uses to reach its operator for instructions. The sample is built for armv7 and arm, the signature of a campaign aimed at routers, cameras and other embedded devices. The mirai / ddos-bot classification carries confidence 0.85 and records the code present in the binary rather than a demonstrated capability.
| SHA-256 | d8712c619b951fc2ea15704ee2002332fdb67d81cbf7f2213d083012f97c4c2d |
| SHA-1 | 3a8397e154cc9a224e9e8f31fee18af12846bc18 |
| MD5 | 5d5b6ec9d0a1d9b1b2aedef26b9df749 |
| ssdeep (a fuzzy hash that stays similar across small edits, so near-identical variants match) | 768:xdnNfPuDl74H89zs7skj9iI6PKww3Nlyrit07StQitQTHve7n:xdnNfW5Zzs7skj97btNlyriC7+eve |
| File type | ELF 32-bit LSB executable, ARM, EABI4, statically linked, stripped, no section headers |
| Size | 44,096 bytes |
| Submitted filename (attacker-controlled, treated as data) | garm7 |
The XOR-encoded string table
All configuration strings, both C2 hostnames included, are stored XOR-encoded in rodata and decoded in place at first use, leaving 25 plaintext ASCII strings in the binary. The 32-bit key 0xDEADBEEF sits at 0x1aaf8, stored little-endian as ef be ad de, and the decoder at 0x98b0 applies it as four sequential byte-XORs per output byte. Those four XORs fold to an effective one-byte key of 0x22, which is the value 0xDE ^ 0xAD ^ 0xBE ^ 0xEF.
That folded byte is the canonical Mirai table.c key, and the table it decodes, id-indexed at 0x1d750 with stride 8, is one of six structural markers of the leaked Mirai source present simultaneously in the sample. The other five are the xorshift128 PRNG at 0x92bc seeded at 0x9318, the hand-rolled DNS client at 0x93a8, the single-instance lock at 0x89d8 with its /proc/net/tcp killer at 0x84e8, the 2-byte big-endian length framing with its zero-length keepalive, and the command layout at 0x82cc that reads duration, vector, target and flags in that order. Both C2 hostnames are entries in that table.
The two recovered endpoints
The table below lists the two recovered endpoints, with values defanged. Source is the decoded string table, VA is the virtual address of the encoded entry, and the verified column records what the independent Ghidra session plus radare2 confirmed in the consensus round.
| Value | Port | Proto | Role | Table id | VA | Conf | Verified |
|---|---|---|---|---|---|---|---|
cmd[.]5gf7jtfk0y[.]st | 9999 | tcp | primary | 3 | 0x12534 | 0.95 | yes |
fb[.]5gf7jtfk0y[.]st | 9999 | tcp | fallback | 4 | 0x12548 | 0.45 | qualified |
The fb[.]5gf7jtfk0y[.]st entry carries confidence 0.45, because an exhaustive linear sweep of the executable segment found the string-table accessor called with ids 2 and 3 only, never with id 4. The fb[.] label is configured but unreferenced, so reaching it would require code that this build never executes. It is reported because it is real operator infrastructure under the same registered domain and is worth blocking. Neither host was contacted or resolved in the course of this analysis, so whether either is still live is unknown.
The bot queries 8[.]8[.]8[.]8:53/udp on every connection cycle, and the endpoint list above omits that address. It is Google Public DNS being abused as a resolver, and blocking it would be an operational error; it is recorded instead in the transport stack and the DNS-resolution capability.
The bundled DNS resolver
The bot carries its own DNS client and resolves the C2 name before every connection rather than calling any system resolver. The resolver at 0x93a8 builds A-record queries by label-length encoding, sends them over UDP to the hardcoded 8[.]8[.]8[.]8:53, handles name-compression pointers in the answer, and picks one returned A record at random through the xorshift PRNG. Compression-pointer bounds were not fully traced.
The wire protocol
The channel is raw TCP with a 2-byte big-endian length prefix. The wire-format document reports no encryption or encoding above that framing, while the profile record lists the C2 traffic as obfuscated with XOR; the bundle is inconsistent on this point and the reading below follows the wire-format document. socket(AF_INET, SOCK_STREAM) sits at 0xaf10 and connect at 0x906c, reaching the resolved address, with the port built as an immediate at 0x9048:
mov r3, #0xf20
add r3, r3, #7 ; 0x0f27
A length of 0x0000 is a keepalive; a length above 0x0400 tears the connection down. Byteswap and threshold both live at 0x91c4. select() runs a 10-second timeout at 0x8df8, and a keepalive goes out on every sixth timeout, i.e. every 60 seconds (0x8e34, 0x908c).
The bot understands the two framed messages in the table below, where selector gives the framed length and handler gives the virtual address reached, verified by the method described above.
| Selector | Handler | Behaviour | Verified |
|---|---|---|---|
0x0000 | 0x8fb0 | keepalive; 2 bytes consumed, no dispatch | corrected in consensus, not re-verified |
0x000c–0x0400 | 0x82cc → 0x81d0 | DDoS tasking: duration u32be, vector u8, target list (IPv4/CIDR), TLV flags | yes |
On the wire-format document’s reading, an interceptor reads both message types without any key. A keepalive frame shows a live infected host on a 60-second cadence. An attack command carries the duration, the vector and the target IP/CIDR list in cleartext before the attack begins, and that target list names the third-party IPv4 addresses and CIDR ranges the operator has tasked the bot to attack.
The DDoS command path
The path runs from the parser through the fork and stops where a vector would be selected. The parser at 0x82cc reads duration, vector, target list and TLV flags; the launcher at 0x81d0 double-forks and bounds the child by the commanded duration. The vector table at 0x1ac60 and its count at 0x1ac5c live in .bss, and an exhaustive linear sweep of the executable segment 0x8000–0x12a34 found only instructions that read them and none that writes either. With a count of zero, the launcher falls into an infinite loop that immediately calls abort() at 0xc5a0, so a commanded attack reaches a forked child that dies and no flood method in this build runs. The sweep covered literal pools, so a write performed through pointer arithmetic would evade it, and whether any path outside the executable segment populates 0x1ac60 was not established.
The unreferenced strings
The strings wget, curl, tftp, socat and /login carry zero code or data cross-references. They are inherited from the parent codebase, and the syscall set contains no execve that could use them.
The host footprint
The strongest host-side signature of the sample is a process named hDvrHelper that owns a listening socket on TCP 48101. The masquerade at 0x8c44 writes hDvrHelper into argv[0] and into the kernel comm field via prctl(PR_SET_NAME), so that string is what ps and /proc/[pid]/comm report, presenting the process as DVR vendor software. The argv scrubbing at 0x8c04 zeroes every argv string first, destroying the original command line in /proc/[pid]/cmdline.
Before binding, the bot learns its outward-facing IPv4 at 0x9d7c by opening a UDP socket, connecting it to 8[.]8[.]8[.]8:53, which sends no packet, and reading back the kernel-selected source address with getsockname, then storing the result at 0x1d73c and binding to it.
The bot binds and listens on TCP 48101 purely to hold it as a single-instance lock. If the bind fails, the bot connects to the current holder, walks /proc/net/tcp for the LISTEN entry on that port, resolves the owning process and kills it before retrying. The tail of that killer at 0x84e8 decompiled only partway, so how the socket inode parsed out of /proc/net/tcp is mapped back to an owning PID before kill() was not determined.
After the second fork the bot daemonises, redirecting stdin, stdout and stderr to /dev/null via dup2, and it writes the banner listening tun0 to stdout once beforehand. The bot never persists to disk, because it writes no boot path and its syscall set contains no rename, symlink or chmod.
Runtime requirements
Execution requires only user privilege, since 48101 is unprivileged. The bot needs outbound TCP/9999 to cmd[.]5gf7jtfk0y[.]st, outbound UDP/53 to 8[.]8[.]8[.]8, and an inbound bind on TCP/48101 at the local address.
The published artifacts
The published artifacts are c2_profile.json, iocs.csv, blocklist.txt, protocol_spec.md and the per-phase JSON records, together with the executable tools in reproduction/: the dissector c2_dissect.py, which replays all three protocol test vectors through validate_protocol.py along with a mutation check; a client that impersonates the sample against a real C2; and an emulator that stands up a fake one. Those tools are live-C2 code and touch the network only when explicitly given --target or --listen; they should be run only on an isolated network. Report rendering was unavailable for this case, and no Tier 1 artifact was fabricated by hand; the gates that ran were jsonschema against the published schemas, validate_profile.py for schema and extractor reconciliation, and validate_protocol.py, all passing.
What is left to decompile
The killer tail at 0x84e8 and the resolver’s compression-pointer bounds remain open, as does whether the empty vector table at 0x1ac60 is written by any path outside the executable segment.
- Family
- mirai
- First seen
- September 14, 2026
- Vector
- Runs as a ddos-bot; the entry vector is not established by static analysis
- Format
- 43 KB arm ELF 32-bit LSB executable
- VirusTotal
- Not on VirusTotal
- Tags
- arm · ddos-bot · elf · raw_tcp
- Sample
- By request. Email [email protected]
SHA-256
-
d8712c619b951fc2ea15704ee2002332fdb67d81cbf7f2213d083012f97c4c2das captured
Analysis performed using an automatic malware analysis pipeline using Binary Ninja