eBPF: Block Linux Fileless Payload "Malware" Execution with BPF LSM

Due to the security features that Linux offers, like booting directly into a readonly filesystem, making filesystems readonly at runtime for apps and containers, some attacks have been using what is known as "fileless binary execution" to avoid such protections, and gain the ability to execute binaries directly from the memory without touching or leaving traces on the disk.

From Wikipedia "Fileless malware is a variant of computer related malicious software that exists exclusively as a computer memory-based artifact i.e. in RAM."

In this post we focus on memfd_create() and shared memory anonymous files.

memfd_create()

memfd_create() was developed when we were hacking on the defunct kdbus IPC, you can read more in the original post memfd_create() by David Herrmann.

memfd_create() creates an anonymous file and returns a file descriptor that refers to it. The file behaves like a regular file, and so can be modified, truncated, memory-mapped, and so on. However, unlike a regular file, it lives in RAM and has a volatile backing storage. Once all references to the file are dropped, it is automatically released. Anonymous memory is used for all backing pages of the file. Therefore, files created by memfd_create() have the same semantics as other anonymous memory allocations such as those allocated using mmap(2) with the MAP_ANONYMOUS flag.

This feature is being abused by attackers and malware to write binary payload to that memory-resident file and then execute into it, like with any other binary file located on the filesystem.

As an example, let's take a look at the memory loader sample that uses memfd_create() published in the Sysmon for Linux post by Pat H. loader code is here github repo.

 1func main() {
 2  // Print PID of loader program
 3  fmt.Printf("  Ldr PID %d\n", os.Getpid())
 4
 5  // Use memfd to create in-memory file
 6  fd, _ := unix.MemfdCreate("", 0)
 7
 8  // open created in-memory file
 9  fp := fmt.Sprintf("/proc/self/fd/%d", fd)
10  memfd_file := os.NewFile(uintptr(fd), fp)
11  defer memfd_file.Close()
12
13  // Read ELF from disk and write into in-memory file
14  // ELF could also be read from a C2 server, encrypted
15  // on disk, hardcoded, piped in from stdin, etc.
16  data, _ := ioutil.ReadFile("/path/to/basic")
17  memfd_file.Write(data)
18
19  // Execute in-memory binary, fake argv[0] filename
20  argv := []string{"from_loader", "AAAA"}
21  unix.Exec(fp, argv, os.Environ())
22}

The loader will create an anonymous file, copy the binary basic or any other passed binary into it, and then execute the referenced file. This is usually the same technique used by malware to perform fileless binary execution: get code execution, receive payload from internet and execute into it, all without touching the filesystem.

We all use ps to see what processes are running, and if you do then it will return:

1USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
2tixxdz     53324  1.0  0.0  17000   944 pts/2    S+   08:42   0:00 from_loader AAAA

Where from_loader is the name or comm of the program after it was modified, and there is no indication if this is from an anonymous file or not.

Detection of Fileless Binary Execution

Classic tools like ps that read /proc/$pid/comm are restricted and can be easily tricked, as changing the process name to arbitrary values is a standard feature of Linux with the prctl().

Some detection solutions go even further: read the /proc filesystem and get the binary that is pointed by /proc/$pid/exe. However, this detection can also be bypassed by calling prctl() with PR_SET_MM_EXE_FILE in order to link to another binary on the filesystem.

Actually, a lot of process properties including kernel memory maps can be changed by calling prctl(PR_SET_MM,...) in a restrictive way of course.

We may also read /proc/$pid/maps to show mapped memory, but as usual it is racey: the process may just execv() into another one, and at the time of reading, maps will show some other data, too late :warning:

The most robust way is to use Linux Security Modules, tracing or eBPF to trace the execution. A good candidate is when the corresponding binfmt loader matches and the process execution hits the point of no return. At this time, we have gathered enough information, we can access the elf header, and we are sure that such information can't be forged, since handing execution to the corresponding process still did not happen. The mm_struct is still attached to the Linux_binprm struct, and the current task still has a copy of the parent's mm_struct. This allows to query the appropriate information, log, or even take actions before bad things happen.

