Bomfather Has Been Funded by Balaji Srinivasan

Attacking and Securing eBPF Maps

BPF Maps aren't really that secure against users with elevated permissions

← Back to Blog

If you are an attacker (who does illegal stuff for a living) and you are reading this blog post… sorry for making your life just a bit harder ;)

Link to demo source code: https://github.com/bomfather/tools/tree/main/ebpf-map-attacker

Why Are We Even Doing Any of This?

eBPF maps are used to almost all eBPF programs because they are essentially the only way for data to be moved from the kernel space to the userspace, and the main way to store information between eBPF hooks.

If a bad actor can read or write data in these maps, it completely compromises the security posture of the entire eBPF tool.

Since the userspace and kernel space must interact with these eBPF maps, they are shared objects, and any elevated-privilege user (sudo, CAP_SYS_ADMIN, etc.) can access these maps.

The Easy Way (Not Really)

It’s pretty easy in theory to attack these maps, with a big caveat. You must have elevated permissions, at a minimum CAP_BPF. Once you have that, just hit the BPF map syscall, and you can edit any map you want. Simple.

Okay, not simple at all. After you get elevated permissions, yes, it’s simple, but getting them is hard.

However, we’ve been seeing a decent number of software vulnerabilities leading to sudo access for bad actors, like NvidiaScape and even sudo itself! So, we can’t assume these attacks can’t happen to our eBPF maps.

If your system contains enough valuable data and you have a kernel-level security solution, it should ideally be able to protect against elevated-privilege attackers.

How Do We Stop Sudo?

Stopping this root-level actor may seem impossible since sudo is… sudo. But eBPF can stop this, we can secure our own eBPF program with eBPF.

We can write an LSM hook(lsm/bpf_map) in our ebpf code to block access to the bpf map open syscall and only allow our userspace program to access it. With this, nobody can call these syscalls and attack us, not even sudo.

eBPFMapsSkepticalKid

We Still Aren’t Safe

Okay, we’ve stopped access to the bpf_map syscall using eBPF, so they shouldn’t be able to access our maps, right?

Well, bad actors can still attack our eBPF maps in a much more roundabout, limited, and sneaky way, one that’s a lot cooler than the first attack. They can attack a single, very useful type of eBPF map: BPF_F_MMAPABLE maps.

What are BPF_F_MMAPABLE maps

Most eBPF maps are stuck behind bpf syscalls, but not all. Sometimes eBPF syscalls take too long.

For example, if we are reading a lot of data off an eBPF map from the userspace, every single time we want to read data we hit a bpf syscall. Since the userspace cannot directly read kernel memory (our eBPF map), the bpf syscall copies that data from the kernel space to the userspace, which is expensive.

Instead, we can use a BPF_F_MMAPABLE map, which allows us to bypass all that. This map allows us to mmap with that memory region without bpf syscalls, removing the heavy copying operation. That means less overhead and less context switching. For large amounts of data, it’s worth it.

                                NORMAL EBPF MAP - RELATIVELY SLOW    

┌──────────────────────────────┐                                ┌──────────────────────────────┐
│          User Space          │                                │          Kernel Space        │
│                              │                                │                              │
│  ┌───────────────────────┐   │                                │  ┌───────────────────────┐   │
│  │  userspace program    │   │                                │  │     eBPF program      │   │
│  └───────────────────────┘   │                                │  └───────────────────────┘   │
│          │   bpf() syscall   │                                │             │                │
│          │                   │                                │             │ direct access  │
│          │                   │   copy_to/from_user()          │             ▼                │
│          └───────────────────────────────────────────────────▶│  ┌───────────────────────┐   │
│                              │                                │  │       eBPF map        │   │
│                              │                                │  │ (kernel value memory) │   │
│                              │                                │  └───────────────────────┘   │
└──────────────────────────────┘                                └──────────────────────────────┘
               ▲                                                                ▲
               │                                                                │
               │                                                                │
    costly copy (syscall + memcpy)                              cheap access (in-kernel pointers)

------------------------------------------------------------------------------------------------

                                EBPF MMAPABLE MAP - SUPER FAST    

┌──────────────────────────────┐                                ┌──────────────────────────────┐
│          User Space          │                                │          Kernel Space        │
│                              │                                │                              │
│  ┌───────────────────────┐   │                                │  ┌───────────────────────┐   │
│  │  userspace program    │   │                                │  │     eBPF program      │   │
│  └───────────────────────┘   │                                │  └───────────────────────┘   │
│          │   mmap(fd, …)     │                                │             │                │
│          │                   │  shared memory (same pages)    │             ▼                │
│          └───────────────────────────────────────────────────▶│  ┌───────────────────────┐   │
│          ▲                   │                                │  │   eBPF map (shared)   │   │
│          └────────────────────────────────────────────────────┤  │   (VMA-backed pages)  │   │
│                              │                                │  └───────────────────────┘   │
└──────────────────────────────┘                                └──────────────────────────────┘
                ▲                                                             ▲
                │                                                             │
                │                                                             │
  direct access via shared VMA                                 cheap access (same physical memory)

Now, did you catch it? We can access the map without a BPF syscall. That means these maps, while very useful for high-performance eBPF programs, also open a new attack surface.

Sneaky Stuff with ptrace

Now that we know we can read and modify BPF_F_MMAPABLE maps, the only thing we need is a file descriptor. Without it, we can’t mmap the bpf map.

But the only way to get this file descriptor is to hit the bpf syscall, and if access to that syscall is blocked, how can a bad actor get a file descriptor? Instead of calling a bpf syscall to get the file descriptor, they can steal it.

A malicious actor can ptrace our userspace process (the eBPF agent) and steal the file descriptor from our benign program. Once they have it, they can dump the map’s contents and do whatever they want with it.

These BPF_F_MMAPABLE maps are usually used for logging, but if attackers can deactivate logging, you essentially become blind to their intrusion. If you don’t know about it, you can’t stop them.

When It Doesn’t Work the First Time, Get a Bigger Hammer

We already blocked off the BPF syscall, but that won’t stop this attack, and since we’re an eBPF program, we can just block all ptrace attempts on us. Now, nobody can directly access our maps or steal the file descriptors.

Okay, Cool… But I’m Lazy. Now What?

At a minimum, you should secure the BPF syscall. It’s essentially three lines of code, something like this, and without it, your code becomes a (very fat) sitting duck:

SEC("lsm/bpf_map")
int BPF_PROG(lsm_bpf_map, struct bpf_map *map, fmode_t fmode) {
	u32 pid = bp f_get_current_pid_tgid() >> 32;
	if (pid == mypid) {
		return 0;
	}
	return -EPERM;
}

You don’t have to stop ptrace if you don’t use BPF_F_MMAPABLE maps (though ChatGPTing a simple eBPF hook can’t be too hard). You don’t really need BPF_F_MMAPABLE maps if you aren’t logging tons of data from kernel to user space.

We may have secured these maps, but an attacker can still just kill our eBPF agent’s userspace process, which would automatically stop all our security. If your interested, our next post will be about how we can secure our agent from death itself?