当前位置:Linux教程 - Linux - Linux内核即时入侵检测安全增强

Linux内核即时入侵检测安全增强

前言
一.介绍
二.系统调用运行步骤
三.防止缓冲区溢出的内核补丁
四.实现
五.后语


前言
冰块


  在计算机制造业中,只要涉及到计算机安全性要求非常重要高的地方,总要提起入侵检测和如何处理黑客的入侵。在这个方面的主要讨论焦点是:在入侵者进行完成入侵后发现并记录入侵,还不如在他们入侵之前就发现并阻止他们对系统的入侵。我们可以检测系统每一个进程,这样我们就可以在入侵者实行入侵攻击的时候实现入侵的防御,因为这样就可以即时阻止恶意的系统命令的执行。实际上,最严重的破坏都是通过在被攻击系统上执行的恶意系统调用带来的。

  这篇文章提供了设计和实现这种对敏感的系统调用的监视功能。我们的目的主要是在类UNIX系统内核上通过截获系统调用来实现这种功能。这个系统的综合功能可以嵌入到内核中的系统调用代码,这样在系统调用被引用的时候就可以检查系统调用是否符合访问控制数据库所定义的规则。

  这里实现的方法是通过一个简单的方法来截获系统调用和他们的参数值。这样做不用改变内核已有的数据结构和算法。在对所有对程序进程的修正和改变后,他们还可以继续稳定的运行,不用改变源代码和重新编译内核。

  这里还提供了一个完整的能够检测和阻止缓冲区溢出攻击的代码,这段代码可以作为一套Linux的操作系统的内核源代码补丁。


一.介绍

现在大家已经公认了,世界上没有绝对的方法或绝对安全的系统来阻止黑客的入侵:

没有漏洞的软件对我们来说还是一个梦想。即使是很流行的程序或是操作系统一样会包含很都可以被入侵者利用的漏洞。
许多软件包的行为都是和其他操作系统的组件(函数库或是内核)相互作用的。
  由于这个原因,所以入侵检测和如何处理入侵行为在计算机工业里一直在广泛的讨论和研究。

  很多现有的入侵检测的技术都是基于一些对审计和日志文件的分析。主要思想是搜索整个系统的文件和参考列表来发现畸形的,不期望的系统配置的改变。(如,有一个新的用户的用户ID是0)。另外一种方法就是周期性的执行一个程序来做系统文件的属性和参考列表的比较。这些过程主要是来对付Trojan木马的。这样的程序是看起来对系统不是很危险,但是可以让入侵者得到控制整个系统所需的权限。

  这样程序的好处是不用进入内核部分。如,他们不用进行对系统命令的修改。但是,他们也不能对入侵进行即时的防范,因为,大多数情况下,这些结果都是在入侵者攻击以后才生成的。

  因为所有对系统的破坏性攻击都是通过一些敏感的系统调用来实现的。所以,即时的入侵检测通过监视每一个系统调用就可以保护你的系统不受攻击者破坏。

  这里我们建立一个在内核级能够实现截获和监视特殊系统调用的函数。我们的方法需要最小的改变系统原来内部的结构。由于类UNIX系统的这个代码都是公开的,我们就可以基于公开的内核代码来截获系统调用,并根据我们设定的规则来检验系统调用是否是恶意的,起到入侵检测和防范的功能。

  这个软件的主要设计目的如下:

在恶意的系统调用执完全行前检测到它,以起到阻止恶意的入侵攻击。
允许对系统调用参数的一个有效的检测。
在不改变系统已经存在的数据结构和体系的情况下,通过扩展内核的功能来实现一个增强的安全的操作系统。
感谢即时的入侵检测,因为原来的扩展操作系统可以让入侵者的恶意进程和合法的进程一起运行。有了这个系统,我们就可以对系统的所有进程进行分析,发现恶意的进程以及他们之间的关系。
  作为一个实现我们技术的例子我们创建了一个在攻击完成前对缓冲区溢出攻击进行检测的原形。一个基于操作系统的入侵检测的实现能在检测缓冲区溢的时候的提供一些必要的功能选项。


