概述
最近看到一个项目:libprocesshider,它可以用于隐藏一个 Linux 进程,处于好奇,就尝试了一下,发现涉及到几个有意思的点,所以记录一下。
原理
我们通常查看进程都是通过 ps
命令看的,或者当机器指标异常的时候,我们通常用 top
命令查看系统的进程情况,那么如果我们使这两个程序“看不到”我们想要隐藏的进程的话,不就完成了进程隐藏的功能了吗?
要实现这两个命令看不到我们的“隐藏进程”,那么还得回到这两个进程的原理上,它们的源码分别在:
- ps:https://gitlab.com/procps-ng/procps/-/tree/master/src/ps
- top:https://gitlab.com/procps-ng/procps/-/tree/master/src/top
ps 原理
[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;
}
可以看到 ps 的原理是遍历 /proc
目录查看进程列表,然后对于每个进程目录:
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
}
再读取 /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 原理
[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;
这里的 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 无法找到这个进程了:
这个进程就这么消失了。
应对方法
在我们知道原理之后,那么我们该如何应对呢?方法有很多,例如我举几个:
- 不使用动态链接库,使用静态编译的 ps 或者 top 之类的程序来查看,因为这里只是替换了动态链接库,静态链接的不会受到影响;
- 查找异常的网络连接,一般来说隐藏的进程都会它的目的,但是光本地跑程序没有太大意思(除非目的就是你的机器),所以一般都会有网络连接,可以通过排查异常的网络连接找到“不存在”的进程;
- 手动/程序遍历 pid 查看,既然 mock 的系统调用是判断的进程名,那我们跳过这一步,自己遍历 pid 列表,一一地读取里面的进程信息来排查“不存在的”进程也不妨是一种方法;
- 检查
/etc/ld.so.preload
等文件,这种属于见招拆招啦,既然我们知道这里可能有可能有问题,那么还不如检查一下直接了当。 - … …
小结
本文从一个小项目入手,了解了一种“隐藏”进程的方法,并且简单地介绍了它的原理(但是,关于 top
和 ps
源码,我也是浅尝了一下,没有深入探究),并且提供了一个真实实例演示。最后,还根据个人的理解给出了个人的应对办法,可能方法不是最好的,但是我认为应该可以解决一些问题。