前面咱们介绍了如何修改/proc目录读取函数的方法实现进程隐藏。这篇博文将介绍另外一种方法——linux
劫持系统调用实现进程隐藏。数组
其基本原理是:加载一个内核模块(LKM),经过劫持系统调用sys_getdents()来针对进程文件进行适当的过滤,从而达到隐藏进程文件的目的。安全
因为实验方法一时,已经修改过了proc_pid_readdir函数,这会影响方法二的实现,所以必须把方法一中所作的修改恢复。打开proc_pid_readdir所在的源文件fs/proc/base.c架构
在其中加入DEBUG_PROC,表示采用方法一,这里我注释掉了,表示这时使用方法二。ide
把该函数中对进程hide字段的判断加入宏定义中便可。以后从新编译内核,编译后执行测试程序,已经没法隐藏进程。函数
下面开始介绍第二种方法:oop
① 分析读取proc文件系统时所执行的系统调用:测试
在linux下,咱们可使用strace命令来跟踪一个用户程序在执行过程当中所使用的系统调用。spa
在linux终端命令行输入:strace ps aux 2>out.txt操作系统
能够查询ps命令所执行的系统调用,结果以下:
......
open("/proc/meminfo", O_RDONLY) = 4
lseek(4, 0, SEEK_SET) = 0
read(4, "MemTotal: 495788 kB\nMemF"..., 2047) = 1114
stat("/proc/self/task", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
openat(AT_FDCWD, "/proc", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 5
mmap(NULL, 135168, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5330b72000
mmap(NULL, 135168, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f5330b51000
getdents(5, /* 140 entries */, 32768) = 3696
stat("/proc/1", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
open("/proc/1/stat", O_RDONLY) = 6
read(6, "1 (init) S 0 1 1 0 -1 4202752 22"..., 4095) = 326
close(6)
......
其中,咱们看到ps命令执行过程当中调用了getdents这个函数,而ps命令正是使
用这个函数来读取/proc目录下的进程文件的。其对应的系统调用是sys_getdents(),其函数原型以下:
int sys_getdents(unsigned int fd, struct linux_dirent64 __user *dirp, unsigned int count)
其中,fd为指向目录文件的文件描述符,该函数根据fd所指向的目录文件读取相应dirent结构,并放入dirp中,其中count为dirp中返回的数据量,正确时该函数返回值为填充到dirp的字节数。
而咱们要作的就是代替原先的系统调用,使用本身定义的系统调用hacked_getdents(),加上咱们本身的判断语句就能实现对进程文件的过滤。Linux中,替换系统调用有两种方法:一是直接修改原先的系统调用;二是编写一个内核模块(LKM),经过得到系统调用表的地址来替换原先的系统调用。
由于前者须要从新编译内核,比较费时。内核模块是一些可让操做系统内核在须要时载入和执行的代码,这一样意味着它能够在不须要时由操做系统卸载。它们扩展了操做系统内核的功能却不须要从新编译内核,所以内核模块机制的好处是:既避免了内核的臃肿不堪,又极大程度地提升了内核的可扩展性。因此这里我采用了内核模块的方法。
一个内核模块应该至少包含两个函数:初始化函数(init)和清理函数(cleanup)。初始函数负责完成一些初始化的工做,在内核模块被insmod装载时调用;而清理函数则负责干一些收尾清理的工做,在内核模块被rmmod卸载时被执行。
模块初始化流程以下:
模块清理流程以下:
③ 得到系统调用表地址
系统调用表(sys_call_table)是用来存放内核中各个系统调用处理函数入口地址的一个数组,所以只要得到系统调用表的地址,咱们就能够修改系统调用函数,甚至替换原先的系统调用函数。在这一节,咱们要作的事就是找到sys_getdents()的入口地址,并用hacked_getdents()的地址进行替换。
出于安全方面的考虑,Linux从2.4.18内核之后就再也不导出系统调用表,所以要修改系统调用,必须从软件上获取系统调用表的地址。因为不一样CPU架构的系统获取系统调用表的方式不同,这里以我本身的系统(x86_64)为例,系统调用表地址获取流程以下:
说明:
a)因为内核的页标记为只读,尝试用函数去写这个区域的内存,会产生一个内核oops。这种保护能够很简单的被规避,可经过设置cr0寄存器的WP位为0,禁止写保护CPU。
b)Linux x86_64有两套调用模式:Long模式和兼容模式,分别对应有两套调用表:sys_call_table,ia32_sys_call_table。实验中采用Long模式的系统调用表。
使用grep命令从系统映射表中获取sys_call_table的地址,其中红框中标出的是系统调用表的地址,能够看到,系统调用表的地址是ffffffff81a00180。
c)Long方式使用syscall,MSR寄存器地址为0xc0000082,宏MSR_LSTAR来表明. 使用rdmsrl指令获取system_call的代码段起始地址,再经过system_call获取sys_call_table特征码。x86_64下获取sys_call_table与x86特征码不一样,是"\xff\x14\xc5"。
④ 劫持系统调用
在得到系统调用表的地址后,咱们就能够对原先的系统调用进行替换了。系统调用
sys_getdents()的入口地址保存在数组sys_call_table以__NR_getdents为偏移的位置上。咱们定义一个函数指针orig_getdents,用于存放原始的sys_getdents()系统调用,其定义以下:
asmlinkage long (*orig_getdents)(unsigned int, struct linux_dirent64 __user *, unsigned int);
替换过程以下:
/* 保存原始系统调用 */ orig_getdents = sys_call_table[__NR_getdents]; /* 替换原始系统调用 */ sys_call_table[__NR_getdents] = hacked_getdents;
/* 恢复原始系统调用 */ sys_call_table[__NR_getdents] = orig_getdents ;
编写完内核模块后,创建Makefile:
接着编译、连接:
使用insmod hook.ko挂载模块,rmmod hook卸载模块
测试结果:
将写好的内核模块加载入内核,能够看到,内核模块得到的系统调用表地址与使用grep命令得到的系统调用表地址一致。
编译好内核模块后,运行咱们的测试程序(测试程序与方法一所用相同),所得结果以下: