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.
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.