Concept familiarization

Tracing a system call

In the previous exercises, we hooked into the scheduler (sched_process_exec). This event fires after the kernel has loaded a binary and populated its internal structures.

But what happens before that? When a program like bash wants to run ls, it makes a request to the kernel through the execve system call.

System calls are different from the specific tracepoints we’ve used so far. They use generic entry and exit events that work for all syscalls.

The generic syscall structure

Because there are hundreds of system calls (open, write, execve…), each with its own set of arguments, the kernel uses a generic structure for tracepoints.

On entry events, we receive a context with type struct trace_event_raw_sys_enter:

struct trace_event_raw_sys_enter {
    long int id;               /* Syscall number */
    long unsigned int args[6]; /* Arguments */
    /* ... */
};

We don’t get convenient fields like name or pid — all we are getting is the syscall number (id) and an array of integers, args[6], which are the arguments to the system call.

Why 6? Because that’s the maximum number of arguments that syscalls are allowed to take.

We also don’t get any types! All arguments are long unsigned int (u64), and it’s up to us to interpret them properly (as pointers, structs, signed numbers, or even u64!).

To know the type and meaning of each argument, you can consult the manual page, which shows the signature:

int execve(const char *filename, char *const argv[], char *const envp[]);

Since filename is the first argument, it’s located at ctx->args[0]. The args array stores it as an unsigned long, so we’ll need to cast it to const char *.

Something like this:

args[0]char* filenameargs[1]char* argv[]args[2]char* envp[]"secret_app\0"argv[]envp[]

But there’s one more detail to handle.

On address spaces

ctx->args[0] points to memory owned by the calling process (e.g., bash), which is user space memory.

The kernel enforces strict separation between user and kernel memory for security. This means we cannot use the kernel-space helpers we’ve used before (like bpf_probe_read_kernel_str) to read this data. Instead, we must use bpf_probe_read_user_str(dst, size, src).

Your task

  1. Construct the filename pointer from ctx->args[0].
  2. Read the string from User Space into a local buffer.

If you use DEBUG_STR on the buffer, you should see multiple programs being executed.

Remember that you can only submit one answer with SUBMIT_STR_LEN, so you will need to filter them like we did in the previous exercises.

You can get len from the return value of bpf_probe_read_user_str — it returns the number of bytes copied.

Quick reference
Compare two strings for equality
Function bpf_strncmp Full documentation
Args:
char* bufdynamic buffer to compare
u32 buf_szlength of dynamic buffer
const char* buf2literal string to compare against
Returns 0 if strings match, non-zero if they differ
Run your code to see execution events here