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