ES EN
Index

shem · Chapter 29

It told me its name three times

Another IoT ELF with the look of a run-of-the-mill Mirai landed in the decoy. I christened it “zbot” after its cron files, and it turned out that wasn’t even its name: by three separate routes (the paths it left on disk, the banners it speaks SSH and HTTP with, and the first bytes of every message to its C2) the thing kept telling me its real name. Behind it was something more interesting than a server-flooding bot: a machine for renting out its victim’s home line as a proxy. And an operator who, with the whole thing built, kept breaking his own download URLs. XLab had already named it “Shemzbot” in a tweet; this is the teardown that tweet didn’t carry.

A 64-bit ELF landed in the trap: static, stripped, 1 MB. The standard look of an IoT bot, the kind I see daily. It came in over telnet, not SSH, and the distinction isn’t cosmetic: telnet on port 23 with the factory password is the oldest IoT vector there is, the one plenty of people don’t even watch any more. The credential that opened the door was root/xc3511: the XiongMai camera one, the same that lit up Mirai in 2016. Ten years on, it still works.

I christened it “zbot” because that’s what the cron files it drops on install are called. Spoiler: that isn’t even its name. But the fun of this one isn’t in the ending, it’s in the road there - in how it kept correcting the name for me, over and over, all on its own. So let’s take it in order.

SPECIMEN · bot.x86_64

shem alias “Shemzbot” (XLab)

◈ LIVE · DO NOT RUN
sha256 (with padding)
1d3c4108…9f46cf8b
sha256 (ELF, tail stripped)
82ab1202…0a2d8124
BuildID
8e7a8299…3ded94c
Type
ELF 64-bit LSB, x86-64, static, stripped
Size
1,088,003 B (ELF 1,086,272 + 1,731 tail)
Internal name
shem · “zbot” only in its crons

01The operator changes the hash on every download

The usual reflex: I take the file’s hash and look it up, to see if it’s already catalogued. It wasn’t: not on VirusTotal, not on MalwareBazaar. And for a mass-delivery, active bot, that doesn’t add up. Something that infects far and wide leaves samples everywhere; that his showed up nowhere smelled off. It smelled off because the hash was lying.

I looked inside the file: the ELF ended at its last loadable segment and yet the file carried on 1,731 bytes further. A tail glued to the end, maximum entropy (nearly 7.9 bits per byte): pure noise, no code, no text. I cut it off, recomputed the hash of what was left, and it turned up at once: catalogued, twenty engines flagging it.

end of the file: the tail that isn’t ELF
# the real ELF ends here
  offset 0x109340  (1,086,272)      ┤ end of the ELF
  offset 0x109340 … 0x109A02        ┤ 1,731 B (0x6C3) of garbage, entropy 7.88 b/byte
  file size = 0x109A03              (1,088,003 B)
  
  sha256(whole file)      = 1d3c4108…   → not catalogued (unique per download)
  sha256(ELF, tail removed) = 82ab1202… → MalwareBazaar / VirusTotal 20/…

I downloaded everything the server was handing out and confirmed the trick: every download gets a different random tail (I saw them from 182 to 2,020 bytes). Which means every file his box serves comes out with a different sha256. The upshot for a defender is harsh: the whole-file hash is worthless as an indicator. What holds up is the hash of the ELF without the tail, and above all the BuildID - the fingerprint the build leaves stamped inside, which survives both the tail and the renaming.

A nuance, because it matters for catching it The tail isn’t added by the bot: it’s added by the operator’s box as it serves the file. When an already-infected node hands its own copy to the next one, it forwards it as-is, same hash. The padding changes between the operator’s downloads, not within a propagation branch. Even so, the moral stands: don’t trust the whole-file hash.

02I expected an encrypted table. It was all in the clear.

