h3xduck@blog:~#

Offensive eBPF - Part 1: Classic BPF

by h3xduck

25 July 2025

Three years ago, on 2022, I had to write my bachelor thesis for my CS degree. Back then, I had already played around with the idea of rootkits, which eventually led me to releasing Umbra, my first open source piece of malware (just for fun ;)). When my then supervisor Juan Tapiador told me about this new thing called eBPF, it was clear to me that it was the time to do another rootkit. And so, TripleCross was born, an open source eBPF-based rootkit.

I am incredibly happy of having done this project. In retrospective, it took many sleepless nights fighting with the eBPF verifier (oh we will ramble about this), but it was really fun and it led to fantastic things later on, specially my talk at RootedCON, the largest cybersecurity conference in Spain, where I first discovered how much do I love going to conferences and giving talks (even though outside of that I’m a little awkward and shy!). I will always be greateful to Alfonso Muñoz for the invitation.

One day, my friend Ismael (btw, he later created a super cool rootkit on eBPF himself!!) asked me why didn’t I write the thesis as a blog post, with my comments, and without all the formalities and unnecessary glitter that a thesis requires. It’s been a year from that, but today I found the energy to start doing so. And after that, I may write about my newer research projects, with which I have since sifted from malware to the exploitation world (but malware is still my true passion).

And before we start, you have to know I was not remotely the first person to explore how eBPF can be used offensively. My work was based on the work of giants like Jeff Dileo in DEFCON and Guillaume Fournier and Sylvain Afchain at BlackHat. The true contribution of my work is not the discovery that eBPF could be used for bad things, but rather that I put together for the first time all the documentation and interesting offensive capabilities of eBPF, and then spinned it up to create my own open source work with some slightly new things.

Let’s start - What is eBPF? Classic BPF

Yes yes, I know, I said I wrote a rootkit with eBPF, but I didn’t even say what it is. Nowadays eBPF is not officially considered to be an acronym anymore, but it remains largely known as “extended Berkeley Packet Filters”, given its roots in the Berkeley Packet Filter (BPF) technology, now known as classic BPF. Let’s start from here, and we will find our way to how eBPF is nowadays.

BPF was introduced in 1992 by Steven McCanne and Van Jacobson in the paper “The BSD Packet Filter: A New Architecture for User-level Packet Capture”, as a new filtering technology for network packets in the BSD platform. It was first integrated in the Linux kernel on version 2.1.75.

At the time, BPF was much more basic. After receiving a packet via the Network Interface Controller (NIC) driver, it would first be analysed by BPF filters, which are programs directly developed by the user. This filter decides whether the packet is to be accepted by analysing the packet properties, such as its length or the type and values of its headers. If a packet is accepted, the filter proceeds to decide how many bytes of the original buffer are passed to the application at the user space. Otherwise, the packet is redirected to the original network stack, where it is managed as usual.

One clarification: classic BPF is still in use, as we will see. It’s just that eBPF has gone much further than that.

The (classic) BPF virtual machine

BPF comprises both the BPF filter programs developed by the user and the BPF module included in the kernel which allows for loading and running the BPF filters. This BPF module in the kernel works as a virtual machine, meaning that it parses and interprets the filter program by providing simulated components needed for its execution, turning into a software-based CPU. Because of this reason, it is usually referred as the BPF Virtual Machine (BPF VM). The BPF VM comprises the following components:

Inside this BPF VM is where we execute the classic BPF filter programs. You can think of a BPF filter as a boolean function:

See above an example of a BPF filter. Green lines indicate that the condition is true and red lines that it is evaluated as false. Here the execution works as a control flow graph (CFG) which ends on a boolean value. In our example, the BPF program which accepts the following frames:

Classic BPF bytecode instruction format

In order to implement the CFG to be run at the BPF VM, BPF filter programs are made up of BPF bytecode, which is defined by a new BPF instruction set. Therefore, a BPF filter program is an array of BPF bytecode instructions.

  OPCODE JT JF K
NUMBER OF BITS 16 8 8 32

The table shows the format of a BPF bytecode instruction. As it can be observed, it is a fixed-length 64-bit instruction composed of:

And each BPF instruction is defined according to the BPF instruction set, as shown in here:

We have every type of instruction we can expect:

Tcpdump uses classic eBPF

At the time, by filtering packets before they are handled by the kernel instead of using a user-level application, BPF offered a performance improvement between 10 and 150 times the state-of-the-art tools of the moment. Since then, multiple popular tools began to use BPF, such as the network tracing tool tcpdump, and still does so until our days.

tcpdump is a command-line tool that enables to capture and analyse the network traffic going through the system. It works by setting filters on a network interface, so that it shows the packets that are accepted by the filter.

Here is an example of BPF code used by tcpdump to implement a simple filter.

We can see how tcpdump sets a filter to display traffic directed to all interfaces (-i any) directed to port 80. Flag -d instructs tcpdump to display BPF bytecode. Using the jf and jt fields, we can label the nodes of the CFG described by the BPF filter. The following image describes the shortest graph path that a true comparison will need to follow to be accepted by the filter. Note how instruction 010 is checking the value 80, the one our filter is looking for in the port:

Wrapping up

That’s all to know about classic BPF! Next, we will start going deep into modern eBPF. See you on the next entry of the eBPF series:

Part 1: Classic BPF

Part 2: Modern eBPF

Part 3 (Yet to come!)

tags:

Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License.