在Linux内核态隐藏用户空间进程
本文最后更新于 547 天前,其中的信息可能已经有所发展或是发生改变。

情景导入

在Linux中,一切皆文件。隐藏进程和隐藏文件的原理是一样的。

每一个用户空间进程,都会在/proc下生成一个以pid作为名称的目录,进程的各项信息放置在该目录中。top等命令可以通过遍历/proc目录来得到系统中正在运行的进程信息。

所以,隐藏一个进程,只需要移除它在proc中的条目即可。


通过网络搜索可以知道,proc文件系统不是一个真实存在的文件系统,它存在于内存上,仅仅只是“挂载”到了文件系统上,proc只是它的入口点。

而深入阅读了Linux内核关于此部分的实现后,其实proc根本无所谓挂载的概念。它符合量子特性,仅在你观察它的时候它才存在。当你通过opendir打开proc文件系统时,内核会为你生成它。当你关闭它时,它的一切都灰飞烟灭。

所以无所谓“移除”proc中的条目,只需要在内核生成proc时,过滤掉一些内容,即可达到隐藏的目的。为了避免对内核源代码的修改,我使用内核模块来实现这一点。

Kprobe

首先需要了解一个概念,kprobe。

kprobe是Linux内核的一个重要特性,是一个轻量级的内核调试工具,它可以在运行的内核中动态插入探测点,执行你预定义的操作。并且,它可以向内核中几乎全部的函数添加探测点(被static修饰的函数除外)。

通过阅读内核源代码,可以得知对proc内容的填充在proc_pid_readdir[fs/proc/base.c]中。如下是它的实现

/* for the /proc/ directory itself, after non-process stuff has been done */
int proc_pid_readdir(struct file *file, struct dir_context *ctx)
{
	struct tgid_iter iter;
	struct proc_fs_info *fs_info = proc_sb_info(file_inode(file)->i_sb);
	struct pid_namespace *ns = proc_pid_ns(file_inode(file)->i_sb);
	loff_t pos = ctx->pos;

	if (pos >= PID_MAX_LIMIT + TGID_OFFSET)
		return 0;

	if (pos == TGID_OFFSET - 2) {
		struct inode *inode = d_inode(fs_info->proc_self);
		if (!dir_emit(ctx, "self", 4, inode->i_ino, DT_LNK))
			return 0;
		ctx->pos = pos = pos + 1;
	}
	if (pos == TGID_OFFSET - 1) {
		struct inode *inode = d_inode(fs_info->proc_thread_self);
		if (!dir_emit(ctx, "thread-self", 11, inode->i_ino, DT_LNK))
			return 0;
		ctx->pos = pos = pos + 1;
	}
	iter.tgid = pos - TGID_OFFSET;
	iter.task = NULL;
	for (iter = next_tgid(ns, iter);
	     iter.task;
	     iter.tgid += 1, iter = next_tgid(ns, iter)) {
		char name[10 + 1];
		unsigned int len;

		cond_resched();
		if (!has_pid_permissions(fs_info, iter.task, HIDEPID_INVISIBLE))
			continue;

		len = snprintf(name, sizeof(name), "%u", iter.tgid);
		ctx->pos = iter.tgid + TGID_OFFSET;
		if (!proc_fill_cache(file, ctx, name, len,
				     proc_pid_instantiate, iter.task, NULL)) {
			put_task_struct(iter.task);
			return 0;
		}
	}
	ctx->pos = PID_MAX_LIMIT + TGID_OFFSET;
	return 0;
}

它的第二个参数是dir_context类型,用来向调用者填充目录内容。

/*
 * This is the "filldir" function type, used by readdir() to let
 * the kernel specify what kind of dirent layout it wants to have.
 * This allows the kernel to read directories into kernel space or
 * to have different dirent layouts depending on the binary type.
 */
struct dir_context;
typedef int (*filldir_t)(struct dir_context *, const char *, int, loff_t, u64,
			 unsigned);

struct dir_context {
	filldir_t actor;
	loff_t pos;
};

dir_context中有一个actor成员,是一个回调函数的指针,在proc_pid_readdir中,将调用此函数指针来向调用方传递目录项。

proc_pid_readdir开始处填充了self和thread-self两个目录项,分别是指向当前进程以及指向当前线程的proc目录项。由此也可以理解proc的魔法了。

我们的主要关注点在它的for循环中。

在循环中,proc_pid_readdir遍历了内核中的每一个用户空间进程,并根据权限设置,决定是否放入dir_context中。

if (!has_pid_permissions(fs_info, iter.task, HIDEPID_INVISIBLE))
        continue;

无法通过hook has_pid_permissions来跳过特定进程,因为has_pid_permissions被static修饰,它没有导出符号。

在has_pid_permissions之后,需要关注的函数只有两个,proc_fill_cache以及put_task_struct。

proc_fill_cache用于填充/proc下关于特定进程的缓存信息,而pus_task_struct被static修饰。