Mirai forks keep their config in a table XOR-encrypted with what is, in practice, a one-byte key; you go looking for it by reflex, barely thinking. Here there was no table: the entire protocol definition sat in the clear in .rodata, in English, visible with a plain strings. The commands, their formats, the templates. (Traffic on the wire is another story: that part is encrypted, and I open it up in §05. But the thing’s vocabulary was handed to me.) The full list of commands it accepts from its C2, with their scanf formats - which, as a bonus, spelled out each one’s arguments:

CommandFormatWhat it does
PING / PONG-heartbeat
PROXYON / PROXYOFF-turns the proxy on / off (§06)
UPDATE%255s → /tmp/.updself-update
SHELLSHELL %s %s %sinteractive remote shell (/bin/sh -i)
UDPUDP %63s %d %dUDP flood (target, port, duration)
AMPAMP %63s %hu %d %15samplification DDoS (chargen, ntp)
TGTS / VTGT%23s %23s %uloads targets for the scanner
SCAN / STOPSCAN-starts / stops the scan
YOURIP%63sthe C2 tells it its public IP
NOREPL / NOTOR / NOIDIRECT-transport modifiers
DIE / LOLDONE / LOL-terminate / ack

And what it emits back: ZBOT %s %s (the check-in), ZCR1 %s (harvested credentials), and the scanner’s finds as %s|%s|telnet-brute. Among the strings, something a bot should never carry into production: the absolute paths from its author’s disk.

strings: the developer’s desktop, compiled in
/root/shem/targets.txt
  /root/shem/web_targets.txt
  /root/shem/targets_adb.txt      ← also keeps ADB targets
  
  SSH-2.0-sheM                    ← its SSH client banner
  User-Agent: sheM                ← in its HTTP requests
  SHEM_INST  SHEM_LAB  SHEM_NOMASQ  SHEM_RAM_MB

shem. That’s the name, and it leaves it all over its own disk: the /root/shem/… paths and the SHEM_INST, SHEM_LAB, SHEM_NOMASQ, SHEM_RAM_MB variables the developer left behind. “zbot” was only the label on its crons; shem is what its author calls it on his own machine. First time it tells me its name.

And it repeats it in how it introduces itself to the world: its SSH client announces SSH-2.0-sheM, and its HTTP requests ride with User-Agent: sheM. Signatures worth far more than “zbot”: the operator can switch servers tomorrow, but those don’t change without a recompile. Second time, by a different route.

03The slip that showed the operator’s panel

The attack log had impossible URLs in it. This one, which I break up so it isn’t clickable:

the loader: a URL that shouldn’t exist
hxxp://176.65.134[.]121:36023PROXYONUPDATE:7416/bot.$m
                          └── a port, a command, and another port ──┘

A port, the word PROXYONUPDATE, and another port. That’s not an address. My first read was that the bot was applying PROXYON and UPDATE to itself as arguments; I set up a test, with the junk tacked on and without it, and it came out exactly the same. The test caught nothing. I filed the lead as worthless - until I realised the fault was mine, not the lead’s. I was measuring the wrong thing; why, I work out in §06.

Because the broken URL, looked at properly, told something much better. I lined up several downloads and the pattern sang. The explanation that best fits the captures: his panel handles a “host” variable as {IP}:{random port}, and sometimes the queue of pending commands (PROXYON, UPDATE) gets concatenated behind it, slipping into the field where only the download address should go. On the days the field is clean, the download URL is valid; on the days it drags commands along, those download URLs fail. PROXYONUPDATE isn’t the bot’s: it’s almost certainly a leak from the attacker’s panel, and it offers a glimpse of how he’s wired his console.

04Into the cage: it speaks UDP, and says “SH”

Where the line is, and why I step right up to it Up to here it’s all cold analysis. But to see the protocol live I had to switch it on, and I do that where I always do: in an isolated cage, on the lab desktop, never on the decoy. Closed network, its own C2 pointed at a machine of mine, and the acid test before starting: not a single packet reaches the internet. Watching a bug in a no-exit box isn’t letting it loose: it’s looking inside it without it being able to touch anyone. That’s the line, and it’s the same one as always.

