概述

最近看到一个项目:libprocesshider,它可以用于隐藏一个 Linux 进程,处于好奇,就尝试了一下,发现涉及到几个有意思的点,所以记录一下。

原理

我们通常查看进程都是通过 ps 命令看的,或者当机器指标异常的时候,我们通常用 top 命令查看系统的进程情况,那么如果我们使这两个程序“看不到”我们想要隐藏的进程的话,不就完成了进程隐藏的功能了吗?

要实现这两个命令看不到我们的“隐藏进程”,那么还得回到这两个进程的原理上,它们的源码分别在:

ps 原理

  1. [root@liqiang.io]# cat library/readproc.c
  2. //////////////////////////////////////////////////////////////////////////////////
  3. // This finds processes in /proc in the traditional way.
  4. // Return non-zero on success.
  5. static int simple_nextpid(PROCTAB *restrict const PT, proc_t *restrict const p) {
  6. static __thread struct dirent *ent; /* dirent handle */
  7. char *restrict const path = PT->path;
  8. for (;;) {
  9. ent = readdir(PT->procfs);
  10. if (!ent || !ent->d_name[0]) break;
  11. if (*ent->d_name > '0' && *ent->d_name <= '9') {
  12. errno = 0;
  13. p->tgid = strtoul(ent->d_name, NULL, 10);
  14. if (errno == 0) {
  15. p->tid = p->tgid;
  16. snprintf(path, PROCPATHLEN, "/proc/%d", p->tgid);
  17. return 1;
  18. }
  19. }
  20. }
  21. return 0;
  22. }

可以看到 ps 的原理是遍历 /proc 目录查看进程列表,然后对于每个进程目录:

  1. next_proc:
  2. new_p = NULL;
  3. for (;;) {
  4. // fills in the PT->path, plus skel_p.tid and skel_p.tgid
  5. if (!PT->finder(PT,&skel_p)) goto end_procs; // simple_nextpid
  6. if (!task_dir_missing) break;
  7. if ((ret = PT->reader(PT,x))) return ret; // simple_readproc
  8. }

再读取 /proc/<pid>/stat,io,smaps_rollup,statm,status,environ,cmdline,cgroup,ns 等目录,具体源码见:library/readproc.c 文件中的:static proc_t *simple_readproc(PROCTAB *restrict const PT, proc_t *restrict const p) {

可以看到这里用到了一个重要的系统调用:readdir 用于罗列 /proc 目录。

top 原理

  1. [root@liqiang.io]# cat library/pids.c
  2. /* procps_pids_reap():
  3. *
  4. * Harvest all the available tasks/threads and provide the result
  5. * stacks along with a summary of the information gathered.
  6. *
  7. * Returns: pointer to a pids_fetch struct on success, NULL on error.
  8. */
  9. PROCPS_EXPORT struct pids_fetch *procps_pids_reap (
  10. struct pids_info *info,
  11. enum pids_fetch_type which)
  12. {
  13. ... ...
  14. if (!pids_oldproc_open(&info->fetch_PT, info->oldflags))
  15. return NULL;
  16. info->read_something = which ? readeither : readproc;

这里的 which ? readeither : readproc 的意思是如果指定了某个进程,那么就用 readproc,否则就通过 readeither 读取所有的进程,所以本质上还是和 top 一样。

隐藏原理

从上面我们可以看到,无论是 ps 还是 top 都是通过读取 /proc/ 目录下的进程信息来展示,那么如果我们在这里做一下手脚是不是就可以让用户无法感知了?

在这里有很多动手脚的方式,一种简单的方式就是替换掉 readdir 的实现,让 readdir 读取 /proc/ 目录的时候忽略掉我们想要隐藏的进程,这样,当用户想通过 top 或者 ps 查看的时候就没办法查看了。

那么如何替换呢,在以前我写过一片 Linux 读取动态链接库的文章(Linux 关于模块和动态链接库(共享对象)的一些记录),里面提到了程序加载动态链接库的顺序,其中有一个 preload 的部分,我们可以通过环境变量或者配置文件的方式,在程序加载系统其他的动态链接库之前,提前用我们修改后的动态链接库覆盖标准的实现(一般是 glibc),这样就能达到替换 readdir,从而隐藏进程的目的了。

操作演练

假如我有个恶意程序,文件名为:evil_script.py,然后我在 /etc/ld.so.preload 中添加我 mock 系统调用后的动态库,但是在替换之前,我先尝试一下我是可以看到这个进程的:

然后我将 mock 的动态链接库添加到 /etc/ld.so.preload 中,然后你就会发现 ps 无法找到这个进程了:

这个进程就这么消失了。

应对方法

在我们知道原理之后,那么我们该如何应对呢?方法有很多,例如我举几个:

小结

本文从一个小项目入手,了解了一种“隐藏”进程的方法,并且简单地介绍了它的原理(但是,关于 topps 源码,我也是浅尝了一下,没有深入探究),并且提供了一个真实实例演示。最后,还根据个人的理解给出了个人的应对办法,可能方法不是最好的,但是我认为应该可以解决一些问题。