Evasion Techniques — Hiding your process from `ps`


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
.
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
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.
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.
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()
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 theopenat()
system call.*buf is our target read buffer, This is passed to
read()
as a pointer to a location in memorycount is the number of bytes to be read from fd
So lets define our hooking function.
|
|
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.
|
|
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.
|
|
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! 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.