Summary
Recently I came across a project: libprocesshider, which can be used to hide a Linux process, and for curious, I tried it out and found that it involves a few interesting points, so I’ll document it.
Theory
We usually view the process through the ps
command, or when the machine indicator of anomalies, we usually use the top
command to view the system’s processes. So if we make these two programs “can not see” we want to hide the process, don’t we complete the process of hiding the function?
To make these two commands can not see our “hidden processes”, we have to go back to the principle of these two processes, their source codes are:
- ps:https://gitlab.com/procps-ng/procps/-/tree/master/src/ps
- top:https://gitlab.com/procps-ng/procps/-/tree/master/src/top
how ps work
[root@liqiang.io]# cat library/readproc.c
//////////////////////////////////////////////////////////////////////////////////
// This finds processes in /proc in the traditional way.
// Return non-zero on success.
static int simple_nextpid(PROCTAB *restrict const PT, proc_t *restrict const p) {
static __thread struct dirent *ent; /* dirent handle */
char *restrict const path = PT->path;
for (;;) {
ent = readdir(PT->procfs);
if (!ent || !ent->d_name[0]) break;
if (*ent->d_name > '0' && *ent->d_name <= '9') {
errno = 0;
p->tgid = strtoul(ent->d_name, NULL, 10);
if (errno == 0) {
p->tid = p->tgid;
snprintf(path, PROCPATHLEN, "/proc/%d", p->tgid);
return 1;
}
}
}
return 0;
}
What we can find is that the ps
program will list the /proc/
directory to view the process list, and for each process directory:
next_proc:
new_p = NULL;
for (;;) {
// fills in the PT->path, plus skel_p.tid and skel_p.tgid
if (!PT->finder(PT,&skel_p)) goto end_procs; // simple_nextpid
if (!task_dir_missing) break;
if ((ret = PT->reader(PT,x))) return ret; // simple_readproc
}
It will read the /proc/<pid>/stat,io,smaps_rollup,statm,status,environ,cmdline,cgroup,ns
directories,the source code can be found at:library/readproc.c
with function:static proc_t *simple_readproc(PROCTAB *restrict const PT, proc_t *restrict const p) {
.
Here we can find a important system call:readdir
was used to list the /proc
directory.
how top work
[root@liqiang.io]# cat library/pids.c
/* procps_pids_reap():
*
* Harvest all the available tasks/threads and provide the result
* stacks along with a summary of the information gathered.
*
* Returns: pointer to a pids_fetch struct on success, NULL on error.
*/
PROCPS_EXPORT struct pids_fetch *procps_pids_reap (
struct pids_info *info,
enum pids_fetch_type which)
{
... ...
if (!pids_oldproc_open(&info->fetch_PT, info->oldflags))
return NULL;
info->read_something = which ? readeither : readproc;
The which ? readeither : readproc
means that if we specify a process, it will use function readproc
,otherwise it will use readeither
to read all the process, so it’s the same as what top
does essentially.
hidden principal
As we can see from the above, both ps
and top
display process information by reading the /proc/
directory, so wouldn’t we be able to make it invisible to the user if we tinkered with it a bit?
There are many ways to do this, one simple way is to replace the implementation of readdir
, so that when readdir
reads the /proc/
directory, it will ignores the processes we want to hide, and then when the user wants to view them via top
or ps
, there is no way to view them.
And how can we replace it? In the past I wrote a piece of Linux read dynamic link library article (Linux about modules and dynamic link library (shared object) some records), which mentioned the order in which the program loads the shared objects, there is a preload
part, we can use environment variables or configuration files to override the standard implementation (usually glibc) with our modified shared objects in advance before the program loads the rest of the system’s shared objects, which will achieve the goal of replacing readdir
, thus hiding the readdir
. readdir` to hide the process.
Practice
Let’s say I have a malicious program with the file name: evil_script.py
, and then I add the dynamic libraries after my mock system call in /etc/ld.so.preload
, but before replacing them, I try to see that I am able to see the process:
Then I added mock’s shared object to /etc/ld.so.preload
and you’ll see that ps can’t find the process:
The magic happen, the process disappear!
How to prevent
After we know the principle, then how do we deal with it? There are many ways, for example, let me cite a few:
- Do not use dynamic link libraries, use statically compiled
ps
ortop
and other programs to view, because here just replace the dynamic link libraries, static links will not be affected; - Find abnormal network connections, generally speaking, hidden processes will be its purpose, but the light local running program does not have much meaning (unless the purpose is your machine), so there will generally be a network connection, you can find the “non-existent” process by troubleshooting abnormal network connections;
- Manually/programmatically traversing the pid to see, since mock’s system call is to determine the process name, then we skip this step and traverse the pid list ourselves, one by one, to read the process information inside to check for “non-existing” processes may also be a good way to do it;
- Checking files such as
/etc/ld.so.preload
is a case of “whatever you can do”, and since we know that there might be a problem here, we might as well just check it out. - … …
Summary
In this article, we started with a small project to understand a way to “hide” a process, and briefly introduced its principle (however, I did not explore the source code of top
and ps
in depth), and provided an example to demonstrate it. Finally, it also gives a personal response based on my personal understanding, which may not be the best method, but I think it should solve some problems.