Overview

I recently came across a project, libprocesshider, which can be used to hide a Linux process, and out of curiosity, tried it out, and found that it involves a few interesting points, so I’ll document them.

Principle

We usually check the process by ps command, or when the machine indicator is abnormal, we usually use top command to check the process situation of the system, so if we make these two programs “can’t see” the process that we want to hide, won’t we complete the function of process hiding?

To make these two commands can not see our “hidden processes”, then also need to understand the principle of these two commands, their source code in the following.

How ps works

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
[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;
}

As you can see, ps works by traversing the /proc directory to see a list of processes.

1
2
3
4
5
6
7
8
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
    }

Then read directories /proc/<pid>/stat,io,smaps_rollup,statm,status,environ,cmdline,cgroup,ns, etc. See: static proc_t *simple_readproc(PROCTAB *restrict const PT, proc_t *restrict const p) in library/readproc.c file for source code.

You can see that an important system call is used here: readdir to list the /proc directories.

How top works

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[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;

Here which ? readeither : readproc means that if a process is specified, then readproc is used, otherwise all processes are read via readeither, so it’s essentially the same as top.

The principle of hiding

As we can see from the above, both ps and top display process information by reading the /proc/ directory, so if we make a change here can we make it invisible to the user?

There are many ways to do this, one simple way is to replace the readdir implementation so that when readdir reads the /proc/ directory it ignores the processes that we want to hide, so that when the user tries to view them via top or ps they won’t be able to see them.

So how to replace it, in the past I wrote a piece of Linux read dynamic link library article, which mentioned the order of the program load dynamic link library, which has a preload part, we can through the environment variables or configuration files, in the program to load the system before the other dynamic link libraries, in advance, with our modified dynamic link library to cover the standard implementation of the standard (usually glibc), so that you can achieve the purpose of replacing the readdir, so that the purpose of the process to hide.

Practice

Suppose I have a malicious program with the file name: evil_script.py, and I add the dynamic libraries from my mock system call to /etc/ld.so.preload, but before replacing them, I try to see that I can see the process:

evil_script.py

Then I added mock’s DLL to /etc/ld.so.preload and you’ll see that ps can’t find the process.

ps can&rsquo;t find the process anymore.

The process just disappeared.

Response

After we know the principle, how do we cope with it? There are many ways, for example, let me cite a few:

  • Do not use dynamic link libraries, use static compilation of ps or top and other programs to view, because here just replace the dynamic link libraries, static links will not be affected;
  • Find abnormal network connections, generally hidden processes will have network connections, you can find the “non-existent” process by checking the abnormal network connections;
  • Manually/procedurally traverse the pid to see, since the mock system call is to determine the process name, then we skip this step, and traverse the pid list by ourselves, and read the process information inside one by one to find the “non-existent” process is also a good way;
  • Check files like /etc/ld.so.preload, since we know there might be a problem here, we can just check it.

Summary

This article started with a small project to understand a way to “hide” a process, and briefly introduced its principle (however, I did not go deeper into the source code of top and ps than I had tasted), and provided a real-life example to demonstrate it. Finally, it also gives a personal response based on my own understanding, which may not be the best method, but I think it should solve some problems.