当前位置:Linux教程 - Linux - Linux可卸载内核模块完全指南

Linux可卸载内核模块完全指南



         作者:pragmatic/THC,(版本1.0) 

    简介

    将Linux操作系统用于服务器在现在是越来越普遍了.因此,入侵Linux在今天也变得越来越有趣.目前最好的攻击Linux的技术就是修改内核代码.由于一种叫做可卸载内核(Loadable KernelModules(LKMs))的机制,我们有可能编写在内核级别运行的代码,而这种代码可以允许我们接触到操作系统中非常敏感的部分.在过去有一些很好的关于LKM知识的文本或者文件,他们介绍一些新的想法,方法以及一名Hacker所梦寐以求的完整的LKMs.而且也有一些很有趣的公开的讨论(在新闻组,邮件列表).
    然而为什么我再重新写这些关于LKMs的东西呢?下面是我的一些理由:
    在过去的教材中常常没有为那些初学者提供很好的解释.而这个教材中有很大一部分的基础章节.这是为了帮助那些初学者理解概念的.我见过很多人使用系统的缺陷或者监听器然而却丝毫不了解他们是如何工作的.在这篇文章中我包含了很多带有注释的源代码,只是为了帮助那些认为入侵仅仅是一些工具游戏的初学者!
    每一个发布的教材不过把话题集中在某个特别的地方.没有一个完整的指导给那些关注LKMs的Hacker.这篇文章会覆盖几乎所有的关于LKMs的资料(甚至是病毒方面的).
    这篇文章是从Hacker或者病毒的角度进行讨论的,但是系统管理员或者内核的开发者也可以参考并从中学到很多东西.
    以前的文章介绍一些利用LKMs进行入侵的优点或者方法,但是总是还有一些东西是我们过去从来没有听说过的.这篇文章会介绍一些新的想法给大家.(不是所有的新的资料,只是一些对我们有帮助的)
    这篇文章会介绍一些简单的防止LKM攻击的方法,同时也会介绍如何通过使用一些像运行时内核补丁(Runtime Kernel Patching)这样的方法来对付这些防御措施.
    要记住这些新的想法仅仅是通过利用一些特殊的模块来实现的.要在现实中真正使用他们还需要对他们进行改进.这篇文章的主要目的是给大家在整个LKM上一个大方向上的指导.在附录A中,我会给大家一些实用的LKMs,并附上一些简短的注释(这是为那些新手的),以及如何使用他们.
    整篇文章(除了第五部分)是基于 Linu 2.0.x的80x86机器的.我测试了所有的程序和代码段.为了能够正常使用这里提供的绝大部分代码,你的Linux系统必须有LKM支持.只有在第四部分会给大家一些不需要LKM支持的源代码.本文的绝大多数想法一样可以在Linux2.2.x上实现(也许你会需要一些小小的改动).
    这篇文章会有一个特别的章节来帮助系统管理员进行系统安全防护.你(作为一名Hacker)也必须仔细阅读这些章节.你必须要知道所有系统管理员知道的,甚至更多.你也会从中发现很多优秀的想法.这也会对你开发高级的入侵系统的LKMs有所帮助.
    因此,通读这篇文章吧.

    最后提醒:本文只是为教育目的而编写.任何将本文用于非法目的的行为本人概不负责.


    第一部分. 基础知识


    1.1 什么是LKMs
    LKMs就是可卸载的内核模块(Loadable Kernel Modules)。这些模块本来是Linux系统用于扩展他的功能的。使用LKMs的优点有:他们可以被动态的加载,而且不需要重新编译内核。由于这些优点,他们常常被特殊的设备(或者文件系统),例如声卡等使用。

    每个LKM至少由两个基本的函数组成:
    int init_module(void) /*用于初始化所有的数据*/
    {

    ...

    }

    void cleanup_module(void) /*用于清除数据从而能有一个安全的退出*/

    {

    ...

    }

    加载一个模块(常常只限于root能够使用)的命令是:

    # insmod module.o

    这个命令让系统进行了如下工作:

    加载可执行的目标文件(在这儿是module.o)

    调用 create_module这个系统调用(至于什么叫系统调用,见1.2)来分配内存.

    不能解决的引用由系统调用get_kernel_syms进行查找引用.

    在此之后系统调用init_module将会被调用用来初始化LKM->执行 int inti_module(void) 等等

    (内核符号将会在1.3节中内核符号表中解释)

    OK,到目前为止,我想我们可以写出我们第一个小的LKM来演示一下这些基本的功能是如何工作的了.

    #define MODULE

    #include

    int init_module(void)

    {

    printk(\"<1>Hello World\\n\");

    return 0;

    }

    void cleanup_module(void)

    {

    printk(\"<1>Bye, Bye\");

    }

    你可能会奇怪为什么在这里我用printk(....)而不是printf(.....).在这里你要明白内核编程是完全不同于普通的用户环境下的编程的.你只能使用很有限的一些函数(见1.6)仅使用这些函数你是干不了什么的.因此,你将会学会如何使用你在用户级别中用的那么多函数来帮助你入侵内核.耐心一些,在此之前我们必须做一点其他的.....

    上面的那个例子可以很容易的被编译:

    # gcc -c -O3 helloworld.c

    # insmod helloworld.o

    OK,现在我们的模块已经被加载了并且给我们打印出了那句很经典的话.现在你可以通过下面这个命令来确认你的LKM确实运行在内核级别中:

    # lsmod

    Module     Pages  Used by

    helloworld     1    0

    这个命令读取在 /proc/modules 的信息来告诉你当前那个模块正被加载.\Pages\

    显示的是内存的信息(这个模块占了多少内存页面).\Used by\显示了这个模块被系统使用的次数(引用计数).这个模块只有当这个计数为0时才可以被除去.在检查过这个以后,你可以用下面的命令卸载这个模块

    # rmmod helloworld

    OK,这不过是我们朝LKMs迈出的很小的一步.我常常把这些LKMs于老的DOS TSR程序做比较,(是的,我知道他们之间有很多地方不一样),那些TSR能够常驻在内存并且截获到我们想要的中断.Microsoft\s Win9x有一些类似的东西叫做VxD.关于这些程序的最有意思的一点在于他们都能够挂在一些系统的功能上,在Linux中我们称这些功能为系统调用.

    1.2什么是系统调用

    我希望你能够懂,每个操作系统在内核中都有一些最为基本的函数给系统的其他操作调用.在Linux系统中这些函数就被称为系统调用(System Call).他们代表了一个从用户级别到内核级别的转换.在用户级别中打开一个文件在内核级别中是通过sys_open这个系统调用实现的.在/usr/include/sys/syscall.h中有一个完整的系统调用列表.下面的

    列表是我的syscall.h
    #ifndef _SYS_SYSCALL_H

    #define _SYS_SYSCALL_H

    #define SYS_setup 0

    /* 只被init使用,用来启动系统的*/

    #define SYS_exit 1

    #define SYS_fork 2

    #define SYS_read 3

    #define SYS_write 4

    #define SYS_open 5

    #define SYS_close 6

    #define SYS_waitpid 7

    #define SYS_creat 8

    #define SYS_link 9

    #define SYS_unlink 10

    #define SYS_execve 11

    #define SYS_chdir 12

    #define SYS_time 13

    #define SYS_prev_mknod 14

    #define SYS_chmod 15

    #define SYS_chown 16

    #define SYS_break 17

    #define SYS_oldstat 18

    #define SYS_lseek 19

    #define SYS_getpid 20

    #define SYS_mount 21

    #define SYS_umount 22

    #define SYS_setuid 23

    #define SYS_getuid 24

    #define SYS_stime 25

    #define SYS_ptrace 26

    #define SYS_alarm 27

    #define SYS_oldfstat 28

    #define SYS_pause 29

    #define SYS_utime 30

    #define SYS_stty 31

    #define SYS_gtty 32

    #define SYS_access 33

    #define SYS_nice 34

    #define SYS_ftime 35

    #define SYS_sync 36

    #define SYS_kill 37

    #define SYS_rename 38

    #define SYS_mkdir 39

    #define SYS_rmdir 40

    #define SYS_dup 41

    #define SYS_pipe 42

    #define SYS_times 43

    #define SYS_prof 44

    #define SYS_brk 45

    #define SYS_setgid 46

    #define SYS_getgid 47

    #define SYS_signal 48

    #define SYS_geteuid 49

    #define SYS_getegid 50

    #define SYS_acct 51

    #define SYS_phys 52

    #define SYS_lock 53

    #define SYS_ioctl 54

    #define SYS_fcntl 55

    #define SYS_mpx 56

    #define SYS_setpgid 57

    #define SYS_ulimit 58

    #define SYS_oldolduname 59

    #define SYS_umask 60

    #define SYS_chroot 61

    #define SYS_prev_ustat 62

    #define SYS_dup2 63

    #define SYS_getppid 64

    #define SYS_getpgrp 65

    #define SYS_setsid 66

    #define SYS_sigaction 67

    #define SYS_siggetmask 68

    #define SYS_sigsetmask 69

    #define SYS_setreuid 70

    #define SYS_setregid 71

    #define SYS_sigsuspend 72

    #define SYS_sigpending 73

    #define SYS_sethostname 74

    #define SYS_setrlimit 75

    #define SYS_getrlimit 76

    #define SYS_getrusage 77

    #define SYS_gettimeofday 78

    #define SYS_settimeofday 79

    #define SYS_getgroups 80

    #define SYS_setgroups 81

    #define SYS_select 82

    #define SYS_symlink 83

    #define SYS_oldlstat 84

    #define SYS_readlink 85

    #define SYS_uselib 86

    #define SYS_swapon 87

    #define SYS_reboot 88

    #define SYS_readdir 89

    #define SYS_mmap 90

    #define SYS_munmap 91

    #define SYS_truncate 92

    #define SYS_ftruncate 93

    #define SYS_fchmod 94

    #define SYS_fchown 95

    #define SYS_getpriority 96

    #define SYS_setpriority 97

    #define SYS_profil 98

    #define SYS_statfs 99

    #define SYS_fstatfs 100

    #define SYS_ioperm 101

    #define SYS_socketcall 102

    #define SYS_klog 103

    #define SYS_setitimer 104

    #define SYS_getitimer 105

    #define SYS_prev_stat 106

    #define SYS_prev_lstat 107

    #define SYS_prev_fstat 108

    #define SYS_olduname 109

    #define SYS_iopl 110

    #define SYS_vhangup 111

    #define SYS_idle 112

    #define SYS_vm86old 113

    #define SYS_wait4 114

    #define SYS_swapoff 115

    #define SYS_sysinfo 116

    #define SYS_ipc 117

    #define SYS_fsync 118

    #define SYS_sigreturn 119

    #define SYS_clone 120

    #define SYS_setdomainname 121

    #define SYS_uname 122

    #define SYS_modify_ldt 123

    #define SYS_adjtimex 124

    #define SYS_mprotect 125

    #define SYS_sigprocmask 126

    #define SYS_create_module 127

    #define SYS_init_module 128

    #define SYS_delete_module 129

    #define SYS_get_kernel_syms 130

    #define SYS_quotactl 131

    #define SYS_getpgid 132

    #define SYS_fchdir 133

    #define SYS_bdflush 134

    #define SYS_sysfs 135

    #define SYS_personality 136

    #define SYS_afs_syscall 137

    #define SYS_setfsuid 138

    #define SYS_setfsgid 139

    #define SYS__llseek 140

    #define SYS_getdents 141

    #define SYS__newselect 142

    #define SYS_flock 143

    #define SYS_syscall_flock SYS_flock

    #define SYS_msync 144

    #define SYS_readv 145

    #define SYS_syscall_readv SYS_readv

    #define SYS_writev 146

    #define SYS_syscall_writev SYS_writev

    #define SYS_getsid 147

    #define SYS_fdatasync 148

    #define SYS__sysctl 149

    #define SYS_mlock 150

    #define SYS_munlock 151

    #define SYS_mlockall 152

    #define SYS_munlockall 153

    #define SYS_sched_setparam 154

    #define SYS_sched_getparam 155

    #define SYS_sched_setscheduler 156

    #define SYS_sched_getscheduler 157

    #define SYS_sched_yield 158

    #define SYS_sched_get_priority_max 159

    #define SYS_sched_get_priority_min 160

    #define SYS_sched_rr_get_interval 161

    #define SYS_nanosleep 162

    #define SYS_mremap 163

    #define SYS_setresuid 164

    #define SYS_getresuid 165

    #define SYS_vm86 166

    #define SYS_query_module 167

    #define SYS_poll 168

    #define SYS_syscall_poll SYS_poll

    #endif /* */

    每个系统调用都有一个预定义的数字(见上表),那实际上是用来进行这些调用的.内核通过中断0x80来控制每一个系统调用.这些系统调用的数字以及任何参数都将被放入某些寄存器(eax用来放那些代表系统调用的数字,比如说)

    那些系统调用的数字是一个被称之为sys_call_table[]的内核中的数组结构的索引值.这个结构把系统调用的数字映射到实际使用的函数.

    OK,这些是继续阅读所必须的足够知识了.下面的表列出了那些最有意思的系统调用以及一些简短的注释.相信我,为了你能够真正的写出有用的LKM你必须确实懂得那些系统调用是如何工作的.

    系统调用列表:

    int sys_brk(unsigned long new_brk);

    改变DS(数据段)的大小->这个系统调用会在1.4中讨论

    int sys_fork(struct pt_regs regs);

    著名的fork()所用的系统调用

    int sys_getuid ()

    int sys_setuid (uid_t uid)

    用于管理UID等等的系统调用

    int sys_get_kernel_sysms(struct kernel_sym *table)

    用于存取系统函数表的系统调用(->1.3)

    int sys_sethostname (char *name, int len);

    int sys_gethostname (char *name, int len);

    sys_sethostname是用来设置主机名(hostname)的,sys_gethostname是用来取的

    int sys_chdir (const char *path);

    int sys_fchdir (unsigned int fd);

    两个函数都是用于设置当前的目录的(cd ...)

    int sys_chmod (const char *filename, mode_t mode);

    int sys_chown (const char *filename, mode_t mode);

    int sys_fchmod (unsigned int fildes, mode_t mode);

    int sys_fchown (unsigned int fildes, mode_t mode);

    用于管理权限的函数

    int sys_chroot (const char *filename);

    用于设置运行进程的根目录的

    int sys_execve (struct pt_regs regs);

    非常重要的系统调用->用于执行一个可执行文件的(pt_regs是堆栈寄存器)

    long sys_fcntl (unsigned int fd, unsigned int cmd, unsigned long arg);

    改变fd(打开文件描述符)的属性的

    int sym_link (const char *oldname, const char *newname);

    int sys_unlink (const char *name);

    用于管理硬/软链接的函数

    int sys_rename (const char *oldname, const char *newname);

    用于改变文件名

    int sys_rmdir (const char* name);

    int sys_mkdir (const *char filename, int mode);

    用于新建已经删除目录

    int sys_open (const char *filename, int mode);

    int sys_close (unsigned int fd);

    所有和打开文件(包括新建)有关的操作,还有关闭文件的.

    int sys_read (unsigned int fd, char *buf, unsigned int count);

    int sys_write (unsigned int fd, char *buf, unsigned int count);

    读写文件的系统调用

    int sys_getdents (unsigned int fd, struct dirent *dirent, unsigned int count);

    用于取得文件列表的系统调用(ls...命令)

    int sys_readlink (const char *path, char *buf, int bufsize);

    读符号链接的系统调用
    int sys_selectt (int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp);

    多路复用I/O操作

    sys_socketcall (int call, unsigned long args);

    socket 函数

    unsigned long sys_create_module (char *name, unsigned long size);

    int sys_delete_module (char *name);

    int sys_query_module (const char *name, int which, void *buf, size_t bufsize, size_t *ret);

    用于模块的加载/卸载和查询.

    以上就是我认为入侵者会感兴趣的系统调用.当然如果要获得系统的root权你有可能需要一些特殊的系统调用,但是作为一个hacker他很可能会拥有一个上面列出的最基本的列表.在第二部分中你会知道如何利用这些系统调用来实现你自己的目的.

    1.3什么是内核符号表(Kernel-Symbol-Table)


    OK,我们现在已经理解了最基本的系统调用和模块的概念,但是还需要继续理解另一个十分关键的概念--内核符号表。

    让我们看一眼/proc/ksyms.在里面的每一个表项代表着一个公共的内核符号.这些符号是可以被我们的LKM引用的.再认真的看一眼这个文件,你会发现很多有趣的事情.

    这个文件真的很有趣,他可以帮助我们看到我们的LKM到底可以调用那些函数.但是这也同时带来一个问题,在我们的LKM中所存取的每一个符号(像函数名)也会被列在这个文件里面,也会被其他人看到.因此,一个经验丰富的系统管理员就会发现我们小小的LKM并且杀掉他.

    有很多方法可以阻止管理员发现我们的LKM,这可以看第二章节.在第二章节中提到的方法可以被称之为\Hacks\,但是当我们在看第二章节的内容时你会发现里面并没有提到\"如何使你的LKM符号不列在/proc/ksyms中\"这样的方法.在第二章中我们没有提到这个问题是基于以下理由的:

    你并不需要什么特殊的技巧来使你的模块的符号不在/proc/ksyms中出现.LKM开发者们用如下的常用代码来声明他们模块的符号.

    static struct symbol_table module_syms= {

    /*定义自己的符号表!!*/

    #include     

    /*我们想引用的符号表*/

    ...                  

    };



    register_symtab(&module_syms);    

    /*实际的注册工作*/



    正如我所说的,我们并不需要对外公开我们的符号,所以我们只要用如下的语句就可以了

    register_symtab(NULL);

    这条语句必须放在init_module()函数中,要记住!

    1.4如何实现从用户空间到内核空间的转换


    直到现在这篇文章所介绍的都是非常基本和简单的一些东西.从现在开始会有一些稍微难一点的.(但是并不是太难的).

    在内核级别内编程带给我们很多好处,同时也给我们带来了不少缺点.系统调用必须从用户空间中获得调用的参数,(系统调用是通过包装在像libc这样的函数库中实现的)然而我们的LKM是运行在内核级别的.在第二章中我们会看到对于我们来说,通过检查某个系统调用的参数来使LKM正确的运行是十分重要的.但是,在内核空间的模块如何才能存取到用户空间分配的参数呢?

    解决方法是:我们必须做一个传输转换.

    对于那些非内核探索者来说,这听上去可能有些奇怪.但是这真的很简单.比如说下面这个系统调用:

    int sys_chdir (const char *path)

    设想系统正在调用这个函数,并且我们拦截了这个调用(我们会在第二章解释这个).我们想检查用户想去的路径.因此我们必须获得const char* path.如果你企图通过下面的

    方法直接获得path变量:

    printk(\"<1>%s\\n\", path);

    你就会遇到真正的问题了.....

    要记住你现在是在内核空间中,你不能很轻松的读取用户空间的内存.好了,你可以获得一个plaguez提供的解决方法.他通过使用内核的函数(宏)从用户空间的内存中中提取一个字符串到内核空间:
    #include
    get_user(pointer);

    传给这个函数一个*path的指针从而把他从用户空间的内存拷贝到内核空间.让我们看看这段由plaguez写的从用户空间移动字符串到内核空间的代码:

    char *strncpy_fromfs(char *dest, const char *src, int n)

    {

      char *tmp = src;

      int compt = 0;



      do {

    dest[compt++] = __get_user(tmp++, 1);

      }
     while ((dest[compt - 1] != \\\0\) && (compt != n));
      return dest;

    }

    如果我们想转换我们的*path变量,我们可以用如下的内核代码:

    char *kernel_space_path;

    kernel_space_path = (char *) kmalloc(100, GFP_KERNEL);

    /*在内核空间中分配内存*/

    (void) strncpy_fromfs(test, path, 20);        

    /*调用plaguez\s的函数*/                     

    printk(\"<1>%s\\n\", kernel_space_path);        

    /*现在我们可以使用这个变量了*/  

    kfree(test);                    

    /*记得释放内存*/                        

    上面的代码工作的很好.由于一个通用的转换程序有点复杂了,plaguez仅仅做了字符串的拷贝(这个函数只能用于字符串的拷贝).对于普通的数据,下面的函数是最简单的做法:

    #include

    void memcpy_fromfs(void *to, const void *from, unsigned long count);

    很显然,两个函数都是同一种类型的命令.但是第二个几乎和plaguez的自己新定义的函数一模一样.我推荐用memcpy_fromfs(.....)用于通用的数据传输而将plaguez的那个用于字符串拷贝.

    现在我们知道了如何将数据从用户空间传送到内核空间.但是相反方向该怎么办呢?这就有点难了,因为我们并不能很容易的在内核为用户空间分配内存.如果我们可以那样做的话,我们就可以用:

    #include

    void memcpy_tofs(void *to, const void *from, unsigned long count);

    来做实际的转换工作.但是我们如何在用户空间分配内存给*to指针呢?plaguez的文章里面给出了最好的解决方案:

    /*我们需要brk系统调用*/
    static inline _syscall1(int, brk, void *, end_data_segment);

    ...

    int ret, tmp;

    char *truc = OLDEXEC;

    char *nouveau = NEWEXEC;

    unsigned long mmm;

    mmm = current->mm->brk;

    ret = brk((void *) (mmm + 256));

    if (ret < 0)

    return ret;

    memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1);

    在这里使用了一个非常漂亮的小技巧.current是当前进程的任务结构(task structure)指针,mm是mm_struct(用于那个进程的内存管理的)的指针.通过对current->mm->brk使用brk系统调用我们可以增加数据段没有使用的内存的大小.我们都知道分配内存只不过是和数据段打交道.因此通过增加未用区域的大小,我们实际上为当前的进程分配了一些内存.这块内存就可以用于从内核到(当前进程的)用户空间的拷贝.

    你可能会对上面代码的第一行感到困惑.这行代码将帮助我们在内核空间使用用户空间的函数.每一个提供给我们的用户空间的函数(像fork,brk,open,read,write,......)都可以用一个_syscall(.....)宏来表示.因此我们可以构造某个系统调用宏来确切的表示特定用户空间的函数(用系统调用表示的);在这儿这个函数就是(brk.....)


    发布人:netbull 来自:it365