I detonated it with its C2 pointed at my own machine (the binary takes that as an argument, no patching needed). And the first thing it did already corrected two things I’d noted down wrong. It doesn’t talk TCP to its C2: it talks UDP, on port 6881, BitTorrent’s. Whether he picked it on purpose or it’s chance I don’t know, but the effect works for him: its UDP traffic on 6881 can pass for P2P noise, and anyone hunting it over TCP won’t see it go by. And the first three bytes of every message:

tcpdump: the beacon to the C2 (UDP/6881)
→ UDP  53 48 01                                  "SH" + type (hello)
  → UDP  53 48 06  e1 f0 46 e1 dc 85 60 52 c6 …    "SH" + check-in (encrypted)
            │
            └── 0x53 0x48 = "SH"  →  the initials of shem

53 48 is SH in ASCII: the initials of shem. The protocol itself signs with the name. Third time it tells me, and that’s three independent routes: the disk, how it introduces itself over SSH and HTTP, and now the two bytes that open every datagram to its C2. (Only those three bytes travel in the clear: the SH header + type. From there on everything is encrypted, starting with a 9-byte session identifier, random on each run except its last byte, which stays constant. It’s no use as an indicator, and §05 shows why it matters that it changes.)

05RC4 with a fixed key, broken without breaking it

After the SH header, the body was encrypted. I ran the binary through capa and it gave me the algorithm: RC4, a stream cipher. RC4’s golden rule is simple: the stream of bytes it generates, the keystream, is never reused. Two messages encrypted under the same keystream and the cipher falls apart.

I suspected it broke that rule, and checking was cheap: I captured the same check-in message on two separate runs, each with its own random session identifier. With a decent cipher, that encrypted body ought to come out different both times. It didn’t.

two runs, different session, same encrypted body
# datagram: [SH+type 3B] · [session 9B, from offset 3] · [body, from offset 12]
  # the session (9B) DOES differ between runs; here I compare only the BODY (offset 12+):
  run 1  (session e1f046…):  body = 01 2f f4 dc 8b 06 6e dd df 4b d3 e0 60 …
  run 2  (session ef368c…):  body = 01 2f f4 dc 8b 06 6e dd df 4b d3 e0 60 …
                                            ↑ byte-for-byte identical, despite different sessions
  
  ⇒ keystream reused: same key every run, independent of the session

The experiment proves one thing directly: reused keystream. With different nonces, the same plaintext yields the same ciphertext, so the cipher stream doesn’t change between runs and doesn’t depend on the nonce; the key, therefore, is constant (I didn’t open the KSA to pull out its bytes, but the effect is that of a fixed key). RC4’s textbook mistake. The nice part is you don’t need to attack RC4 with maths: it’s enough to recover the keystream. I set a breakpoint on the RC4 function, dumped the buffer before (plaintext) and after (ciphertext) the call, and with a plain K = plaintext ⊕ ciphertext I had the stream. The whole keystream I’m keeping to myself: publishing it would be handing over the key to decrypt and forge any node’s traffic, and this is an analysis, not a kit. With two different messages I confirmed K is always the same, applied to everything following the 3-byte header (offset 3: the cipher covers the session identifier and the body). From there, the channel with the C2 reads in the clear:

con el keystream, el canal C2 en claro
# K recovered with gdb (break @ 0x40a410). Shown deliberately trimmed:
  K = 0e7aeeba 7f3c91a0e4d28b651af09c37b802e5d49c6a1f08d3b47e52a0c9 …   ← keystream (redacted for OPSEC)
  
  # with K, the C2 channel reads in the clear:
  SH 06  <9B session> "ZBOT x86_64 overlay"   ← check-in: architecture + transport
  SH 06  <9B session> "ZCR1 <32hex>"          ← report of harvested credentials

And with the protocol in hand I stood up a sinkhole that speaks shem, all inside the cage. Here the bot gave itself away: I sent it an SH 02 over UDP and it noted me as a peer (it kept the source IP and port of my datagram), called me back over TCP and spoke SOCKS5 to me as a client. Live proof that, in overlay mode, it routes its egress through its own peers.

