如果在入侵事件調查中,傳統的工具完全失效了,你該怎麼辦?當我在對付入侵者已經加載的內核模塊時,就陷入了這種困境。由於從用戶空間升級到了內核空間,LKM方式的入侵改變了以往使用的入侵響應的技術。一旦內核空間遭破壞,影響將覆蓋到整個用戶空間,這樣
如果在入侵事件調查中,傳統的工具完全失效了,你該怎麼辦?當我在對付入侵者已經加載的內核模塊時,就陷入了這種困境。由於從用戶空間升級到了內核空間,LKM方式的入侵改變了以往使用的入侵響應的技術。一旦內核空間遭破壞,影響將覆蓋到整個用戶空間,這樣入侵者無須改動系統程序就能控制他們的行為。而用戶即使將可信的工具包上傳到被入侵的主機,這些工具也不再可信。下面我將揭示惡意的內核模塊如何工作,並且給出一些我
開發的對付此類入侵的工具。
LKM概述 LKM的存在對系統管理員是個福音,對
入侵檢測卻是個噩夢。lkm最初被設計用來無須重新啟動而改變運行中的內核,從而提供一些動態功能。動態內核提供了對諸如新文件系統類型和網卡等設備的額外支持。此外,由於內核模塊能夠訪問內核的所有調用和存儲區,它能不受控制地改動整個操作系統的各個部位,因而所有調用和內存常駐的結構都有被惡意內核模塊修改的危險。
lkm的一個臭名昭著的例子是knark。一旦knark編譯並加載到入侵主機,將改變系統調用表從而改變操作系統的行為。系統調用表常駐在內核空間,基本上是提供給用戶級別程序訪問操作系統的入口。大多數
unix系統在手冊的第二部分給出syscalls的正式定義。一旦內核作為用戶空間運行,OS將把命令行上運行的所有命令和調用映像到系統調用表中。因此當knark改變系統調用表時也就改變了用戶命令的執行。knark改動了以下的重要系統調用。
* ge
tdents - 獲得目標路徑的目錄項內容(即文件和子目錄)。通過修改這個調用,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上。如果你的機器有超過一個處理器,可以用如下命令編譯:g
clearcase/" target="_blank" >cc -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<linux/module.h>
#include<linux/config.h>
#include<linux/version.h>
#include<linux/kernel.h>
#include<linux/sys.h>
#include<linux/param.h>
#include<linux/sched.h>
#include<linux/timer.h>
#include<sys/syscall.h>
/* 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_module;
printk(KERN_INFO " SysCallSentry Removed ");
}
目前的lkm工具使用的技術都是修改系統調用表。因此,由於系統調用表在實際應用中極少改變,為調用表增加一個哨兵的做法是可行的。也許真正徹底的方法是完全禁止使用lkm。成品
服務器應該將需要的全部編譯到內核中,並禁止使用lkm。
另外還有一種防護惡意lkm的方法。一個叫做"St.Jude"的工具和"St.Michael"一起,基於在學習態中生成的規則集,分別監控對系統調用表的修改,和檢查root狀態的轉換,作為入侵的證據。(http://www.sourceforge.net/projects/stjude).
調查工具和技術的研究 很顯然,要有效的響應內核級別的入侵,必須檢查機器的內核空間。因此,我們必須改變使用的工具和技術。假定在涉及到knark的入侵事件時,都建立了機器存儲設備的備份。這樣,我們就能獲得所有隱藏的文件。我們無法獲得的是隱藏的進程和
網絡信息。通過開發一個類似ps的內核級工具搜集每個進程的運行鏡像,可以對此作出補救。這個工具應該是一個lkm,以便事故發生後能動態加載。本節將講述這樣一個工具以及他如何在linux 2.2平台上進行工作。
內核級別的ps最重要的數據結構是task_struct。它是系統當前所有進程的循環鏈表。表中有進程所有的行為信息,例如打開的文件,進程的執行鏡像,打開的網絡套接字,文件操作符等等。下面這些字段非常重要,並將寫入日志中。
* 進程ID(PID) - 識別運行進程的唯一號碼。
* 用戶ID - 運行進程的用戶號碼。了解進程運行的權限級別很重要。
* 進程狀態 - 指明進程目前如何運行。進程不能總是占用全部的cpu,有時會處於睡眠狀態。此標志指明進程的運行狀態。
* 進程名稱 - 等同於執行進程的命令。
* 開始時間 - 從系統啟動到進程運行所經過的系統時鐘單位數。用來確定進程什麼時候開始運行。顯然在系統啟動時使用啟動腳本運行的進程數值相對較小。同時也未確定入侵時間提供了更多的線索。
* 打開文件句柄 -
UNIX中什麼都是文件,所以察看進程的打開文件句柄就能看見所有打開的常規文件,網絡套接字和FIFO。這些信息在跟蹤進程項文件存儲信息(比如sniffer)或者打開套接字(比如後門)等有用。
* 命令行參數 - 在解釋進程執行的選項時有用。比如,設想入侵者運行netcat,除非你獲得命令行參數,否則很難觀察到入侵者連接到哪裡,而結構裡的命令行參數則包含了netcat的ip地址和端口。
* 進程環境變量 - 每個運行的進程都有自己的環境變量表。一般來說,它是運行用戶在執行進程時的環境表的拷貝。因此,檢查這個表有助於獲得入侵者入侵回話的額外信息。
由此,我們的工具應當遍歷此雙循環鏈表,將每個進程的信息記錄到日志中。記錄與ps -ef命令的結果很類似,因此我們能夠輕松閱讀結果。另外,將創建一個包含進程運行鏡像的獨立文件,供將來的離線工具分析。在linux中訪問進程執行鏡像不太容易直接完成,但仍然可能。鏡像駐留在任務結構的內存映像中,在此內存映像結構中有一塊虛擬內存區和任務相關聯。內存區中的虛擬內存文件包含了文件操作符數組。一旦我們找到適當的讀取函數,將執行映像讀取出來並寫到另一個文件中就非常簡單了。理論上,由於進程在運行後其二進制代碼可能被刪除,進程應將可執行映像完全載入到內存中。獲得映像最困難的部分是找到它在內存中的什麼地方。
當我們的模塊運行時,任何其他進程不能調度運行。中斷和其他系統活動仍然可以發生,但是此模塊優先運行。由於此模塊“凍結”了進程表,我把它命名為“硝酸甘油”。源代碼可以在http://www.foundstone.com 找到,作為在以後版本的linux和其他操作系統開發類似工具的基礎。
最後一個判斷knark是否在你的系統中加載的依據是查看網卡的狀態。當knark加載,網卡將永遠不會報告處於混雜狀態。這是為了防止系統管理員注意到入侵者把網卡置於混雜狀態的嗅探器。實際上,你需要做的,僅僅是運行tcpdump或者其他嗅探器並且用ifconfig觀察網卡的狀態。如果網卡仍不處於混雜狀態,可能knark已經加載了。
結論 可加載模塊對入侵響應來說是一個明顯的沖擊。破壞性的可加載模塊已經公諸於眾,並且可能已經被許多入侵者使用。這將入侵和入侵檢測提高到了內核級別。盡管看起來進行調查時內核模塊是很明顯的要素,但這不是致命的。我們可以看到能夠采取簡單的方法來保護自己免受此類攻擊。此外,對此類攻擊的檢測工具和技術同樣利用運行在內核空間向入侵者開火