For further reference, please see filelesslock.bpf.c program from the bpflock project, where it checks if the executed file is linked or not on the filesystem, if not then it logs and blocks the execution.

Other tools like SysmonForLinux, Tracee, Falco are capable to detect if the file being executed is prefixed with "memfd:" which indicates a memfd file execution.

Blocking Fileless Binary Execution

Now let's how to use bpflock to block executing into memfd files in their basic form.

1docker run --name bpflock -it --rm --cgroupns=host --pid=host --privileged \
2    -e "BPFLOCK_EXEC_SNOOP=all" -e "BPFLOCK_FILELESSLOCK_PROFILE=restricted" \
3    -v /sys/kernel/:/sys/kernel/ -v /sys/fs/bpf:/sys/fs/bpf linuxlock/bpflock

Then run the loader program that loads payload into a memfd file:

1$ ./loader /bin/sleep 10
2Failed to execute: operation not permitted

If we check the logs of the bpflock container, we see:

1time="2022-02-07T06:19:43Z" level=info msg="event=syscall_execve tgid=52906 pid=52906 ppid=6594 uid=1000 cgroupid=7014 comm=loader pcomm=bash filename=./loader retval=0" bpfprog=execsnoop subsys=bpf
2
3time="2022-02-07T06:19:43Z" level=info msg="event=lsm_bprm_creds_from_file tgid=52906 pid=52906 ppid=6594 uid=1000 cgroupid=7014 comm=loader pcomm=bash filename=memfd:memfd-test retval=-1 reason=denied (restricted)" bpfprog=filelesslock subsys=bpf
4
5time="2022-02-07T06:19:43Z" level=info msg="event=syscall_execve tgid=52906 pid=52906 ppid=0 uid=1000 cgroupid=7014 comm= pcomm= filename=/proc/self/fd/3 retval=-1" bpfprog=execsnoop subsys=bpf

The execution failed at log entry 2, the event is an LSM hook bprm_creds_from_file, the filename points to a memfd: backed file, the return value is -1 which is -EPERM and the reason is denied (restricted) due to the profile restricted that is being used when starting bpflock.

A note here: the exec() arguments and the interpreter are not currently handled by bpflock.

Now, let's try to block injector, a program that uses a technique published back in 1998 by Silvio Cesare in a paper "Unix ELF parasites and viruses" about patching ELF binaries to inject code at the program’s entrypoint.

We run the injector:

1./injector ./shellcode.bin /bin/echo aaaa

The logs of bpflock produce:

1time="2022-02-07T06:34:50Z" level=info msg="event=syscall_execve tgid=53151 pid=53151 ppid=6594 uid=1000 cgroupid=7014 comm=injector pcomm=bash filename=./injector retval=0" bpfprog=execsnoop subsys=bpf
2
3time="2022-02-07T06:34:50Z" level=info msg="event=lsm_bprm_creds_from_file tgid=53151 pid=53151 ppid=6594 uid=1000 cgroupid=7014 comm=injector pcomm=bash filename=memfd: retval=-1 reason=denied (restricted)" bpfprog=filelesslock subsys=bpf
4
5time="2022-02-07T06:34:50Z" level=info msg="event=syscall_execve tgid=53151 pid=53151 ppid=0 uid=1000 cgroupid=7014 comm= pcomm= filename=/proc/self/fd/3 retval=-1" bpfprog=execsnoop subsys=bpf

We see in the second line that again, bpflock detected and blocked it.

Conclusion

As demonstrated bpflock tool which is still experimental allows to detect and block memfd backed files execution, this is achieved by taking advantage of BPF LSM programs.

Thanks to eBPF which is a great technology, we are able to develop new network and security usecases. Also thanks to the ebpf kernel maintainers that are doing an amazing work by listening and incorporating developers resquests.

There are other more complex modules in bpflock, like:

  • kimglock to protect kernel image direct and indirect modifications.
  • bpfrestrict to restrict access to the bpf() system call.
  • kmodlock to restrict loading kernel modules.

We will give further details in the next posts.

Reference:

Commandline Cloaking and Sysmon for Linux

FilelessMalware picture from: https://ophtek.com/fileless-malware-the-rise-of-a-new-threat/