当前位置:Linux教程 - Linux文化 - 恶意的Linux内核模块是如何工作的

恶意的Linux内核模块是如何工作的


如果在入侵事件调查中,传统的工具完全失效了,你该怎么办?当我在对付入侵者已经加载的内核模块时,就陷入了这种困境。由于从用户空间升级到了内核空间,LKM方式的入侵改变了以往使用的入侵响应的技术。一旦内核空间遭破坏,影响将覆盖到整个用户空间,这样入侵者无须改动系统程序就能控制他们的行为。而用户即使将可信的工具包上传到被入侵的主机,这些工具也不再可信。下面我将揭示恶意的内核模块如何工作,并且给出一些我开发的对付此类入侵的工具。

LKM概述

LKM的存在对系统管理员是个福音,对入侵检测却是个噩梦。LKM最初被设计用来无须重新启动而改变运行中的内核,从而提供一些动态功能。动态内核提供了对诸如新文件系统类型和网卡等设备的额外支持。此外,由于内核模块能够访问内核的所有调用和存储区,它能不受控制地改动整个操作系统的各个部位,因而所有调用和内存常驻的结构都有被恶意内核模块修改的危险。

LKM的一个臭名昭著的例子是knark。一旦knark编译并加载到入侵主机,将改变系统调用表从而改变操作系统的行为。系统调用表常驻在内核空间,基本上是提供给用户级别程序访问操作系统的入口。大多数Unix系统在手册的第二部分给出syscalls的正式定义。一旦内核作为用户空间运行,OS将把命令行上运行的所有命令和调用映像到系统调用表中。因此当knark改变系统调用表时也就改变了用户命令的执行。knark改动了以下的重要系统调用。

* getdents - 获得目标路径的目录项内容(即文件和子目录)。通过修改这个调用,knark实现对用户程序隐藏文件和目录。

* kill - 向进程发送信号,通常是杀掉进程。修改过的调用将使用无用的信号31,触发设置进程为"hidden"状态。当进程在hidden状态时,它在/proc中的纪录被删除,从而实现了对ps命令隐身。信号32被用来解除隐藏状态。

* read - 读取目标文件的内容。knark通过修改此调用实现对netstat隐藏入侵者的连接。

* ioctl - 改变文件和设备的状态。通过修改此调用,knark能够隐藏网卡的混杂位,同时在调用中插入了隐藏文件的函数。

* fork - 派生新进程。knark修改用来隐藏一个隐藏的父进程所派生的所有子进程。

* execve - 执行一个程序。每次用户在命令行下输入命令时调用。一旦此调用被劫持,内核模块可以控制命令的选择和运行。knark使入侵者可以把一个程序指向另一个,如同符号连接一样,而不留下罪证。knark控制了execve后,任何你希望执行的程序都有可能是入侵者的替代品。

* settimeofday - 设置系统时间。knark用来监控预定的时间。当这些预定时间之一被送给此系统调用时,knark可以触发某些管理任务或者立即赋予当前用户root的用户和组id。这样就无需更改到suid的shell而直接获得root权限。

由于系统调用被更改,那些管理工具的功能也被更改了。netstat将永远不报告网卡的混杂模式,来自特定地点的连接也被隐藏。ps和top命令不会报告隐藏的进程,因为/proc中没有信息。ls将跳过隐藏的文件和目录。所有这些,都是因为此类工具依靠操作系统提供信息,而入侵者在控制了操作系统后就能够向来自用户空间的请求反馈虚假情报,并且无需改动netstat,ps,top和ls程序的二进制文件。因此,tripwire一类的文件系统校验工具对这类工具将失效,也无法防备knark的执行重定向功能。如果入侵者将hackme连接到cat上,每次cat被调用,实际上是hackme在执行。这样,cat仍然保留在系统上,md5校验码也没有改变,但执行的功能却改变了。

更糟糕的是,将一套新的工具上传到被knark入侵的主机也无济于事。即使是可信的工具一样要使用系统调用,于是他们也变得不再可信。目前还无法绕过入侵者在内核级别的陷阱,除非我们也进入内核空间。基于此,我开发了检测系统是否安装了恶意LKM的工具。

