linux-kernelebpf

ebpf helper func "bpf_probe_write_user" return error (-14)


I was attempting Experiment 2 specified on the site, which involves modifying the first parameter (the file path of the executed program) in the sys_enter_execvfunction. However, when I called bpf_probe_write_user, it failed with a return value of ​​-14​​.

env: Linux cjh 5.15.0-152-generic #162-Ubuntu SMP Wed Jul 23 09:48:42 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux libbpf is compiled from the kernel source

The ebpf code (exechijack.bpf.c):

// SPDX-License-Identifier: BSD-3-Clause
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "exechijack.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// Ringbuffer Map to pass messages from kernel to user
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} rb SEC(".maps");

struct TASK_COMM {
   char comm[8];
};

// Optional Target Parent PID
const volatile int target_ppid = 340126;

SEC("tp/syscalls/sys_enter_execve")
int handle_execve_enter(struct trace_event_raw_sys_enter *ctx)
{
    size_t pid_tgid = bpf_get_current_pid_tgid();
    // Check if we're a process of interest
    if (target_ppid != 0) {
        struct task_struct *task = (struct task_struct *)bpf_get_current_task();
        int ppid = BPF_CORE_READ(task, real_parent, tgid);
        if (ppid != target_ppid) {
            return 0;
        }
    }

    char prog_name[TASK_COMM_LEN];
    // Read in program from first arg of execve
    char prog_name_orig[TASK_COMM_LEN];
    __builtin_memset(prog_name, '\x00', TASK_COMM_LEN);
    bpf_probe_read_user(&prog_name, TASK_COMM_LEN, (void*)ctx->args[0]);

    // Program can't be less than out two-char name
    if (prog_name[1] == '\x00') {
        bpf_printk("[EXECVE_HIJACK] program name too small\n");
        return 0;
    }

    prog_name[0] = '/';
    prog_name[1] = 'a';
    for (int i = 2; i < TASK_COMM_LEN ; i++) {
        prog_name[i] = '\x00';
    }

    long ret = bpf_probe_write_user((void*)ctx->args[0], prog_name, 3);
    bpf_printk("[EXECVE_HIJACK] ret: %d, %s\n", ret, (void*)ctx->args[0]);

    return 0;
}

prog to load bpf prog (exechijack.c):

#include <stdio.h>
// #include <bpf/bpf.h>
#include "exechijack.skel.h"


int main() {

    //int zero = 0;

    struct exechijack_bpf *_obj = exechijack_bpf__open_and_load();
    exechijack_bpf__attach(_obj);
    printf("attach ...\n");

    while (1);

    return 0;
}

user prog to be hijacked (victim2.c):

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

// const char *proga = "./proga";

int main()
{
        int pid = getpid();
        printf("current pid: %d\n", pid);
        while (1) {
                pid = fork();
                if (pid == 0) {
                        char *args[] = {"a", NULL};
                        printf("./proga");

                        execv("./proga", args);
                        usleep(1000 * 3000);
                }
                else {
                        // waitpid
                        wait(NULL);
                }
                usleep(1000 * 3000);
        }
        return 0;
}

build script:

clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I/usr/local/include/ -c exechijack.bpf.c -o exechijack.bpf.o
bpftool gen skeleton exechijack.bpf.o > exechijack.skel.h
gcc -g -Wall -I/usr/local/include/ exechijack.c -lelf -lz -lbpf -o exechijack
gcc -O0 victim2.c -o victim2

the output of /sys/kernel/tracing/trace_pipe:

           <...>-339834  [002] ....1 87540.993130: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

           <...>-340159  [002] ....1 88515.102412: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

           <...>-340160  [002] ....1 88518.103038: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

           <...>-340161  [000] ....1 88521.103815: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

         victim2-340162  [003] ....1 88524.104488: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

         victim2-340163  [003] ....1 88527.105239: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

         victim2-340164  [003] ....1 88530.105970: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

         victim2-340165  [003] ....1 88533.106663: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

         victim2-340166  [003] ....1 88536.107434: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

         victim2-340167  [003] ....1 88539.108153: bpf_trace_printk: [EXECVE_HIJACK] ret: -14, ./proga

After reviewing the kernel source code, I confirmed that the failure wasn’t caused by permission issues, such as !capable(CAP_SYS_ADMIN). In another test BPF program, I verified that bpf_probe_write_userworks correctly—but I don’t understand why it fails here.


Solution

  • because first argument at execv in .rodata section of victim2 elf file. it can not be modify.

    memory area of "./proga" at .rodata

    execv("./proga", args);
    

    .rodata section without W(write) permission

    # readelf -S victim2
    [16] .rodata           PROGBITS         0000000000402000  00002000
           000000000000001f  0000000000000000   A       0     0     4
    

    you should write like this:

    char comm[] = "./proga";
    execv(comm, args);
    

    then, comm[] will in stack of victim2, it can be modify.