二.系统调用运行步骤

我们的系统安全增强是建立在一个内核后面的参考监视器上的。

  用户的进程要想访问系统的调用就必须经过这个参考的监视器。如图1。这个系统调用参考的监视器包括两个重要的功能部分:参考功能函数和认证功能函数。参考功能函数是用来生成一个结果,这个结果是关于是否允许或是拒绝一个基于访问控制数据库系统调用请求的。这个访问控制数据库概念性的包括入口和访问控制的规则(规则涉及进程,系统调用,访问模式)。在ACD中的访问控制规则能捕捉在系统调用和他们相关参数的情况。举例,一个execve系统调用的控制规则可以在executable文件列表中指定访问的模式来控制调用这个进程的调用execve的系统调用。这个文件就是调用进程对execve的合法通行凭证。这样的功能可以允许阻止一个有特权的没有正确注册的进程(如,setuid)如交互的shell对系统的非授权攻击或是滥用。

  ACD不是参考监视器的一部分,但是对这个数据库的所有控制都是通过参数监视器来控制的。这就是我们说的第二个组件,认证功能组件。

  这个组件是用来监视对个别访问控制规则改变的。参考监视器的一个基本的规则就是全部的,也就是所有的访问都必须在这里调停。然而,一般方法在系统调用的高端级别来实现这个功能会有很到的资源的消耗。大约会对系统调用有30%的资源浪费。为了减少这样的消耗,我们提出了一个更加有效率的方法,就是在基于拦截系统内核的系统调用来实现这个参考监视器的功能。我们这个方法能尽量少的改变本地操作系统,也不会改变操作系统内部的官方接口。如类UNIX系统,还有其他的一些公开源代码的操作系统(如,Linux、xxBSD、Solaris)。我们的目的是建立一个参考的监视妻来截获系统调用,并把截获到的系统调用和访问控制数据库进行比较,来确定是不是合法的系统调用和参数。

  因为这样的方法是在系统调用生效前就截获的,所以他可以阻止任何由入侵者对系统非法的恶意的系统调用。从而起到保护系统的功能。


三.防止缓冲区溢出的内核补丁

这一部分我们来描述一个对系统安全有很大危害的缓冲区溢出的Linux内核补丁。

3.1 缓冲区溢出漏洞攻击

  在C语言中对队列资源的限制很容易让一个内存溢出。有很多广为人知的方法来利用缓冲区溢出的漏洞来对系统进行攻击。他们可以利用缓冲区溢出的漏洞来向系统内插入一些恶意的系统命令操作,就可以得到一个特权的shell,从而对系统进行控制。主要就是利用攻击由root或是administrator运行的程序,插入一些命令,而这些命令就有了root的运行权限。

  一个root的进程可以划分为以下的几种类型:

交互式的:这是一个标准的超级用户进程。用户ID(UID)和有效用户ID(SUID)都为0。对于这个用户没有什么权限问题,因为用户已经取得了对系统的所有控制权限。
后台:这个主要是指在系统启动的时候生成的root用户级别运行的程序后台进程。虽然对这方面的技术比较复杂,但是大多数时候也可以利用来进行缓冲区溢出漏洞的攻击。
Setuid进程:这样的进程有一队用户的标记(UID,EUID),并且值都应该是大于-的。所以一个setuid到root的进程表明下面的一个宏定义。
  #define IS_SETUID_TO_ROOT(proc) !((proc)->euid)&&(proc)->uid

  对于这个宏不同的系统可能有不同的定义。

  在下面的内容里,我们将会讨论在内核中什么样的root进程可以被马上识别出来。

3.2 后台的root程序

  只有一个UNIX系统才会有root程序在后台运行。大多数情况下,系统管理员不会去直接开启他们也不会控制他们的执行,所以,象我们前面提到的一样,这样的特权程序就会被当作缓冲区溢出漏洞攻击的主要目标。大概可以分成下面几种类型:

