网络安全 频道

玩转freebsd内核模块

注:文章很老的,linux大家都熟了,相对来说bsd少了很多,但是原理还是一样的,这篇文章还是相当不错的,有助于
kernel hacking in freebsd :) 献给我即将开始的4年级的最后几门考试.....
文中提到的Curious Yellow可以在xfocus找到。

1. 介绍
1.1. 内核模块
1.2. 一些有用的函数

2. 方法
2.1. 替换函数指针
2.1.2. 系统调用
2.1.3. 其它的表
2.1.4. 单一的函数指针
2.2. 修改内核空间的队列
2.3. 读写内核空间
2.3.1. 查找符号的地址
2.3.2. 读数据
2.3.3. 修改内核数据

3. 应用
3.1. 隐藏并重定向文件
3.2. 隐藏进程
3.3. 隐藏网络连接
3.4. 隐藏防火墙规则
3.5. 触发器
3.6. 隐藏模块
3.7. 其它的应用

4. 内核补丁
4.1 介绍
4.2 插入跳转指令
4.3 替换内核代码

5. 越过重启

6. 实战

7. 保护自己:猫和老鼠的游戏
7.1. 检查符号表
7.2. 构件一个陷阱模块
7.3. 重新直接得到数据
7.4. 注意事项

8. 结论

9. 代码

10. 参考

11. 感谢
---------------------------------------------

1. 介绍
首先介绍内核模块的概念,还有系统调用的概念,说明的一点就是freebsd安全级别问题,通常在2级就不可以加载模块了
可以用sysctl 调整设置或者在/etc/rc.conf中增加如下条目在启动时调整:
kern_securelevel_enable="YES"
kern_securelevel="2"
本文only用来教育目的,:)所有涉及的代码都可以在Curious Yellow (CY)中找到.

1.2. 内核模块
请参考<Dynamic Kernel Linker (KLD) Facility Programming Tutoria> scz@nsfocus 前辈翻译的<FreeBSD 4.0 动态
内核链接机制(KLD)编程指南>,如果你对linux的lkm了解,这个很好理解。在/usr/share/examples/kld/ 有简单的例子。

1.2 一些有用的的函数
这里给出一些有用的函数,通常在系统调用中用到copyin/copyout/copyinstr/copyoutstr 这几个函数可以用来从用户空间得到
连续的大块数据,manpage copy(9)可以得到更多了解,在KLD tutorial也可以找到
下面是个小例子来展示copyin的用法,我们构造了一个带有一个字符串指针做参数的系统调用,通过copyin把字符串从用户空间移动
到内核空间
struct example_call_args {
char *buffer;
};

int
example_call(struct proc *p, struct example_call_args *uap)
{
int error;
char kernel_buffer_copy[BUFSIZE];

/* copy in the user data */
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
struct sysent sysent[] = {
{ 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表.
struct ipprotosw intesw[]保存了一系列被支持的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,这就取决你的目的了,kernel source 是最重要的文档

2.1.3 单个的函数指针
偶尔我们也会碰到单个的函数函数指针,比如说ip_fw_ctl_ptr,这个函数用来处理ipfw的请求,这里我们也可以用来hook。

2.2. 修改内核队列
替换函数不够有意思呀:),也许你想修改内核中的一些数据,一些感兴趣的东西都以队列的形式存储在内核中,如果你从来没有
使用过/sys/sys/queue.h的一些宏,你先要熟悉一下它然后在进行下面的阅读。这可以让你轻松面对下面的kernel source
并且在你使用这些宏时不会出错。

一些感兴趣的队列
进程队列:struc proclist allproc 和 zombproc 也许你并不想修改这的东西因为进程调度的目的,除非你想重写大部分的
内核代码,但是你可以过滤它当有用户请求时。

linker_files队列:这个队列中包括了连接到了kernel的文件,每个文件可以包含多个模块,它的描述可以在这里找到(THC art
icle)这篇文章的连接是http://www.thehackerschoice.com/papers/bsdkern.html),自己找吧。:)这个队列非常重要
当我们改变符号的地址,或者隐瞒这个文件所包含的模块。

模块队列:module list_t 这个队列包含了加载的内核模块,注意这个模块队列区别于linker_files队列,这对于隐藏模块很重要

还是那句话,最好的文档就是kernel source

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(struct proc *p, register struct open_args *uap)
{
char name[NAME_MAX];
size_t size;

/* get the supplied arguments from userspace */
if(copyinstr(uap->path, name, NAME_MAX, &size) == EFAULT)
return(EFAULT);

/* if the entry should be hidden and the user is not magic, return not found */
if(file_hidden(name) && !(is_magic_user(p->p_cred->pc_ucred->cr_uid))) //检查特定文件名和用户uid
return(ENOENT);
return(open(p,uap));
}

还有一些类似的系统调用,只有getdirentries有一些特别,因为它返回一个目录列表,所以要多做一些变换(这个以前引起了不少的
讨论,在linux lkm中)。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的同时,也要改变相应的cached lookup 函数(因为有缓存呀,找的时候先找缓存)
下面展示了一个例子 代码来自module/file-ufs.c

int new_ufs_lookup(struct vop_cachedlookup_args *ap)
{

0
相关文章