eBPF for Cybersecurity - Part 3

eBPF for Cybersecurity - Part 3

Β·

8 min read

previously we learned the basics of Ebpf and How to use the BPF library for the Rust programming language here will see eBPF program attached to sys_enter_exeve tracepoint in the Linux kernel and is executed for each sys_execve syscall.

Unsafe Rust

for eBPF programs written using rust, it is often required to access Linux kernel data structure or call Linux kernel helper functions. Unsafe Rust is the glue that helps achieve this functionality

following are the two essential functionalities enabled by unsafe Rust concerning eBPF programs

  • ability to dereference a raw memory pointer that points to Linux kernel data structure

  • ability to call a Linux kernel helper function implemented in C

Unsafe code in Rust is enclosed in a code block or a function block marked with an unsafe keyword the best way to understand how unsafe rust enables an eBPF program to interface with the Linux kernel is via the following two simple programs

Kernel Data Structure Access

every eBPF program gets calked with context passes as an input parameter. The context is the kernel data structure pointer, its type depends on the eBPF program type. the memory referenced by the context can access only with unsafe Rust code

unsafe fn ptr_at<T>(ctx: &Xdpcontext,offset: unsize)

-> Result<*const T ,()> {

    let start = ctx.data();
    let end = ctx.data_end();
    let len = mem::size_of::<T>();

  if start + offset + len > end {
    return Err(());
  }  

  Ok((start + offser) as *const T)

}

In the code snippet above, the ptr_at the function used by an eBPF program of the type BPF_PROG_TYPE_XDP the function takes ctx as an input parameter, a reference to an object type XdpContext this reference represents a C struct variable of type struct xdp_md in the Linux kernel. The xdp_md data structure points to a region in memory where a network packet resides. the start and end variables store the starting and ending memory address for the packet, while offset represent an offset within the packet. ptr_at the function returns a pointer to a valid offset within the packet's memory representation

fn try_xdp_firewall(ctx: XdpContext) ->
   Result<u32, () > {


    let eth_type = u16::from_be(unsafe {
    *ptr_at(&ctx, offset_of!(ethhdr,h_proto))?

    });

    if eth_type != ETH_P_IP {

        return Ok(XdpContext::XDP_PASS);
    }

    let source = u32::from_be(unsafe {
        *ptr_at(&ctx, ETH_HDR_LEN + offset_of!(iphdr,saddr))?
    });
   if blocked_ip(source){

    return Ok(xdp_action::XDP_DROP)
   }

   Ok(xdp_action::XDP_PASS)
   }

the try_xdp_firewall uses the ptr_at function to get the EtherType from the raw packet. if the packet encapsulated in the ethernet frame is an IP packet, the source IP is extracted using ptr-at function. if the source IP address is blocked the packet is dropped by return XDP_DROP action.

kernel Function call

unsafe rust code also enables the calling of certain Linux kernel helper function from the eBPF programs. The kernel helper functions that the eBPF program is permitted to call are contingent on the type of the program

use aya_bpf::{

    programs::ProbeContext,

    helpers::bpf_ktime_get_ns, 
};

fn try_nfs_file_read(ctx:ProbeContext)

  - > Result<u32,u32> {

    let start_mount_ns = 

    unsafe { bpf_ktime_get_ns()};
    ok(0)
  }

In the code snippet above, bpf_ktime_get_ns is a Linux kernel helper function that returns the number of nanoseconds since boot time and is used to compute the time delta between events or as a timestamp in this example the bpf_ktime_get_ns the function is called to record the start time of the NFS file read encapsulating the helper function call in an unsafe block.

let's install lima with ubuntu

limactl start ubuntu-lts       
INFO[0000] Creating an instance "ubuntu-lts" from template://default (Not from template://ubuntu-lts) 
WARN[0000] This form is deprecated. Use `limactl start --name=ubuntu-lts template://default` instead 
? Creating an instance "ubuntu-lts" Proceed with the current configuration
INFO[0017] Attempting to download the image from "https://cloud-images.ubuntu.com/releases/22.10/release-20221201/ubuntu-22.10-server-cloudimg-arm64.img"  digest="sha256:9575dfe9f925ec251a933b88a38c5582a18e9d19495025ac01cb2e217e5f14ca"
47.94 MiB / 664.62 MiB [-->___________________________________] 7.21% 3.52 MiB/s
limactl shell ubuntu-lts

connect Lima to your Vscode