后台程序都是在系统系统的时候直接由初始化脚本运行的。就象网络服务器中的inetd超级服务器程序那样,里面有web服务器,mail服务器(主要是sendmail)就是利用这样的方式来启动的。另外一个这样的例子就是syslogd后台程序。
网络服务器由inetd超级服务器启动来履行如远程访问(telnet),文件传输(ftp)等等的服务。
程序执行启动一个特定的后台程序,大多数都是在系统启动的时候运行。(属于类型级别1。)
程序是在未来的特定时间用at命令来启动的。实际上这些程序可以认为是特定类型的级别分类。
程序是在交互的会话时刻在后台运行的。这些都是某些特定的原因才应用的。
  这些程序一般来说都没有一个控制终端。为了告诉它们不在程序交互的模式下运行,我们可以利用下面的宏定义。

  #define IS_A_ROOT_RAEMON(proc) !((proc)->euid)&&((proc)->tty==NULL)

  这里,我们首先检查程序是否是以root级别来运行的,然后我们检查这个程序是否有一个控制终端。

3.3 系统调用的工具

  为了防止危险的缓冲区溢出漏洞的攻击,有一些简单的简单代码加入到以下的几个系统调用中去。

Execve(executable-file,…):这个系统调用允许一个嵌入的setuid进程来调用一个交互的shell外壳。为了阻止这样的调用和其他的简单攻击,我们定义了一个原始的检查,来确定这个调用的进程是不是拥有root的权限。如果没有,就不再继续进行检查,这个系统调用可以继续运行,就忽略截取系统调用的其他动作。如果调用的系统调用有root的权限,进一步的,setuid为root,那么这个访问控制数据库就要决定证明是否这个参数executable-file是否可以被setuid进程调用。如果接受了的话,这个调用就继续进行。除非这个调用被入侵处理的子系统禁止抛弃。其他情况下,这些调用的情况都会记录在一个日志文件里面。
Setuid:我们认为任何一个setuid为root或是在后台以root权限的进程都是可以作为一个潜在的攻击目标。对这样的进程我们用特定的检查程序来过滤这些系统调用。一个setuid的进程是可以在交互模式下,在调用其他避免运行setuid进程的系统调用之前来运行setuid(0)调用的。通过这样的方式,我们可以跳过所有的我们介绍过的检查。因为这个原因,我们必须加载一段代码来补充setuid系统调用来阻止一个setuid进程得到root的UID权限。
Chmod:当我们调用一个setuid的进程。用户可以应用Chmod来取得对敏感文件的写权限。(如,密码文件)。阻止这样的和其他相关的攻击,一个检查代码加到chmod系统调用代码里来阻止一个setuid进程对一般文件或目录访问权限的修改。
Chown:当调用一个setuid进程的时候,chown可以用来改变一个可执行文件的拥有者为root。Chown是一个非常的调用,尤其是当他和chmod一起应用的时候。基于这个原因,我们也加入了一段检查的代码到chown系统调用里面去,来阻止一个setuid进程修改常规文件或目录的拥有权。
Chgrp:和chown一样,来利用chgrp改变组用户来取得一定的访问权限来攻击系统。


四.实现

  这一段我们来实现我们刚才提出的在linux系统下建立参考监视器的设想。开始我们将描述一个访问控制功能函数,这个功能函数包括增加到内核的访问控制数据库(ACD)的数据结构定义。这个新的系统调用来读,写和升级ACD和参考功能函数。还会附有check_rootproc的代码。

