三.防止緩沖區溢出的內核補丁
冰塊
這一部分我們來描述一個對系統安全有很大危害的緩沖區溢出的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程序在后臺運行。大多數情況下,系統管理員不會去直接開啟他們也不會控制他們的執行,所以,象我們前面提到的一樣,這樣的特權程序就會被當作緩沖區溢出漏洞攻擊的主要目標。大概可以分成下面幾種類型:
后臺程序都是在系統系統的時候直接由初始化腳本運行的。就象網絡服務器中的.netd超級服務器程序那樣,里面有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_aclearcase/" target="_blank" >ccess,這個調用是來實現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是:
這篇文章和以前發表的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;
}