前言 这原本是HDU操作系统课程设计的第二个作业,内核模块编程,但是寻思着仅仅重复当初编写系统调用的功能代码也太无趣了,况且还不能被重复调用。正好之前在系统调用的时候遇见过使用模块编程hook系统调用的教程,我为什么不正好来实践一下呢?说干就干!
选题 其实选题并不重要,但是做事情总要有目标对吧?况且最终提交方式也是作业。我选择了一个比较好在hook中有意义的功能:
我们hook的目标函数是fork(),当使用syscall(__NR_FORK)时,我们将会输出当前进程与fork子进程的上述相关信息。
思路
获取syscall_table。
保存原始函数:1 2 asmlinkage long (*origin) (void ) = NULL ; origin = (long (*)(void ))(sys_call_table[__NR_fork]);
替换为我们的hook函数:1 sys_call_table[__NR_fork] = (unsigned long )&hacked_func;
在hook函数中加入我们自己的功能。
过程 获取系统调用表 很遗憾,在Linux2.6版本后,syscall_table不再被EXPORT,我们只能采取曲线救国的方式。方法很多,由于不是我们实验的重点,我们采取简单的获取+硬编码 的方式。
1 $ sudo grep sys_call_table /boot/System.map-`uname -r`
在此,我们选择sys_call_table即可,后两者是为了兼容性准备的。
1 unsigned long *sys_call_table = (unsigned long *) 0xffffffff82000300 ;
关闭写保护 在改写系统调用表前,我们需要关闭寄存器cr0的写保护位(WP)也就是第17位:
Q:在这里,可能会有疑问为什么不需要改变内存的读写权限呢?(系统调用表的内存一般是只读的) A:
页面错误 正当我们开开心心完成我们的基础设施后(隐藏了一些次要代码):
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 unsigned long *sys_call_table;asmlinkage long (*origin) (void ) = NULL ; asmlinkage long hacked_func (void ) ; int origin_cr0;static int __init init_mymodule (void ) { printk("start init\n" ); sys_call_table = (unsigned long *) 0xffffffff82000300 ; printk("the syscall table's address is %lx\n" , (unsigned long )sys_call_table); origin = (long (*)(void ))(sys_call_table[__NR_fork]); printk("got origin func\n" ); printk("try to change mode\n" ); origin_cr0 = clear_cr0(); sys_call_table[__NR_fork] = (unsigned long )&hacked_func; printk("changed successfully\n" ); setback_cr0(origin_cr0); return 0 ; } static void __exit exit_mymodule (void ) { origin_cr0 = clear_cr0(); sys_call_table[__NR_fork] = (unsigned long )origin; setback_cr0(origin_cr0); printk(KERN_DEBUG "goodbye~\n" ); } asmlinkage long hacked_func () { printk("hack successfully!\n" ); return origin(); }
执行装载:
1 2 $ sudo insmod mymodule.ko KILLED
查看dmesg,我们看到了上篇文章出现的熟悉字样PAGE FAULT,这个问题困扰了很久,因为我们的代码是没错的。
原因在于kaslr(Kernel Address Space Layout Randomization),盲猜是为了防止栈溢出攻击(滑稽。我们可以很轻松地在grub中添加启动选项-nokaslr来关闭它。(这样的行为是不安全的,完全是为了实验目的,真实情况还需采用其他方式!)
1 2 3 $ sudo vim /etc/default/grub # ... $ sudo reboot
期间还碰到一件很有意思的事情,在我调试尝试用printk("%p")去打印sys_call_table的地址时,发现我的地址打印错误!刚开始有点怀疑人生,后来查阅资料才知道,printk直接打印的%p地址会经过哈希加密防止内核地址泄漏。How to get printk format specifiers right
实现我们的功能 这个模块的实际功能并不复杂,主要有一点需要注意的是关于进程父子关系链表的实现逻辑。
在task_struct中,我们可以看到有两个list_head字段:children,sibling。
我们可以这样认为,因为在linux的链表实现中,采用的是空头节点的双向循环链表,而children可以理解为儿子节点的头节点,扩展到container的角度讲,当前节点就是儿子节点的空头节点,而sibling我们可以理解为是当前节点的条目(entry)。
按照上述逻辑,我们可以通过以下方式来达到遍历儿子节点的目的:
1 2 3 4 struct task_struct *pos = NULL ;list_for_each_entry(pos, p_children, sibling) { }
源代码 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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 #include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/kallsyms.h> static int clear_cr0 (void ) { unsigned int cr0 = 0 ; unsigned int ret; asm volatile ("mov %%cr0,%%rax" : "=a" (cr0)) ; ret = cr0; cr0 &= 0xfffeffff ; asm volatile ("mov %%rax,%%cr0" ::"a" (cr0)) ; return ret; } static void setback_cr0 (int val) { asm volatile ("mov %%rax,%%cr0" ::"a" (val)) ; } unsigned long *sys_call_table;asmlinkage long (*origin) (void ) = NULL ; asmlinkage long hacked_func (void ) ; int origin_cr0;static int __init init_mymodule (void ) { printk("start init\n" ); sys_call_table = (unsigned long *) 0xffffffff82000300 ; printk("the syscall table's address is %lx\n" , (unsigned long )sys_call_table); origin = (long (*)(void ))(sys_call_table[__NR_fork]); printk("got origin func\n" ); printk("try to change mode\n" ); origin_cr0 = clear_cr0(); sys_call_table[__NR_fork] = (unsigned long )&hacked_func; printk("changed successfully\n" ); setback_cr0(origin_cr0); return 0 ; } static void __exit exit_mymodule (void ) { origin_cr0 = clear_cr0(); sys_call_table[__NR_fork] = (unsigned long )origin; setback_cr0(origin_cr0); printk(KERN_DEBUG "goodbye~\n" ); } void print_children (struct task_struct *p_task) { struct task_struct *pos = NULL ; struct list_head *p_children ; p_children = &p_task->children; if (list_empty(p_children)) { printk("Children: (null)\n" ); } else { printk("Children: \n" ); list_for_each_entry(pos, p_children, sibling) { printk("\t%d\n" , pos->pid); } } } void print_task (struct task_struct *p_task) { printk("pid is %d\n" , p_task->pid); printk("Parent's pid is %d\n" , p_task->real_parent->pid); print_children(p_task); } asmlinkage long hacked_func () { pid_t new_pid; struct pid *p_pid ; struct task_struct *p_new ; printk("hack successfully!\n" ); new_pid = origin(); printk("[Current]\n" ); print_task(current); if (new_pid != -1 ) { p_pid = find_get_pid(new_pid); p_new = get_pid_task(p_pid, PIDTYPE_PID); printk("[Fork]\n" ); print_task(p_new); } else { printk("fork fail\n" ); return new_pid; } return new_pid; } module_init(init_mymodule); module_exit(exit_mymodule); MODULE_LICENSE("GPL" ); MODULE_AUTHOR("yztz" ); MODULE_VERSION("v1" ); MODULE_DESCRIPTION("This is my module" );
参考文献 https://blog.csdn.net/qq_37414405/article/details/84487591 https://stackoverflow.com/questions/58523370/kernel-module-crash-when-reading-system-call-table-function-address https://elixir.bootlin.com/linux/v5.15-rc6/source/include/linux/list.h#L280 https://wohin.me/linux-rootkit-shi-yan-0004-ling-wai-ji-chong-xi-tong-diao-yong-gua-gou-ji-zhu/ https://www.kernel.org/doc/html/latest/core-api/printk-formats.html https://www.cnblogs.com/jiayy/p/3562055.html