4.1 认证功能函数

  访问控制数据库包括一个关于每一个系统调用的参考监视器。在这里,只有两个主要的数据结构起作用,一个叫做setuid_acd用来检查对setuid的系统调用,一个是execve_acd来检查对execve的系统调用。这两个结构都在下面的图2中列出。

  /*setuid_acd*/

  static char rpasswd[LEN_PWD];

  /*execve_acd*/

  typedef struct setuid_proc_id{

  char comm[16];

  unsigned long count;

  }suidpid_t;

  typedef struct setuid_program{

  suidpid_t suidp_id;

  suidp_t *next;/*下一个程序*/

  }suidp_t;

  typedef struct exe_file_id{

  __kernel_dev_t device; /*设备号码r*/

  unsigned long inode; /*inode结点号码*/

  __kernel_off_t size; /*大小*/

  __kernel_time_t modif: /*修改时间*/

  }efid_t;

  typedef struct executable_file{

  efid_t efid;/*文件鉴定时间信息*/

  int prog_nr; /*可以调用exe的程序数量*/

  suidp_t *programs; /*认证程序列表*/

  }efile_t;

  typedef struct executable_file_list{

  efile_t lst[NR_EXE];

  unsigned int total; /*在列表里的exe的总数*/

  }eflst_t;

  图2

  Setuid_acd只包括串rpasswd,用来保存在内核存储的加密root密码。这是用来健壮性的对

  setuid系统调用认证进行实现的。

  Execve_acd包括两个eflst_t结构的数组:

  Admitted:在这个结构里提供了一个入口给可执行的文件F,一个setuid程序需要通过执行F来调用execve。在入口里存储了所有的调用F的setuid程序的列表。

  Failure:这里保存了一些没有得到认证的利用setuid进程来调用execve尝试的日志。

  下面列出了一个admitted数据结构,这个结构是一个有关于可执行文件的列表和指向一系列setuid程序的数组。

  Failure数据结构没有在这里列出,它和admitted数据结构差不多,但是它会动态的增长,记录那些非授权的setuid程序的访问。

  每一个admitted数据结构的元素包括下面三个域:efid,proc_nr和programs.

  Efid标明可执行文件F。存储在efid的信息为:

  Device 这个是F的文件系统的设备号码;

  Inode 文件F的inode号码;

  Size 文件F的字节长度;

  Modif 这里保存了对文件F的最后修改时间;

  这里的device和inode能够唯一独立的标明一个系统文件F。size和modif允许来检测非授权的文件内容修改。

  Proc_nr:这个域来标明程序列表的长度,这个列表是可调用文件F的setuid程序的列表数量。

  Programs:是一个指向setuid程序的一个指针,每一个元素,叫做suidp_id,包含两个域:comm和count。Comm保存了在setuid程序的名称的一个备份。域count是用来统计和指出在F文件上的调用数量的。

  在下面的阶段里,我们描述sys_setuid_aclm,这是个新的系统调用,只能被UID=0和EUID=0的root级进程才可以调用的系统调用。Sys_setuid_aclm的目的是用来实现对存储在ACD里面的信息进行读和修改的功能的。

  由于root进程可以访问ACD数据库,这样就会出现冲突。因此我们的定义了一个原始的增强程序来处理这些冲突的程序。一般情况下,一个叫做write_pid来实现这个互斥。为了避免对write_pid自身的竞争,这个变量必须可以检测和自动升级。这个功能可以借助调用atomic_access,这个调用是来实现intel体系结构的自动改变的:xchg。

  实际上,sys_setuid_aclm系统调用从头到尾经历了六个不同的操作,以下就是对这六个操作的描述:

  PUT(exe-file,suid-prog,list)增加(exe-file,suid-prog)组合对到特定的ACD列表中,可能返回的值有:

  PUTEP (exe-file,suid-prog)组合对已经成功的加入;

  PUTPI exe-file已经在ACD中出现,只有suid-prog被加进ACD中。

  PUTAE (exe-file,suid-prog)组合对已经在ACD中出现,没有任何的操作执行。

  PUTFULL 数据结构已经满;对NR-SUID-EXE的限制已经溢出,升级终止退出。

  PUTBUSY ACD数据库忙,另外一个进程正在升级。

  GET(exe-file,suid-prog,param)从param-list中读出(param-file-nr,param-proc-nr)组合对。可能的返回值为。

  GETOK 这个对已经成功的读出,并且不是列表中的最后一个。

  GETL最后一对组合已经关联文件param.file-nr;

  GETLACL最后的在ACD中的对已经读出。

  GETOB越出边界,没有这样的组合对(param.file-nr,param.proc-nr);

  GETBUSY ACD数据库忙,另外一个升级进程正在运行。

  PUTHEADERACL(header-acl)在内核内存区存储ACD的头部。这个对于在补丁安装后系统的第一次启动运行很重要。如果这个服务不可行,就会返回PUTBUSY。

  GETHEADERACL(local-header-acl)在内核区域重新找访问控制数据库的头部,并且存储在变量local-header-cal中。

  DELETE(exe-file,suid-prog,list)在特定的列表中删除(exe-file,suid-prog)组合对。如果exe-file为NULL,就会删除在(*,suid-prog)中所有的组合对,就是用它来阻止suid-prog执行其他的文件。

  PUT-PWD(param)写密码文件param->passwd到内核内存区域。它会删除在内核区域和用户区域的密码拷贝。

  系统管理员(root用户)可以通过一些新的命令来管理访问控制数据库汇写到sys_setuid_aclm系统调用中,它叫做aclmng,它有以下的功能选项:

  -l 列出保存在内核区域的访问控制数据库的内容。

  -L 从文件/etc/bop/acd中引导数据到访问控制数据库,大多数都用在启动时刻。

  -w从文件/etc/bop/acd中写数据到访问控制数据库,大多数都用在系统关闭的时刻。

  -h 显示功能信息;

  efault 如果没有选项,默认执行-l选项。