sinkhole: I inject a peer and the bot relays through me
→ UDP 6881  53 48 02            I reply "SH" 02 to the beacon
              → the bot notes me as a peer (my source IP:port)
  ← TCP       05 01 → CONNECT 127.0.0.1:1338   it speaks SOCKS5 to me as a client
  →           05 00 …            I complete the handshake: tunnel accepted
              (127.0.0.1:1338 = the destination it asks to reach through the tunnel; I'd be the exit)

06PROXYON: the switch that explains everything

When I detonated it, it opened three ports: the C2’s 6881/UDP and two TCP, 7331 and 7332. The 7331 finally answered the question I’d been dragging around since the PROXYON command. Because that port always opens and always listens. The difference isn’t in opening: it’s in the accept.

the switch, in the binary (decompiled)
connection = accept(...);
  if (switch == 0)  close(connection);       // off: hangs up on you, not a word
  else              spawn_thread(connection);  // on: serves you
  
  // switch (global) ← moved by exactly two commands from the C2:
  PROXYON  → switch = 1        PROXYOFF → switch = 0

It’s not a startup mode: it’s a remote control. And here is why my §03 test failed: from the outside, on and off have the same ports open. I was measuring the lock instead of checking whether the door opens. With the switch at 1, I sent 7331 a SOCKS5 greeting and it answered with the “go ahead”; with the switch at 0, it hung up on me without a word.

And this closes the §03 doubt: the proxy turns on through the C2 channel, not through a startup argument. So the PROXYONUPDATE stuck in the URL can’t be the bot applying commands to itself, and it fits the panel leak. Honestly, it’s inference, not a closed proof: I didn’t relaunch the bot with the junk attached while measuring the 7331 handshake, I only compared ports (which was the mistake). But with PROXYON already proven as a C2 command, it’s the only reading that fits everything.

talking to 7331 with the switch at 1
→ 05 01 00   SOCKS5 greeting (version, 1 method, no auth)
  ← 05 00      replies: it's a SOCKS5 proxy, serving

The word “SOCKS” doesn’t appear once in the binary: it’s implemented by hand, byte by byte. That “there are no proxy strings” that threw me off early wasn’t the absence of a proxy: it was the absence of literals. And to get out, the bot handles four transport modes (direct, relay, tor, overlay): it speaks SOCKS5 on both sides, offers it on 7331 and consumes it to route its own traffic, Tor among the options.

infected device(camera, router, home line)puertos = base + 2·N  (N = SHEM_INST)C2176.65.134.121UDP 6881magic “SH” · RC4proxy client(whoever uses it)TCP 7331SOCKS5 · only if PROXYONnew victimasks it for bot.<arch>TCP 7332serves its own binary over HTTP
A shem node opens three ports, and they’re a formula: 6881/UDP (command, encrypted), 7331/TCP (the SOCKS5 proxy, with a remote switch) and 7332/TCP (serves itself). With N = 0: 6881 / 7331 / 7332.

07There are no dedicated delivery servers. There are victims.

The third port, 7332, is a web server. I asked it for bot.x86_64 and it served me its own binary, byte-for-byte identical to the sample. Which is to say: each infected device is also the one that infects the next. No dedicated delivery server needed: the bulk of the delivery falls on the victims themselves. But the C2 serves too (the .se/.sk come from its IP, §10), so it isn’t all victim-to-victim.

And the three ports aren’t fixed: they’re computed, base + 2N with N = getenv("SHEM_INST"). The delivery ports of the C2’s loaders (7416 → N=42, 7420 → N=44, 7428…) all fit 7332 + 2N: it wasn’t “port rotation”, it was arithmetic. His box delivers mostly over 8080 by default, but it serves the binary on several of those high ports too. Shodan saw 7434 open (= 7332 + 2·51), which would point to an instance with N=51 running on the C2 itself. I leave that as a lead, not a fact: a full N=51 would also open 7433/TCP (SOCKS5) and 6983/UDP, and the Shodan record (22/8080/7434) doesn’t list them, probably because it doesn’t sweep those ports. What’s solid is that the operator serves from his box too.

C2 (Pfcloud)coordinates and also servesvictim Aresidential line (HK)serves bot on :7332victim Binfected by Anow serves on :7332victim C…the next onecommands (UDP/6881)the bulk of delivery falls on the victims; the C2 serves too
Propagation is self-replicating. The case’s second IP, which I’d noted as a “delivery server”, turned out to be a Hong Kong residential line: a victim acting as a distributor. And a home IP is, precisely, the merchandise sold as a “residential proxy”.

There you see what it’s built for: shem infects home lines, opens a SOCKS5 on them on demand, and along the way uses them to infect the next ones. The product isn’t the attack: it’s your connection.

08The propagation arsenal

Over telnet it hands out the bot.<arch> in the clear. But to spread across the network it also carries a set of exploits against edge devices, embedded as templates.

A note on rigour The binary doesn’t contain a single CVE-… string. The mapping below is my own identification of each endpoint, not a label the thing carries. The modules are labelled internally od_hnap1, od_netgear, od_fourfaith, od_thinkphp…
Endpoint (verified in the binary)DeviceMy identification
/ctrlt/DeviceUpgrade_1Huawei HG532CVE-2017-17215
/GponForm/diag_FormGPON routersCVE-2018-10561/10562
SetNTPServers (SOAP)TR-064 / CWMPNTP injection (Mirai 2016)
SetNTPServerCredentials (HNAP1)D-Link HNAPHNAP/SOAPAction injection (e.g. CVE-2015-2051)
/setup.cgi?…todo=syscmdNetgear DGNsetup.cgi syscmd RCE
/cgi-bin/luci/;stok=/locale?form=countryTP-Link Archer AX21CVE-2023-1389
/cgi-bin/nas_sharing.cgi?user=message…D-Link NASCVE-2024-3273
/apply.cgi (od_fourfaith)Four-FaithCVE-2024-12856 (likely)
/tmUnblock.cgiLinksys E-series«TheMoon»
thinkphpThinkPHP 5ThinkPHP RCE
/ftptest.cgi?loginuse=…DVR / cameraftptest.cgi injection

What matters isn’t that it uses old CVEs (the Huawei one is from 2017; the GPON and TR-064 ones, from 2016–18, are public knowledge): it’s that it mixes classics with fairly recent ones: TP-Link Archer from 2023, D-Link NAS and Four-Faith from 2024. It’s a maintained arsenal, not an abandoned copy-paste.

And a fine detail: there are two delivery styles. Some templates drop the bot in the clear; others deliver an encrypted chain, split into three pieces.

A · over telnetbot.<arch>IN THE CLEAR/tmp/.z → runB · over exploit (stealthier)stub.<arch>bot.<arch>.sebot.<arch>.sk.se = ENCRYPTED bot (~1 MB).sk = RC4 key (128-bit, rotates)stub decrypts.se with .skbot in the clear/tmp/.z → execve
Over telnet, the bot travels in the clear. Over exploit, it travels encrypted: the stub is a loader that reads the encrypted bot (.se) and its key (.sk), decrypts and runs it. Only the stub gets chmod +x.

09Persistence, and what it throws in on top

Fivefold persistence, more stubborn than usual: it hooks into boot through five places at once: crontab -, /etc/cron.d/zbot, /var/spool/cron/zbot, /etc/crontab and /etc/rc.local. And the cron doesn’t watch a process: every 5 minutes it checks [ -x /tmp/.z ] and, if it’s missing, restores it from /var/.z or /etc/.z. So it has somewhere to restore from, it copies itself (%s/.z) into six paths: /tmp, /var, /etc and on top of that /jffs, /rw, /data (writable partitions on routers, embedded firmware and Android), which confirms, without saying so, that the target is IoT.

And thrown in on top: a sweep of the system’s SUID binaries that launches the bot from each one, an attempt to load a kernel module (the .ko wasn’t embedded yet), a self-delete attempt (unlink of /proc/self/exe and its descriptors) that on Linux doesn’t even remove the file from disk, camouflage with [kworker/…] names, the opening of /dev/watchdog so the hardware watchdog doesn’t reboot the device (a reboot would take the bot with it, since it lives only in RAM), and a competitor-killer. That last one carries a lovely slip of lineage:

killer list: the genealogy peeking out through a slip
# Linux bots (expected):
  mirai.arm  tsunami  hakai  sora  qbot  astaroth  …
  # …and suddenly, WINDOWS antivirus in a Linux bot:
  msmpeng  avast  avgnt  kavsvc  clamd  msseces.exe
  # nothing here on Windows to kill → a sign of recycled code (Windows-born)

There isn’t a single Windows process to kill on a Linux router. Those names look like a list inherited from somewhere else and never cleaned up, probably from code meant for Windows, which is where those antivirus names make sense. Which family exactly I don’t know: it’s a good sign of recycled code, not proof of the ancestor.

10They named it in passing; here it is in full

Before wrapping up I ran the usual, boring check: has someone told this story already? Because a proxy-bot this complete and this active could well be documented, with me showing up late. I looked at the public sites (MalwareBazaar, VirusTotal, ThreatFox, URLhaus) and the answer has a nuance: the sample does circulate, but almost nobody knows what it is.

The tail-stripped ELF is catalogued, but as plain trojan.mirai, one more in the pile. The whole file, with its random tail, exists in no repository - which is exactly what the operator was after. The C2 they’ve filed as “Bashlite”. None of those labels says shem, or proxy, or describes the protocol. To automation it’s Mirai noise; the family name isn’t something a machine assigns. It isn’t uncatalogued because it’s stealthy: it’s miscatalogued because it breaks the very thing cataloguing relies on - the hash.

With one exception, and it’s worth saying: XLab, Qi’anxin’s lab, named it Shemzbot on 23 September, in a tweet with the correct C2 and a diagram of its .se/.sk/.z RC4 chain. A sharp glance, from people who know their trade. But a tweet isn’t a teardown: what you’re reading is what that message didn’t carry - from the anti-IOC tail in §01 to the reused keystream in §05 and the proxy switch in §06. And a wink that amused me: they merged into a single name the two I’d pulled out separately, my shem from the strings and the zbot from the crons.

What the public record confirms, without touching a thing Two of my findings from the cage can be checked from the outside, with third-party data. A device search engine sees, on the C2, the same SSH key I’d noted and port 7434 open (= 7332+2·51, §07). And a malicious-URL repository lists, on that same IP, the .se and .sk pieces of the encrypted delivery from §08. I didn’t need to go back to the bug: the network was already telling the story, it just didn’t know whose.

11A family of its own, with inheritance (and under construction)

Its own where it counts: an invented protocol (UDP/6881, magic SH, RC4), without Mirai’s XOR table, a four-transport architecture, and the shem markers everywhere. It’s not a Mirai in makeup.

Inherited in the details: the killer-list with Windows antivirus and the arsenal of public-knowledge exploits. And under construction: compared with a build from barely five days earlier, in that gap the author had added the relay and overlay transports, the SSH spreader, the SHEM_INST instance numbering, the ZCR1 report and the kernel-module attempt. It’s a project its owner improves week by week.

At bottom, shem isn’t “either proxy or DDoS”: it’s an access platform that does all four (on-demand SOCKS5 proxy, DDoS, backdoor and self-replicating worm). The design suggests the main money may be in reselling residential proxies; the DDoS, the backdoor and the propagation are the platform’s add-on capabilities.

The trend, beyond this one bug, is clear: the network’s edge is turning into proxy merchandise, and shem is a particularly complete specimen of that wave.

12Indicators (IOCs)

The stable ones, the kind that survive the operator switching servers tomorrow:

TypeValue
Familyshem (internal name) · external alias “Shemzbot” (XLab/Qi’anxin) · “zbot” = its cron files
Network signatureSSH-2.0-sheM · User-Agent: sheM · magic 53 48 (“SH”) over UDP/6881, RC4 body
Ports (node)6881/UDP · 7331/TCP (SOCKS5) · 7332/TCP (delivery) · formula base+2N (N=SHEM_INST)
Detectiona host listening on P and P+1 TCP + (P−450) UDP
Vectortelnet 23 / 2323, factory credentials (root/xc3511, admin/admin, admin/1234)
Markers/root/shem/{targets,web_targets,targets_adb}.txt · SHEM_INST · SHEM_LAB · SHEM_NOMASQ · SHEM_RAM_MB
CommandsPROXYON PROXYOFF UPDATE SHELL UDP AMP SCAN STOPSCAN TGTS VTGT YOURIP NOREPL NOTOR NOIDIRECT DIE · modes direct/relay/tor/overlay
On the host%s/.z copies in /tmp /var /etc /jffs /rw /data · 5-min cron that restores · SUID sweep · insmod /tmp/zbot.ko · self-delete attempt unlink /proc/self/exe (ineffective on Linux)
Stages via exploitstub.<arch> (loader+decryptor) · bot.<arch>.se (encrypted bot) · bot.<arch>.sk (RC4 128-bit key, rotates)
Observed .sk keys (RC4 128-bit)b2134c3e17e0a9b77119ebbf05f122b5 · 659ed8e9cee54f7127358ff92a96f6d2  (decrypt their own .se; rotate per build, now expired)
HashIOC = BuildID + sha256 of the ELF WITHOUT the padding tail  (the whole-file one is NO good)
SHA-256 · ELF, tail stripped (x86_64, the one that hit the decoy)82ab1202a5941b9d2947e99b2b03bc469e2a97f590c6a4efea819da20a2d8124  (catalogued on MalwareBazaar/VT)
BuildID (x86_64)8e7a8299899cf8dcb9b64116645a236a63ded94c  (stable pivot, survives the tail and the renaming)
SHA-256 · copy with padding1d3c4108464cd2370ee428c296f64552af89fdebf19051e4bb1666db9f46cf8b  (example of ONE download; doesn’t group: random padding per delivery)
ELF, tail stripped · multi-arch (delivery batch, 23 Sep)bot.x86_64 674107dcf1a876489123240add215cbb92649c2bbb321780057822232891ac10 (delivery build, different from the captured one)
bot.arm64 ef7466d1507430fc0e0f99c29d7184ee726e68934895bfdc37ad855c0c488b91
bot.armv7 74af9d637530df497444e47bfc12e4aba66a084546d413d1e0b54d7282c2a777
bot.mipsel fe12480efddd9603004a4165265e57192242a79144b124a936bfe95e66de2cb4 (= mips64el, same ELF)
stub.x86_64 37dd737a342729c7b8c4e6f28140f2ff977a433793482b6bd488efd56a20812e (catalogued on MB since 20 Sep)
C2 fingerprintSSH key ce:cd:4c:9e:86:a2:76:40:40:88:f9:50:85:a8:21:01 (ecdsa) · hassh 671c5858… · ports seen by Shodan 22 / 8080 / 7434 (7434 = 7332+2·51; partial record)
C2 infra176.65.134.121  (datacentre, AS51396 Pfcloud)
Deliveryvia victims (e.g. a HK residential line); do NOT list as the operator’s infrastructure
Actor signaturetemplate bug: hxxp://<IP>:<port>PROXYONUPDATE:<port>/bot.$m
Public documentationXLab (Qi’anxin) named it “Shemzbot” on 23 Sep 2026 · the tweet (@Xlab_qax)

To be continued - the decoy stays lit, and shem is still under construction. When the operator drops the next version, I’ll go back and look at what he added. 🍯

Comments