limactl show-ssh --format=config ubuntu-lts
Host lima-ubuntu-lts
  IdentityFile "/Users/sangambiradar/.lima/_config/user"
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  NoHostAuthenticationForLocalhost yes
  GSSAPIAuthentication no
  PreferredAuthentications publickey
  Compression no
  BatchMode yes
  IdentitiesOnly yes
  Ciphers "^aes128-gcm@openssh.com,aes256-gcm@openssh.com"
  User sangambiradar
  ControlMaster auto
  ControlPath "/Users/sangambiradar/.lima/ubuntu-lts/ssh.sock"
  ControlPersist 5m
  Hostname 127.0.0.1
  Port 55305

we will see Trace Programs

  • Kernel Proble(kprobe)

  • kernel Tracepoint

  • User Space Probe ( UProbe)

  • User Space Tracepoint

Kernel Probe ( Kprobe)

Linux kernel probes enable you to attach eBPF programs to kernel functions to gather information or modify the function’s behaviour. This section will provide examples of both.

$ cat /proc/kallsyms

There are some 300+ different system calls in the Linux kernel. Applications/workloads running in the user space interact with the kernel using syscalls. e.g. if an application wants to do something like access a file, communicate using a network or even find the time of day, it will have to ask the kernel to do it on the application's behalf via a syscall; hence by attaching these probes to syscalls we're able to judiciously monitor what applications are doing in the user space. A list of all kernel syscall identifiers for a kernel can be found in this file: /proc/kallsyms. Here is an excerpt:

0000000000000000 T vs_create
0000000000000000 T vfs_mkdir
0000000000000000 T vfs_mknod
0000000000000000 T vs link
0000000000000000 T vs unlink
0000000000000000 t path_openat
0000000000000000 T vs_rename
0000000000000000 T getname kernel
0000000000000000 T putname
0000000000000000 t getname_flags.part. O
0000000000000000 T getname flags
0000000000000000 T getname

System.map file and /proc/kallsyms

System.map file format: address type notation

The type is lowercase for local symbols, and uppercase for global (external).
Focus on several types:
T, The symbol is in the text(code) section
D The symbol is in the initialized data section
R The symbol is in a read-only data section
t static
d static
R const
r static const

For example, suppose you have a Rust object file named hello.o that contains a function named hello. You can use nm to list the symbols in this object file by running the following command:

$ nm hello.o

This will print a list of symbols in the hello.o object file, which might look something like this:

0000000000000000 T hello
                 U __libc_start_main

Block Mount

start of the open_ctree function call. The eBPF helper function bpf_override_return short circuits the open_ctree function execution by returning ENOMEM. This prevents the btrfs volume from being mounted.

#![no_std]
#![no_main]

use aya_bpf::{
    helpers::bpf_override_return, macros::kprobe,
    programs::ProbeContext,
};
use aya_log_ebpf::info;

const ENOMEM: i32 = -12;
#[kprobe(name = "block_mount")]
pub fn block_mount(ctx: ProbeContext) -> u32 {
    match try_block_mount(ctx) {
        Ok(ret) => ret,
        Err(ret) => ret,
    }
}

fn try_block_mount(
    ctx: ProbeContext,
) -> Result<u32, u32> {
    info!(&ctx, "function open_ctree called");
    unsafe {
        bpf_override_return(ctx.regs, ENOMEM as u64)
    };
    Ok(0)
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe { core::hint::unreachable_unchecked() }
}

kernel Tracepoint