4.2 函数参考

  /*

  *sys_execve() executes a new program.

  */

  int do_execve

  (char * filename,char **argv,char **envp,struct pt_regs *regs){

  ......

  dentry=open_namei(filename,0,0);

  retval=PRT_ERR(dentry);

  if (IS_ERR(dentry))

  return retval;

  ......

  retval=prepare_binprm(&bprm);

  /*************BUFF OWERFLOW PATCH**********************/

  rc=check_rootproc(bprm.dentry->d_inode);

  if ((rc==EXENA)||(rc==EFNA)){

  printk(BOP_LEVEL""BOP kernel:do_execve psuid %s no

  authorized to exec file %sn"",current->comm,filename);

  printk(BOP_LEVEL""by euid %d uid %dn"",

  current->euid,current->uid);

  if (rc==EXENA)

  printk(BOP_LEVEL""EXE NO AUTHORIZEDn"");

  else printk(BOP_LEVEL""EXE NO AUTHENTICATEDn"");

  return rc;

  }

  /*****************************************************/

  .......

  if (retval>=0)

  retval=search_binary_handler(&bprm,regs);

  if (retval>=)

  /*execve success*/

  return retval;

  .......

  }

  图3

  /*

  *kernel/sys.c

  */

  int setuid(uid_t uid){

  if (suser()) { /*if euid==0*/

  #define IS_SETUID_TO_ROOT(current)){

  read(unencrypted);

  get_pw(correct);

  encrypted=crypt(unencrypted,correct);

  memset(unencrypted,0,strlen(unencrypted));

  if(strcmp(encrypted,correct)){

  printk(BOP_LEVEL""Error in setuid from uid %d n"",current->uid);

  return -EPERM;

  }else /* ok auth .User root,set uids*/

  }else /*non setuid-root*/

  ..........

  }

  图4

  这一节我们介绍图3中的系统调用的函数功能。

  Execve 在图3,我们加了一段程序代码到里面。Check_rootproc()函数检查调用execve这个系统调用的进程是否符合访问控制数据库定义的规则。系统调用会在check_rootproc返回下面两个值的时候终止:

  EXENA:调用的进程不能得到调用该系统调用的请求认证。就是说,请求调用的进程没有得到在访问控制数据库规则允许的列表中。

  EFNA:调用的进程得到运行的认证,但是文件没有得到认证,如,修改时间或是文件大小不匹配。

  在后语里我们会提供一个check_rootproc函数的细节。如果调用的进程没有用root级别运行EUID=0的话,就不对进程的进行其他的检查了。除非这个进程可以通过访问控制数据库的认证。

  Setuid 图4显示了setuid的系统调用的代码。这个调用的认证和execve差不多。一个运行要想调用setuid(0)来得到uid=0的程序,必须要键入root的密码。这个密码保存在访问控制数据库中。如果密码不匹配,进程就会被禁止。就象我们经常用的su命令一样,必须键入root密码才可以获得一个root级别的shell。

  /*

  *fs/open.c

  */

  asmlinkage int sys_chmod(const char *filename,mode_t mode)

  {struct dentry *dentry;

  struct inode *inode;

  int error;

  struct iattr newattrs;

  lock_kernel();

  dentry=namei(filename);

  /****************BUFFER OWERFLOW PATCH**************/

  #define IS_SETUID_TO_ROOT(proc) !((proc)->euid)&&(proc)->uid

  #define S_ISREG(dentry->d_inode->i_mode)||S_ISDIR(dentry->d_inode->i_mode))){

  printk(BOP_LEVEL""BOP proc %s failed changing mode:n"",current->comm);

  printk(BOP_LEVEL""inode %d device %dn"",

  (int) dentry->d_inode->i_ino,(int) dentry->d_inode->i_dev);

  printk(BOP_LEVEL""uid %d gid %dn"",

  dentry->d_inode->i_uid,dentry->d_inode->i_gid);

  printk(BOP_LEVEL""from %d to %dn"",dentry->d_inode->i_mode,mode);

  return -EPERM;

  }

  /**********************************************/

  图5

  图5显示了在chmod系统调用中另外加的一段代码。这里和setuid的区别就是这里不需要root密码的认证。

  代码chown()和chgrp系统调用的增加和chmod类似。

