Kernel probes

Introduction

We’ve used tracepoints to hook into syscalls. Now let’s learn kprobes - attaching to internal kernel functions.

Kprobes vs tracepoints

Tracepoints are stable, documented hooks at specific kernel events (syscall entry/exit, scheduling, etc.). They provide structured context:

SEC("tracepoint/syscalls/sys_enter_connect")
int trace(struct trace_event_raw_sys_enter *ctx) {
    u32 fd = ctx->args[0];  // Predefined fields
}

Kprobes attach to any kernel function by name.

This mechanism is more flexible but kprobes are not part of the kernel’s stable API, so they can change between kernel versions. On top of that, they do not provide a defined ‘context’, we need to get the argument types and order from the kernel sources.

Why kprobes?

Tracepoints expose specific events. Kprobes let us hook deeper into kernel internals. For example, connect() syscall returns immediately for non-blocking sockets, but tcp_finish_connect() is called when the TCP handshake actually completes.

Accessing function arguments

Kprobes receive struct pt_regs *ctx - raw CPU registers. Use macros to extract the arguments:

PT_REGS_PARM1(ctx)  // First parameter
PT_REGS_PARM2(ctx)  // Second parameter
PT_REGS_PARM3(ctx)  // Third parameter
// up to 8

We can find the signature for tcp_finish_connect in net/ipv4/tcp_input.c, which is

void tcp_finish_connect(struct sock *sk, struct sk_buff *skb);

And so, to get access to the struct sock* we need to cast the first argument:

struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);

The challenge

A program connects to a remote server. The connect() syscall returns -EINPROGRESS (non-blocking), but the connection completes successfully.

Socket properties

You can read the socket’s properties with bpf_probe_read_kernel. Here are the relevant fields:

struct sock {
    struct sock_common __sk_common;  // Common socket info
    // ...
};

struct sock_common {
    u16 skc_dport;   // Destination port
    u32 skc_daddr;   // Destination IPv4 address
    // ...
};

Remember that ports are in big endian, so you’ll need to use bpf_ntohs to convert it.

Your task

Submit the destination port.

Quick reference
Read bytes from kernel space into kernel buffer
Function bpf_probe_read_kernel Full documentation
Args:
void* dstkernel buffer to read into
u32 sizebytes to read
const void* srckernel space pointer
On success, returns 0
On error, returns negative error code
Read string from kernel space into kernel buffer
Function bpf_probe_read_kernel_str Full documentation
Args:
void* dstkernel buffer to read into
u32 sizemaximum bytes to read
const void* srckernel space pointer to string
On success, returns number of bytes read (including null terminator)
On error, returns negative error code
Run your code to see execution events here