2

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.

1
  • @sawdust Sorry, I made a mistake in my previous writing. The error occurred with bpf_probe_write_user instead of bpf_probe_read_user. I have revised the problem description. Commented Aug 25 at 7:38

1 Answer 1

1

Because the first argument of execv is located in the .rodata section of victim2 elf file, it cannot be modified.

Memory area of "./proga" at .rodata

execv("./proga", args);

.rodata section doesn't have 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 be at the stack of victim2, so it can be modified.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.