情景导入
在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目录,需要隐藏的进程已经消失。
不过,本文省略了设置需要隐藏的进程的细节,这个需要自行实现,本文仅介绍隐藏进程。