Linux入侵监测系统LIDS原理

发表于:2007-07-04来源:作者:点击数: 标签:
一、入侵 随着Internet上的Linux主机的增加,越来越多的 安全 漏洞在当前的GNU/ Linux系统 上发现。你也许在Internet上听说过 在Linux下发现 bug ,它会导致系统很容易的被黑客攻击。 因为Linux是一个开放代码的系统,漏洞很容易发现,并且也会很快的有补丁

  一、入侵
  随着Internet上的Linux主机的增加,越来越多的安全漏洞在当前的GNU/Linux系统上发现。你也许在Internet上听说过 在Linux下发现bug,它会导致系统很容易的被黑客攻击。
  
  因为Linux是一个开放代码的系统,漏洞很容易发现,并且也会很快的有补丁出来。但是当漏洞没有公布的时候,并且管理员很懒,没有去打补丁。黑客就会很容易的攻击这个系统,取得root权限,在现有的GNU/Linux下,他就可以做任何他想做的事情。现在你可以问,我们现在到底可以做些什么呢?
  
  1.1 现在的GNU/Linux错误在哪里?
  
  超级用户会滥用职权,他能够做所有他要做的事情。作为root。他会改变所有的东西。
  
  许多系统文件很容易被更改。这些文件可能是很重要的文件,如/bin/login,如果一个黑客进入,他可以上传一个login程序来覆盖/bin/login,这样他就可以不用登陆名和密码来登陆系统。但是这些文件不需要经常改动,除非你要升级系统。
  
  模块modules很容易用来中断内核。模块是为了让Linux内核更模块话和更高效而设计的。但是当模块加入到内核,它就会成为内核的一部分并且能做原始内核能做的工作。因此,一些不友好的代码可以写成模块来加入到内核里,这些代码就会重定向系统调用并且作为一个病毒来运行。
  
  进程是不受保护的,一些进程,如后台的web服务器,一直都认为是没有严格保护的程序。因此,他们就会很容易被黑客攻击。
  
  1.2 LIDS的设想是什么。
  
  保护重要文件。因为文件很容易被root更改,为什么不严格文件操作呢?因此,LIDS改变了文件系统在内核里的安全系统调用。如果某个时候一些人访问一个文件,他就会进入系统调用然后我们就可以检查文件名并且看她们是否被保护。如果它已经被保护,我们就可以拒绝这个访问者的要求。
  
  保护重要的进程。这个和上面的保护进程的想法不是一样的。当一个系统里运行一个进程,它会在/proc 文件系统里有一个用pid作为路径名的入口。所以,如果你用“ps –axf”你就可以显示出当前运行的进程。你可以问如果保护这些进程。如果你要杀死一个进程的话,首先,你键入“ps”来得到进程的PID,然后,你键入“kill 〈pid〉”来杀死它。但是,如果我不让你看到进程,你怎么来杀死这个进程呢?因此,LIDS是用隐藏进程来保护它的。
  
  另外一个重要的方法就是不让任何人可以杀死进程,包括root用户。LIDS能够保护父进程是init(pid=1)的所有进程 。
  
  封装内核。有时候我们需要要把一些必要的模块加入到内核里来使用,另外,我们也要拒绝任何人包括root用户向内核插入模块。那么如何来平衡这个矛盾的问题呢?我们可以只允许在系统启动的时候插入模块,然后我们封装模块,在封装后,内核不允许任何人插入模块到内核里。通过这种封装功能,我们能用它来保护重要的文件,进程,我们可以在系统启动的时候只允许必要的进程,只改变必要的文件。在封装内核后,我们就不能在对文件有任何的修改。
  
  二、保护文件系统
  2.1 保护文件系统是LIDS的重要功能之一。这个功能是在内核的VFS(虚拟文件系统)层实现的,我们可以保护任何种类的文件系统,如EXT2,FAT。
  
  在LIDS,保护的文件按种类分为以下几种:
  
  只读的文件或目录。只读文件意味着它们不被允许改写,如,在目录/usr/bin,/sbin。这些类型的文件大多数都是二进制系统程序或是系统配置文件,除了在升级系统的时候,我们不需要改变它们。
  
  只可增加文件或目录。这些文件是那些只可以增加大小的文件。大多数是系统的日值文件,如在/var/log里的只可增加文件。
  
  额外的文件或目录,这些文件没有被保护。一般来说,你想要保护目录下的所有文件,但是,还需要有一些特殊的文件不要被保护。所以我们可以定义这些文件作为额外的其他的只读文件。
  
  保护挂载或卸载文件系统。当你在启动的时候挂载文件系统的时候,你可以禁止所有人,甚至是root,去卸载文件系统。你也可以禁止任何人在当前文件系统下挂载文件系统来覆盖它。
  
  2.2 LIDS如何在内核保护文件
  
  在这部分,我们会看到一些内核的代码来理解LIDS是如何保护文件的。
  
  Linux文件系统数据结构程序
  首先,我们必须了解Linux的虚拟文件系统。
  
  在Linux里的每一个文件,不管是什么样子的,都有一个结点inode数,文件系统提供了以下数据结构。
  
  在/usr/src/Linux/include/Linux/fs.h
  
  struct inode {
  struct list_head i_hash;
  struct list_head i_list;
  struct list_head i_dentry;
  
  unsigned long i_ino; ----> inode number.
  unsigned int i_count;
  kdev_t i_dev; ----> device number.
  umode_t i_mode;
  nlink_t i_nlink;
  uid_t i_uid;
  ......
  }
  
  注意:用来鉴定一个结点inode。这个意思是你可以用一对来得到一个系统里独一无二的inode。
  
  在/ur/src/Linux/cinclude/Linux/dcache.h里
  struct dentry {
  int d_count;
  unsigned int d_flags;
  struct inode * d_inode; /* Where the name belongs to - NULL is negative */
  struct dentry * d_parent; /* parent directory */
  struct dentry * d_mounts; /* mount information */
  struct dentry * d_covers;
  struct list_head d_hash; /* lookup hash list */
  struct list_head d_lru; /* d_count = 0 LRU list */
  struct list_head d_child; /* child of parent list */
  struct list_head d_subdirs; /* our
  ......
  }
  
  dentry是一个目录文件的入口。通过这个入口,我们可以很容易的在文件的父目录下移动。
  
  例如,如果你一文件的inode是(struct inode*)file_inode,如果你可以用file_inode->d_entry来得到它的目录入口并且用file_inode->d_entry->d_parent来得到父目录的目录入口。
  
  LIDS保护数据结构
  在分析完Linux文件系统后,让我们来看看LIDS是如何容VFS来保护文件和目录的。
  
  在/usr/src/Linux/fs/lids.c
  
  struct secure_ino {
  unsigned long int ino; /* the inode number */
  kdev_t dev; /* the dev number */
  int type; /* the file type */
  };
  
  上面的结构用一对来存储保护文件或目录的结点。“type”是用来标明保护结点文件类型的。
  
  LIDS有4种类型
  在/usr/src/Linux/include/Linux/fs.h
  #define LIDS_APPEND 1 /* APPEND ONLY FILE */
  #define LIDS_READONLY 2 /* Read Only File */
  #define LIDS_DEVICE 3 /* Protect MBR Writing to device */
  #define LIDS_IGNORE 4 /* Ignore the protection */
  
  通过secure_ino结构,我们能很容易的初使化保护的文件或是在内核里执行以下函数。
  
  在/usr/src/Linux/fs/lids.c
  int lids_add_inode(unsigned long int inode ,kdev_t dev , int type)
  {
  
  if ( last_secure == (LIDS_MAX_INODE-1))
  return 0;
  
  secure[last_secure].ino = inode;
  secure[last_secure].dev = dev;
  secure[last_secure].type = type;
  
  secure[++last_secure].ino = 0;
  
  #ifdef VFS_SECURITY_DEBUG
  printk("lids_add_inode : return %d
  ",last_secure);
  #endif
  return last_secure;
  }
  
  就象你在上面代码上可以看到的,给secure_ino加到一个结点上是非常容易的。被保护的结点会在系统启动的时候初使化。初使化程序在/usr/src/Linux/fs/lids.c的init_vfs_security()里。
  
  现在,让我们看看LIDS是如何来检查是否一个结点已经受到保护。
  
  在/usr/src/Linux/fs/open.c
  int do_truncate(struct dentry *dentry, unsigned long length)
  {
  struct inode *inode = dentry->d_inode;
  int error;
  struct iattr newattrs;
  
  /* Not pretty: "inode->i_size" shouldnt really be "off_t". But it is. */
  if ((off_t) length < 0)
  return -EINVAL;
  
  #ifdef CONFIG_LIDS
  if (lids_load && lids_local_load) {
  error = lids_check_base(dentry,LIDS_READONLY);
  if (error) {
  lids_security_alert("Try to truncate a protected file (dev %d %d,inode %ld)",
  MAJOR(dentry->d_inode->i_dev),
  MINOR(dentry->d_inode->i_dev),
  dentry->d_inode->i_ino);
  .....................
  
  这个是LIDS加到内核里做检测的一个例子。你会看到lids_check_base()是LIDS保护方法的一个核心函数。
  
  你可以在LIDS要保护的地方看到很多LIDS保护方法用到lids_check_base()函数,特别是在Linux内核的子目录下。
  
  在/usr/src/Linux/fs/lids.c
  
  int lids_check_base(struct dentry *base, int flag)
  {
  ..................
  inode = base->d_inode; /* get the inode number */
  parent = base->d_parent; /* get the parent diretory */
  
  .................
  ----> do {
  if ( inode == parent->d_inode)
  break;
  if ((retval = lids_search_inode(inode))) {
  if ( retval == LIDS_IGNORE ||
  (retval == LIDS_DEVICE && flag != LIDS_DEVICE))
  break;
  if ( flag == LIDS_READONLY ||
  ( flag == LIDS_APPEND && retval >flag ) ||
  ( flag == LIDS_DEVICE && flag == retval )) {
  return -EROFS;
  }
  break;
  }
  inode = parent->d_inode;
  } while( ((parent = parent->d_parent ) != NULL) );
  
  return 0;
  }
  lids_check_base()会检查一个给定文件的dentry和它的父目录是否被保护。
  
  注意:如果它的父目录被保护,它下面的文件也会被保护。
  
  例如,如果“/etc/”被保护,“/etc/passwd”也一样被保护。
  
  在内核保护系统调用
  为了保护系统,LIDS会在一些检查临界的系统调用的时候做检查。因此,我们可以保护系统调用和限制文件系统的用户调用。
  
  这些是一些例子
  
  open(),open是通过禁止一些权利来保护文件的打开。 你可以在打开调用open_namei()调用的时候LIDS在检测它。
  mknod(),mknod是用来在指定目录下保护mknod。
  unlink(), 在内核代码检查do_unlink()。
  
  三、保护设备
  Linux的设备会在/dev/目录下以文件的形式列出,我们可以用上面保护文件的方法来保护设备。但是在一些情况下,用户也可以用IO操作来旁路文件系统来读写设备,我们必须注意这个问题。
  
  3.1 设备,内核I/O
  
  在GNU/Linux系统下的设备会以文件的形式表达,所以我们可以用保护文件系统那样来保护设备。
  
  用户的I/O访问是通过系统调用sys_operm和sys_iopl来实现的。你可以看看/usr/src/Linux/arch/i386/kernel/ioport.。这个是要基于系统结构的,要是到其他平台,就需要注意它们的变化。
  
  3.2 如何用LIDS来保护
  
  大多数情况下,程序不需要通过在/dev的设备文件名称来访问设备。但是,一些特殊的程序需要直接访问,如X Server,这个会写到/dev/mem和甚至是I/O设备。我们需要一些额外的东西来保护设备。LIDS会在配置内核的时候来定义这个功能。
  
  CONFIG_LIDS_ALLOW_DEV_MEM,如果你选择了开启这个功能,你就可以允许一些特殊程序来访问/dev/men和/dev/kmen这些内核临界的设备。如果你想要用内核的X Server,选择这个功能就会在配置内核的时候提供整个路径和文件名。
  
  CONFIG_LIDS_ALLOW_RAW_DISKS,如果选择这个开启,你就可以允许一些特殊的程序来访问物理磁盘。
  
  CONFIG_LIDS_ALLOW_IO_PORTS,如果你选择了开启这个功能,你就可以允许一些特殊的程序来访I/O端口。
  
  当系统运行fs/lids.c里的init_vfs_security()的时候初使化就被调用。
  
  #ifdef CONFIG_LIDS_ALLOW_DEV_MEM
  lids_fill_table(allow_dev_mem,&last_dev_mem,LIDS_MAX_ALLOWED,CONFIG_LIDS_DEV_MEM_PROGS);
  #endif
  
  #ifdef CONFIG_LIDS_ALLOW_RAW_DISKS
  lids_fill_table(allow_raw_disks,&last_raw_disks,LIDS_MAX_ALLOWED,CONFIG_LIDS_RAW_DISKS_PROGS);
  #endif
  
  #ifdef CONFIG_LIDS_ALLOW_IO_PORTS
  lids_fill_table(allow_io_ports,&last_io_ports,LIDS_MAX_ALLOWED,CONFIG_LIDS_IO_PORTS_PROGS);
  #endif
  
  如果一个进程或是程序要直接访问ip端口或是磁盘设备,LIDS就会检查它在数组 allow_raw_disk,last_io_ports,等)。这个检查是通过调用lids_check_base()里的lids_search_inode(inode)来实现的。
  
  如,让我们看看CONFIG_LIDS_ALLOW_DEV_MEM
  
  /* in lids_search_inode() */
  
  #ifdef CONFIG_LIDS_ALLOW_DEV_MEM
  for( i = 0 ; i < last_dev_mem ;i++ ) {
  if ( allow_dev_mem[i].ino == ino && allow_dev_mem[i].dev == dev) {
  return LIDS_READONLY;
  }
  }
  #endif
  #ifdef CONFIG_LIDS_ALLOW_RAW_DISKS
  
  在allow_dev_mem包括了哪一个程序结点在系统启动的时候在init_vfs_security()里初使化。用同样的方法,除了一些特殊程序,我们可以保护设备,I/O访问等等。
  
  四、保护重要进程
  进程是操作系统的动态入口。内核里有两个特殊进程,进程ID 0 (swapd) 和进程ID 1(init)。Init进程是在系统启动的时候所有进程的父进程。
  
  4.1 不可杀死的进程。
  
  就象你可以看到是否有人要夺得root特权一样,我们可以很容易的杀死那些该内核发送特别信号的进程。为了杀死一个进程,你必须得到进程的ID,然后用kill命令来杀死它。
  
  系统杀死进程的调用是kill,是在内核里的sys_kill()命令里的调用。
  
  让我们看看LIDS的保护代码
  
  在/usr/src/Linux/kernel/signal.c里
  
  asmlinkage int
  sys_kill(int pid, int sig)
  {
  struct siginfo info;
  
  #ifdef CONFIG_LIDS_INIT_CHILDREN_LOCK pid_t this_pid;
  int i;
  #ifdef CONFIG_LIDS_ALLOW_KILL_INIT_CHILDREN
  if (!(current->flags & PF_KILLINITC))
  #endif
  if (lids_load && lids_local_load && LIDS_FISSET(lids_flags,LIDS_FLAGS_LOCK_INIT_CHILDREN)) {
  this_pid = pid>0?pid:-pid;
  for(i=0;i if( this_pid == lids_protected_pid[i]) {
  lids_security_alert("Try to kill pid=%d,sig=%d
  ",pid,sig);
  return -EPERM;
  }
  }
  }
  #endif
  ...
  }
  你可以在内核里看到两个标签,,CONFIG_LIDS_INIT_CHILDREN_LOCK 和CONFIG_LIDS_ALLOW_KILL_INIT_CHILDREN.
  
  在CONFIG_LIDS_INIT_CHILDREN_LOCK的开启状态,LIDS能保护初使的运行程序。如,如果你在系统里运行inetd程序,你可以在隐藏内核前运行它,然后,你还可以杀死它。但是一些人如果telnet到你的机器,inetd就会创造子进程来为用户服务,这个子进程不会被LIDS保护,因为用户在任何时候退出和杀死程序。
  
  4.2 隐藏进程
  
  另外一个保护进程的方法就是隐藏进程。当一个黑客危机你的系统。他会登陆,然后会看看有没有一些已知的进程在监视它。然后他就杀死它。如果你隐藏了这个功能的进程,黑客就不会知道进程的所有情况并且你可以记录他在你系统上做的任何事情。
  
  如何隐藏进程
  为了隐藏进程,你必须在配置内核的时候提供一个完全的路径名。
  
  当内核启动的时候,LIDS会访问文件结点到一个叫proc_to_hide[]的结构里。
  
  在include/Linux/sched.h里
  
  #ifdef CONFIG_LIDS_HIDE_PROC
  #define PF_HIDDEN 0x04000000 /* Hidden process */
  #endif
  
  /* in fs/lids.c */
  
  #ifdef CONFIG_LIDS_HIDE_PROC
  struct allowed_ino proc_to_hide[LIDS_MAX_ALLOWED];
  int last_hide=0;
  #endif
  ....
  
  /* in fs/lids.c , init_vfs_security(),
  fill up the hidden process in proc_to_hide[]
  */
  #ifdef CONFIG_LIDS_HIDE_PROC
  lids_fill_table(proc_to_hide,&last_hide,LIDS_MAX_ALLOWED,CONFIG_LIDS_HIDDEN_PROC_PATH);
  #endif
  
  PF_HIDDEN是否用户可以用显示进程的命令(如“ps –a”)来显示和检查进程,如果一个进程被LIDS隐藏,当他执行的时候,进程就会得到一个PF_HIDDEN的属性。然后,当系统输出系统进程信息到用户的时候,它就会可以检查当前输出进程是否有PF_HIDDEN标志。如果发现了,它就不会输出这个进程的信息。
  
  在in fs/exec.c
  
  int do_execve(char * filename, char ** argv, char ** envp, struct pt_regs * regs)
  {
  ...
  if (retval >= 0) {
  #ifdef CONFIG_LIDS_HIDE_PROC
  if (lids_search_proc_to_hide(dentry->d_inode))
  current->flags |= PF_HIDDEN;
  ...
  
  因为每一个Linux的进程都有一个在/proc文件系统的入口,我们为了隐藏进程也需要修改proc的文件入口。
  
  在fs/proc/root.c
  
  static struct dentry *proc_root_lookup(struct inode * dir, struct dentry * dentry)
  {
  ...
  inode = NULL;
  
  #ifdef CONFIG_LIDS_HIDE_PROC
  if ( pid && p && (! ((p->flags & PF_HIDDEN) && lids_load && lids_local_load)) ) {
  #else
  if (pid && p) {
  #endif
  unsigned long ino = (pid >> 16) + PROC_PID_INO;
  inode = proc_get_inode(dir->i_sb, ino, &proc_pid);
  if (!inode)
  return ERR_PTR(-EINVAL);
  inode->i_flags|=S_IMMUTABLE;
  }
  ...
  }
  
  然后如果进程被PF_HIDDEN标记,它就不会在proc文件系统里显示。

原文转自:http://www.ltesting.net