Detecting memfd_create linux fileless malware with EBPF

Siddharth
5 min readFeb 18, 2022

--

Detecting Fileless malwares on the systems has always been tricky. In the past we have seen some linux malwares which used fileless techniques. For example, NiuB malware creates a hidden directory on the fly inside user’s bin location and from that hidden directory, it executes the payload.

In this blog we would be covering the well known memfd_create() syscall technique using which Pupy RAT creates space from physical memory to inject its malicious code.

The memfd_create() creates an anonymous file and returns a file descriptor that refers to it. However, unlike a regular file, the file lives in RAM and has volatile backing storage. The file behaves like a regular file, and so can be modified, truncated, memory-mapped. Man page for memfd_create() here.

The Pupy RAT(linux version) is written in C. The pupy rat:

  1. First daemonizes itself with a different name (‘/usr/sbin/atd’ in this case which is non-existent on filesystem/disk).
  2. The via /usr/sbin/atd, It heavily uses memfd_create function to create anonymous pages in its own process address space in order to inject the malicious code inside. If we take a look inside the address space layout(cat /proc/11155/maps) of the pupy RAT, we can see its usage of memfd_create() call.
maps layout of the pupy RAT

For writing the malicious code pupy rat uses write function call along with memfd_create. Disassembly for the same shown below.

sys_memfd_create()
write() after memfd_create syscall

fileless execution bypasses *snoop!!

The *snoop here refers to opensnoop and execsnoop. Opensnoop and Execsnoop are a part of the Ebpf toolset. The opensnoop monitors the open() syscall and execsnoop monitors exec() syscall. If we run puppy RAT alongside open and execsnoop, we can see the ‘/usr/sbin/atd’ is nowhere getting logged.

opensnoop logs (no atd inside)
execsnoop logs (no atd in the logs)

Detecting Pupy’s /usr/sbin/atd with EBpF

Using EBpf, we will try to detect the atd file which is created on the fly by the pupyRAT. The EBPf gives us capability to monitor(or trace) any syscall subsystem in linux. With EBPf, one can monitor system performance, memory usage etc.. In the previous blog, we discussed how using EBpF we could detect ftrace syscall hook activity. In this blog our focus is on memfd_create() syscall, monitoring which our detection will be headed.

We can write our monitor tool in both BPF-CORE and the BCC. For simplicity, we will get started with BCC. To monitor memfd, we would be using tracepoint, we will start by putting kernel tracepoint on memfd syscall.

Now, we navigate to /sys/kernel/debug/tracing/events/syscalls/sys_enter_memfd_create/format. (This format file exists for almost every function call in linux and this format file is produced by TRACE_EVENT() macro).

Inside the format file (shown below), we can see that memfd_create takes uname as a parameter. The uname is the name(link) of created file by memfd_create().

format of memfd_create

Along with uname we would be requiring the PID of the program which calls memfd_create. The pid can be fetched with Ebpf helper programs which we will see later in this blog.

We will now prepare a structure containing the pid and uname.

We can add more fields inside our event struct like id, uid, gid, flags etc.depending on the syscall. For this case let’s continue with uname and the pid.

EBPf provides certain helper functions using which data can be transferred from user to kernel and vice versa. This is done via mapping and the table creation inside maps.

For our case, we would use BPF_RINGBUF_OUTPUT which creates a BPF table for pushing out custom event data to user space via a ringbuf ring buffer.

In our case we create a ringbuf called a buffer with 16 pages of space. Going forward, we assign event pointer to our event structure which we created above.

In the above image, points(in red) are explained as follows:

  1. The bpf_probe_read_user_str() copies a NULL terminated string from user address space to the BPF stack, so that BPF can later operate on it.
  2. Using the bpf_get_current_pid_tgid() helper function, we store the pid of the calling program i.e. the program which calls memfd_create.
  3. Assigning higher part i.e. PID to event’s pid field.
  4. At last, we push the event data to user space using ringbuf_output.

With this the kernel mode component of our bpf program is ready.

Now let’s write the user mode code component.

We first create an object to our kernel mode BPF program.

Then, we open our ring buffer named buffer and give it the callback function as the parameter. After this, once our callback function gets called, we print the uname, pid and pid’s process name. Along with these we also print file descriptors of the pid in our log.

Below image shows the functions we defined for printing file descriptors and PID’s process name.

With this our monitor code is ready.

Now let’s see if we can catch Pupy RAT’s /usr/sbin/atd with our detection code.

If we take a look in the above image, we can see that our tool logs the /usr/sbin/atd successfully with its pid and name.

NOTE: The puppy rat gives libc.so.6 as the uname parameter to memfd_create() function. If not supplied, the name contains random characters.

References

--

--