cargo generate https://github.com/aya-rs/aya-template
🀷   Project Name: tracepoint-sangam
πŸ”§   Destination: /home/ubuntu/tracepoint-sangam ...
πŸ”§   project-name: tracepoint-sangam ...
πŸ”§   Generating template ...
βœ” 🀷   Which type of eBPF program? Β· tracepoint
🀷   Which tracepoint category? (e.g sched, net etc...): sys_enter_execve
🀷   Which tracepoint name? (e.g sched_switch, net_dev_queue): systemcalls
[ 1/35]   Done: .cargo/config.toml                                                                                    [ 2/35]   Done: .cargo                                                                                                [ 3/35]   Done: .gitignore                                                                                            [ 4/35]   Done: .vim/coc-settings.json                                                                                [ 5/35]   Done: .vim                                                                                                  [ 6/35]   Done: .vscode/settings.json                                                                                 [ 7/35]   Done: .vscode                                                                                               [ 8/35]   Done: Cargo.toml                                                                                            [ 9/35]   Done: README.md                                                                                             [10/35]   Ignored: pre-script.rhai                                                                                    [11/35]   Done: xtask/Cargo.toml                                                                                      [12/35]   Done: xtask/src/build_ebpf.rs                                                                               [13/35]   Done: xtask/src/main.rs                                                                                     [14/35]   Done: xtask/src/run.rs                                                                                      [15/35]   Done: xtask/src                                                                                             [16/35]   Done: xtask                                                                                                 [17/35]   Done: tracepoint-sangam/Cargo.toml                                                                          [18/35]   Done: tracepoint-sangam/src/main.rs                                                                         [19/35]   Done: tracepoint-sangam/src                                                                                 [20/35]   Done: tracepoint-sangam                                                                                     [21/35]   Done: tracepoint-sangam-common/Cargo.toml                                                                   [22/35]   Done: tracepoint-sangam-common/src/lib.rs                                                                   [23/35]   Done: tracepoint-sangam-common/src                                                                          [24/35]   Done: tracepoint-sangam-common                                                                              [25/35]   Done: tracepoint-sangam-ebpf/.cargo/config.toml                                                             [26/35]   Done: tracepoint-sangam-ebpf/.cargo                                                                         [27/35]   Done: tracepoint-sangam-ebpf/.vim/coc-settings.json                                                         [28/35]   Done: tracepoint-sangam-ebpf/.vim                                                                           [29/35]   Done: tracepoint-sangam-ebpf/.vscode/settings.json                                                          [30/35]   Done: tracepoint-sangam-ebpf/.vscode                                                                        [31/35]   Done: tracepoint-sangam-ebpf/Cargo.toml                                                                     [32/35]   Done: tracepoint-sangam-ebpf/rust-toolchain.toml                                                            [33/35]   Done: tracepoint-sangam-ebpf/src/main.rs                                                                    [34/35]   Done: tracepoint-sangam-ebpf/src                                                                            [35/35]   Done: tracepoint-sangam-ebpf                                                                                πŸ”§   Moving generated files into: `/home/ubuntu/tracepoint-sangam`...
πŸ’‘   Initializing a fresh Git repository
✨   Done! New project created /home/ubuntu/tracepoint-sangam
ubuntu@ip-172-31-56-217:~$

its generate template

.
β”œβ”€β”€ Cargo.toml
β”œβ”€β”€ README.md
β”œβ”€β”€ tracepoint-sangam
β”‚   β”œβ”€β”€ Cargo.toml
β”‚   └── src
β”‚       └── main.rs
β”œβ”€β”€ tracepoint-sangam-common
β”‚   β”œβ”€β”€ Cargo.toml
β”‚   └── src
β”‚       └── lib.rs
β”œβ”€β”€ tracepoint-sangam-ebpf
β”‚   β”œβ”€β”€ Cargo.toml
β”‚   β”œβ”€β”€ rust-toolchain.toml
β”‚   └── src
β”‚       └── main.rs
└── xtask
    β”œβ”€β”€ Cargo.toml
    └── src
        β”œβ”€β”€ build_ebpf.rs
        β”œβ”€β”€ main.rs
        └── run.rs
  • tracepoint-sangam - contains the user space program

  • tracepoint-sangam-ebpf - contains the epbf program

  • trace-sangam-common contains the code for data structure and data types common to both user space and eBPF programs

The user space program can be buit using

~/tracepoint-sangam$ cargo build

the eBPF program can be built using

~/tracepoint-sangam$ cargo xtask build-ebpf

the release version of the eBPF program can be build using

~/tracepoint-sangam$ cargo xtask build-ebpf --release

The userspace program which loads the eBPF program into kernel can be run using

/tracepoint-sangam$ cargo xtask run

above simple ebpf program attached to sys_enter_exeve tracepoint in the Linux kenel and is executed for each sys_execve syscall.

The eBPF program on execution logs a perf event and this event is returned to the user space for printing via the perf event array map. exporting the logs from the kernel to the user space is abstracted from the developer and handled by the aya-logging library

That's it for this part we will learn more about aya and ebpf .

the eBPF developer experience by allowing Rust programs to easily run within the kernel.

Aya is the first Rust-native eBPF library that is similar in nature to libbpf but entirely written in the Rust programming language, popular for its memory safety and concurrency features, among other reasons this programming language is becoming very popular for systems programming.

Did you find this article valuable?

Support CloudNativeFolks Community by becoming a sponsor. Any amount is appreciated!

Β