五.后语

  这篇文章的整个补丁代码我没有找到,可能是因为这个人关于这个东西的开发已经放弃了吧。呵呵,如果谁能找到。请告诉大家,原来他们提供的url是:

www.iac.rm.cnr.it/tecno/software/indexs.html
  这篇文章和以前发表的LIDS文章都是关于内核模块级的系统安全的。这篇主要是对缓冲区溢出的漏洞进行防范。虽然没有代码的原形,但是它给出的几个代码事例能够帮助我们更好的理解Linux的内核模块如何截获系统调用,或是如何对进程运行权分析的实现。对于我们理解Linux和其他操作系统的内核有一定的帮助。希望大家能通过这篇文章能多提高点理解认识,对以后自己开发系统安全软件能有一定理论上的帮助作用。下面是check_rootproc函数的代码:

  /*check_rootproc.c*/

  int check_rootproc(struct inode *ino){

  int cont=0,iproc=0,error=0;

  suidp_t * suidproc;

  efile_t f;

  suidp_t p;


  if ((IS_SETUID_TO_ROOT(current))||(IS_A_ROOT_DAEMON(current))) {

  for (;cont
  if ((permitted.lst[cont].efid.device==ino->i_dev&&

  permitted.lst[cont].efid.inode==ino->i_ino)){

  if ((permitted.lst[cont].efid.size==ino->i_size)&&

  permitted.lst[cont].efid.modif==ino->i_mtime)){

  suidproc=permitted.lst[cont].processes;

  for(iproc=1;iproc<=permitted.lst[cont].proc_nr;iproc++){

  if(!strcmp(suidproc->suidp_id.comm,current->comm)){

  suidproc->suidp_id.count++;

  return PSA;

  }

  if (iproc
  suidproc=suidproc->next;

  }

  }

  }else{

  error=EFNA;

  goto file_exe_unauthorized;

  }

  }

  }

  error=EXENA;/*EXE is not in the database*/

  goto file_exe_unauthorized;

  }

  return PNS;/*the process is not setuid to root or root daemon*/


  file_exe_unauthorized:

  f.efid.device=ino->i_dev;

  f.efid.inode=ino->i_ino;

  f.efid.size=ino->i_size;

  f.efid.modif=ino->i_mtime;

  strncpy(p.suidp_id.comm,current->comm,

  sizeof(p.suidp_id.comm));

  p.suidp_id.count=1;

  do{

  while(writer_pid!=0){

  cli();/*interrupt disabled*/

  if (writer_pid!=0)

  interruptible_sleep_on(&pid_queue);

  sti();

  }

  }

  while(!atomic_access(&writer_pid,current->pid));

  /*start of critical section*/

  do_setuid_put(&(f.efid),&(p.suidp_id),FAILURE);

  writer_pid=0;/*end of critical section*/

  atomic_access(&writer_pid,0);/*release of the lock*/

  return error;


  }