之前有一点我们没有提及,lsmod会报告装载了knark.o模块。不幸的是,入侵者能轻易的将此信息抹去。knark同时还包括了另一个LKM叫做modhide,能够隐藏自身以及上一个模块。一旦模块隐藏,如果不重启动机器就无法卸载,而且没有简单的方法检测到模块的加载,所有的相关信息都不见了。正如之前介绍的,knark的所有功能令其成为终极秘密武器。

预防方法

阻止LKM破坏显然是最佳解决方案。我们有几种方法能够提前预防LKM。可以通过保护系统调用表来预防大部分的恶毒LKM。我们可以构造一个简单的LKM,定时的或者在其他模块加载时监控系统调用表。如果它发现系统调用表改变了,可以通知系统管理员甚至将调用表修改回原来的值。下面的例子能很好的工作在Linux 2.2和2.4上。如果你的机器有超过一个处理器,可以用如下命令编译:gcc -D __SMP__ -c syscall_sentry.c。如果是单处理器,去掉-D __SMP__就行了。编译成功后,用insmod加载。具体参看下面的例子。

     /*   * This LKM is designed to be a tripwire for the sys_call_table.   */   #define MODULE_NAME "syscall_sentry"   /* This definition is the time between periodic checks. */   #define TIMEOUT_SECS 10   #define MODULE   #define __KERNEL__   #include   #include   #include   #include   #include   #include   #include   #include   #include   /* This function is a simple string comparison function */   static int mystrcmp( const char *str1, const char *str2)   {   while(*str1 && *str2)   if (*(str1++) != *(str2++))   return -1;   return 0;   }   /* This function builds a timer struct for versions of Linux   * less than Linux 2.4. It is used to set a timer   */   #if linux_VERSION_CODE < KERNEL_VERSION(2,4,0)   /* Initializes a timer */   void init_timer(struct timer_list * timer)   {   timer->next = NULL;   timer->prev = NULL;   }   #endif   /* This is our timer */   static struct timer_list syscall_timer;   /* This is the system’s syscall table */   extern void *sys_call_table[];   /* This is the saved, valid syscall table */   static void *orig_sys_call_table[ NR_syscalls ];   /* This function is needed to protect yourself */   static unsigned long (*orig_init_module) (const char *, struct module*);   /* This function checks the syscalls for changes   * and changes them back to the original if it has   * been changed.   */   static int check_syscalls( void )   {   int i;   /* Add a new timer for our next check */   del_timer( &syscall_timer );   init_timer( &syscall_timer );   syscall_timer.function = (void *)check_syscalls;   syscall_timer.expires = jiffies + TIMEOUT_SECS * HZ;   add_timer( &syscall_timer );   for ( i = 0; i < NR_syscalls - 1; i++ )   {   if (orig_sys_call_table[i] != sys_call_table[i])   {   printk(KERN_INFO " SysCallSentry - sys_call_table has been   modified in entry %d! ", i);   sys_call_table[i] = orig_sys_call_table[i];   }   }   return 1;   }   /* Check sys_call_table anytime a new module is loaded. */   static int long sys_init_module_wrapper( const char *name, struct   module *mod )   {   int i;   int res = (*orig_init_module)(name,mod);   for ( i = 0; i < NR_syscalls - 1; i++ )   {   if (orig_sys_call_table[i] != sys_call_table[i])   {   printk( KERN_INFO " SysCallSentry - sys_call_table has been   modified in entry %d! ", i);   sys_call_table[i] = orig_sys_call_table[i];   }   }   return res;   }   /* Module Init Code */   static int init_module (void)   {   int i;   printk(KERN_INFO " SysCallSentry Inserted ");   /* Initiate the periodic timer */   init_timer( &syscall_timer );   /* Save the old values of the sys_call_table */   orig_init_module = sys_call_table[SYS_init_module];   /* Wrap the init_module syscall. This will check to see   * if any calls have been altered when a new module loads.   */   sys_call_table[SYS_init_module] = sys_init_module_wrapper;   for ( i=0; i < NR_syscalls - 1; i++ )   {   orig_sys_call_table[i] = sys_call_table[i];   }   /* Start our first check */   check_syscalls();   return(0);   }   /* Module Cleanup Code */   static void cleanup_module (void)   {   /* Return system status to the original */   sys_call_table[SYS_init_module] = orig_init_mo