考虑之后,我决定阻止proc_pid_readdir向调用方添加目录项,即hook dir_context的actor函数。

实现

首先编写一个内核模块

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

static int __init lsx_init(void){
    
}

static void __exit lsx_exit(void){

}


module_init(lsx_init);
module_exit(lsx_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ling");
MODULE_DESCRIPTION("Memory Hook Module");

本文不会详细介绍内核模块的技术细节,这是很基础的技能。

首先我们需要一个位图来索引需要隐藏的进程。我们不能直接修改task_status结构体,因为我们要避免直接修改内核源代码,这也是为什么我要编写内核模块。

Bitmap hide_process_bitmap;

///设置位
void process_set_bit(Bitmap * bitmap,int pos){
    bitmap->data[pos / (sizeof(u32) * 8)] |= (1 << (pos % (sizeof(u32 ) * 8)));
}

///清除位
void process_clear_bit(Bitmap *bitmap,int pos){
    bitmap->data[pos / (sizeof(u32) * 8)] &= ~(1 <<(pos %(sizeof(u32) * 8)));
}

///测试位
int process_test_bit(Bitmap *bitmap,int pos){
    return bitmap->data[pos / (sizeof(u32) * 8)] & (1 << (pos % (sizeof(u32) * 8)));
}

然后,使用kprobe来在proc_pid_readdir的入口点埋一个钩子。

typedef int(*actor_t)(struct dir_context *,const char *name,int namlen,loff_t offset,u64 ino,unsigned d_type);

//原始actor函数
actor_t original_actor = NULL;

/// 替代dir_context中actor的函数
/// 执行一些检查后,将位图中保存的需要隐藏的进程,阻止对原始actor的调用,达到过滤的目的。
static int custom_actor(struct dir_context *ctx,const char *name,int namlen,loff_t offset,u64 ino,unsigned d_type){
    int pid;
    int ret = kstrtoint(name,10,&pid);
    if(ret != 0){
        pr_info(LOG_INFO "actor name to int error : %s",name);
        return original_actor(ctx,name,namlen,offset,ino,d_type);
    }
    if(process_test_bit(&hide_process_bitmap,pid)){
        pr_info(LOG_INFO "prevent actor add pid : %s",name);
        return 0;
    }
    return original_actor(ctx,name,namlen,offset,ino,d_type);
}

/// 在proc_pid_readdir之前被执行
/// 在这里获取proc_pid_readdir的第二个参数,也就是dir_context的指针
/// 并在保存dir_context中原始的actor之后,使用custom_actor替换它
/// 使proc_pid_readdir对actor的调用交由我们来执行。
static int handler_pre(struct kprobe *p,struct pt_regs *regs){
    struct dir_context *ctx = (struct dir_context*)regs->regs[1];
    original_actor = ctx->actor;
    ctx->actor = custom_actor;
    pr_info(LOG_INFO "hook success!");
    return 0;
}

static struct kprobe kp = {
    .pre_handler = handler_pre,
    .symbol_name = "proc_pid_readdir",
};

static int __init lsx_init(void){
    int ret = register_kprobe(&kp);
    if(ret < 0){
        pr_info("[Proc Call] kprobe proc_pid_readdir error! code : %d",ret);
        return;
    }
}

至此,隐藏功能已经实现,但是在进程退出后,模块不会清理该进程的设置。如果被隐藏的进程退出了,而它的pid被分配给了另一个新进程来使用,那么就会错误的隐藏原本不因该隐藏的进程。

Linux内核中并没有进程退出的事件,但是我们可以使用刚刚的技术来自己实现一个。

阅读源码可知,所有进程要退出,最后都会执行do_exit[kernel/exit.c]系统调用。所以,我们可以使用kprobe在do_exit中埋下一个钩子,来监听进程退出事件。

它的签名如下

void __noreturn do_exit(long code);

我们不需要关心它内部的实现,也不需要关心它的参数列表,我们仅仅只需要在do_exit的入口点埋下一个钩子,然后就可以通过current宏获得当前进程的task_status结构体。

//监听进程退出事件,并清理位图
static int process_exit_notifier_pre(struct kprobe *p,struct pt_regs *regs) {
    struct task_struct *task = current;
    if(process_test_bit(&hide_process_bitmap,task->pid)){
        process_clear_bit(&hide_process_bitmap,task->pid);
        pr_info(LOG_INFO "process %d exit , so unhide",task->pid);
    }
    return 0;
}

static struct kprobe process_exit_kp = {
    .pre_handler = process_exit_notifier_pre,
    .symbol_name = "do_exit",
};

void __init hide_process_init(void){
    int ret = register_kprobe(&process_exit_kp);
    if(ret < 0){
        pr_info(LOG_INFO "kprobe process_exit_notifier_ptr error! code : %d",ret);
    }
}

至此,在Linux内核中隐藏用户空间进程的目的已经达成。查看proc目录,需要隐藏的进程已经消失。

不过,本文省略了设置需要隐藏的进程的细节,这个需要自行实现,本文仅介绍隐藏进程。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