摘要:本文深入分析了Linux環境下檔案、進程及模組的進階隱藏技術,其中包括:Linux可卸載模組編程技術、修改記憶體映象直接對系統調用進行修改技術,通過虛擬檔案系統proc隱藏特定進程的技術。
隱藏技術在電腦系統安全中應用十分廣泛,尤其是在網路攻擊中,當攻擊者成功侵入一個系統後,有效隱藏攻擊者的檔案、進程及其載入的模組變得尤為重要。本文將討論Linux系統中檔案、進程及模組的進階隱藏技術,這些技術有的已經被廣泛應用到各種後門或安全檢測程式之中,而有一些則剛剛起步,仍然處在討論階段,應用很少。
1.隱藏技術
1.1.Linux下的中斷控制及系統調用
Intel x86系列微機支援256種中斷,為了使處理器比較容易地識別每種中斷源,把它們從0~256編號,即賦予一個中斷類型碼n,Intel把它稱作中斷向量。
Linux用一個中斷向量(128或者0x80)來實現系統調用,所有的系統調用都通過唯一的入口system_call來進入核心,當使用者動態進程執行一條int 0x80彙編指令時,CPU就切換到核心態,並開始執行system_call函數,system_call函數再通過系統調用表sys_call_table來取得相應系統調用的地址進行執行。系統調用表sys_call_table中存放所有系統調用函數的地址,每個地址可以用系統調用號來進行索引,例如sys_call_table[NR_fork]索引到的就是系統調用sys_fork()的地址。
Linux用中斷描述符(8位元組)來表示每個中斷的相關資訊,其格式如下:
位移量31….16 一些標誌、類型碼及保留位
段選擇符 位移量15….0
所有的中斷描述符存放在一片連續的地址空間中,這個連續的地址空間稱作中斷描述符表(IDT),其起始地址存放在中斷描述符表寄存器(IDTR)中,其格式如下:
32位基址值 界限
其中各個結構的相應聯絡可以如下表示:
通過上面的說明可以得出通過IDTR寄存器來找到system_call函數地址的方法:根據IDTR寄存器找到中斷描述符表,中斷描述符表的第0x80項即是system_call函數的地址,這個地址將在後面的討論中應用到。
1.2.Linux 的LKM(可裝載核心模組)技術
為了使核心保持較小的體積並能夠方便的進行功能擴充,Linux系統提供了模組機制。模組是核心的一部分,但並沒有被編譯進核心,它們被編譯成目標檔案,在運行過程中根據需要動態插入核心或者從核心中移除。由於模組在插入後是作為Linux核心的一部分來啟動並執行,所以模組編程實際上就是核心編程,因此可以在模組中使用一些由核心匯出的資源,例如Linux2.4.18版以前的核心匯出系統調用表(sys_call_table)的地址,這樣就可以根據該地址直接修改系統調用的入口,從而改變系統調用。在模組編程中必須存在初始化函數及清除函數,一般情況下,這兩個函數預設為init_module()以及clearup_module(),從2.3.13核心版本開始,使用者也可以給這兩個函數重新命名,初始化函數在模組被插入系統時調用,在其中可以進行一些函數及符號的註冊工作,清除函數則在模組移除系統時進行調用,一些恢複工作通常在該函數中完成。
1.3.Linux下的記憶體映像
/dev/kmem是一個字元裝置,是電腦主存的映像,通過它可以測試甚至修改系統,當核心不匯出sys_call_table地址或者不允許插入模組時可以通過該映像修改系統調用,從而實現隱藏檔案、進程或者模組的目的。
1.4.proc 檔案系統
proc檔案系統是一個虛擬檔案系統,它通過檔案系統的介面實現,用於輸出系統運行狀態。它以檔案系統的形式,為作業系統本身和應用進程之間的通訊提供了一個介面,使應用程式能夠安全、方便地獲得系統當前的健全狀態何核心的內部資料資訊,並可以修改某些系統的配置資訊。由於proc以檔案系統的介面實現,因此可以象訪問普通檔案一樣訪問它,但它只存在於記憶體之中。
2.技術分析
2.1 隱藏檔案
Linux系統中用來查詢檔案資訊的系統調用是sys_getdents,這一點可以通過strace來觀察到,例如strace ls 將列出命令ls用到的系統調用,從中可以發現ls是通過sys_getedents來執行操作的。當查詢檔案或者目錄的相關資訊時,Linux系統用sys_getedents來執行相應的查詢操作,並把得到的資訊傳遞給使用者空間啟動並執行程式,所以如果修改該系統調用,去掉結果中與某些特定檔案的相關資訊,那麼所有利用該系統調用的程式將看不見該檔案,從而達到了隱藏的目的。首先介紹一下原來的系統調用,其原型為:
int sys_getdents(unsigned int fd, struct dirent *dirp,unsigned int count)
其中fd為指向目錄檔案的檔案描述符,該函數根據fd所指向的目錄檔案讀取相應dirent結構,並放入dirp中,其中count為dirp中返回的資料量,正確時該函數傳回值為填充到dirp的位元組數。是修改後的系統調用hacked_getdents執行流程。
圖中的hacked_getdents函數實際上就是先調用原來的系統調用,然後從得到的dirent結構中去除與特定檔案名稱相關的檔案資訊,從而應用程式從該系統調用返回後將看不到該檔案的存在。
應該注意的是,一些較新的版本中是通過sys_getdents64來查詢檔案資訊的,但其實現原理與sys_getdents基本相同,所以在這些版本中仍然可以用與上面類似的方法來修改該系統調用,隱藏檔案。
2.2 隱藏模組
上面分析了如何修改系統調用以隱藏特定名字的檔案,在實際的處理中,經常會用模組來達到修改系統調用的目的,但是當插入一個模組時,若不採取任何隱藏措施,很容易被對方發現,一旦對方發現並卸載了所插入的模組,那麼所有利用該模組來隱藏的檔案就暴露了,所以應繼續分析如何來隱藏特定名字的模組。Linux中用來查詢模組資訊的系統調用是sys_query_module,所以可以通過修改該系統調用達到隱藏特定模組的目的。首先解釋一下原來的系統調用,原來系統調用的原型為:
int sys_query_module(const char *name, int which, void *buf, size_t bufsize , size_t *ret)
如果參數name不空,則訪問特定的模組,否則訪問的是核心模組,參數which說明查詢的類型,當which=QM_MODULES時,返回所有當前已插入的模組名稱,存入buff, 並且在ret中存放模組的個數,buffsize是buf緩衝區的大小。在模組隱藏的過程中只需要對which=QM_MODULES的情況進行處理就可以達到目的。修改後的系統調用工作過程如下:
1)調用原來的系統調用,出錯則返回錯誤碼;
2)如果which不等於QM_MODULES,則不需要處理,直接返回。
3)從buf的開始位置進行處理,如果存在特定的名字,則將後面的模組名稱向前覆蓋該名字。
4)重複3),直到處理處理完所有的名字,正確返回。
2.3 隱藏進程
在Linux中不存在直接查詢進程資訊的系統調用,類似於ps這樣查詢進程資訊的命令是通過查詢proc檔案系統來實現的,在背景知識中已經介紹過proc檔案系統,由於它應用檔案系統的介面實現,因此同樣可以用隱藏檔案的方法來隱藏proc檔案系統中的檔案,只需要在上面的hacked_getdents中加入對於proc檔案系統的判斷即可。由於proc是特殊的檔案系統,只存在於記憶體之中,不存在於任何實際裝置之上,所以Linux核心分配給它一個特定的主裝置號0以及一個特定的次裝置號1,除此之外,由於在外存上沒有與之對應的i節點,所以系統也分配給它一個特殊的節點號PROC_ROOT_INO(值為1),而裝置上的1號索引節點是保留不用的。通過上面的分析,可以得出判斷一個檔案是否屬於proc檔案系統的方法:
1)得到該檔案對應的inode結構dinode;
2)if (dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) && MINOR(dinode->i _dev) == 1) {該檔案屬於proc檔案系統}
通過上面的分析,給出隱藏特定進程的虛擬碼表示:
hacket_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
{
調用原來的系統調用;
得到fd所對應的節點;
if(該檔案屬於proc檔案系統&&該檔案名稱需要隱藏)
{從dirp中去掉該檔案相關資訊}
}
2.4 修改系統調用的方法
現在已經解決了如何修改系統調用來達到隱藏的目的,那麼如何用修改後的系統調用來替換原來的呢?這個問題在實際應用中往往是最關鍵的,下面將討論在不同的情況下如何做到這一點。
(1)當系統匯出sys_call_table,並且支援動態插入模組的情況下:
在Linux核心2.4.18版以前,這種核心配置是非常普遍的。這種情況下修改系統調用非常容易,只需要修改相應的sys_call_table表項,使其指向新的系統調用即可。下面是相應的代碼:
int orig_getdents(unsigned int fd, struct dirent *dirp, unsigned int count)
int init_module(void)
/*初始化模組*/
{
orig_getdents=sys_call_table[SYS_getdents]; //儲存原來的系統調用
orig_query_module=sys_call_table[SYS_query_module]
sys_call_table[SYS_getdents]=hacked_getdents; //設定新的系統調用
sys_call_table[SYS_query_module]=hacked_query_module;
return 0; //返回0表示成功
}
void cleanup_module(void)
/*卸載模組*/
{
sys_call_table[SYS_getdents]=orig_getdents; //恢複原來的系統調用
sys_call_table[SYS_query_module]=orig_query_module;
}
(2)在系統並不匯出sys_call_table的情況下:
linux核心在2.4.18以後為了安全起見不再匯出sys_call_table符號,從而無法直接獲得系統調用表的地址,那麼就必須找到其他的辦法來得到這個地址。在背景知識中提到了/dev/kmem是系統主存的映像,可以通過查詢該檔案來找到sys_call_table的地址,並對其進行修改,來使用新的系統調用。那麼如何在系統映像中找到sys_call_table的地址呢?讓我們先看看system_call的原始碼是如何來實現系統調用的(代碼見/arch/i386/kernel/entry.S):
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_CURRENT(%ebx)
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x02,tsk_ptrace(%ebx) # PT_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp) # save the return value
ENTRY(ret_from_sys_call)
這段原始碼首先儲存相應的寄存器的值,然後判斷系統調用號(在eax寄存器中)是否合法,繼而對設定調試的情況進行處理,在所有這些進行完後,利用call *SYMBOL_NAME(sys_call_table)(,%eax,4) 來轉入相應的系統調用進行處理,其中的SYMBOL_NAME(sys_call_table)得出的就是sys_call_table的地址。從上面的分析可以看出,當找到system_call函數之後,利用字元匹配來尋找相應call語句就可以確定sys_call_table的位置,因為call something(,%eax,4)的機器指令碼是0xff 0x14 0x85。所以匹配這個指令碼就行了。至於如何確定system_call的地址在背景知識中已經介紹了,下面給出相應的虛擬碼:
struct{ //各欄位含義可以參考背景知識中關於IDTR寄存器的介紹
unsigned short limit;
unsigned int base;
}__attribute__((packed))idtr;
struct{ //各欄位含義可以參考背景知識中關於中斷描述符的介紹
unsigned short off1;
unsigned short sel;
unsigned char none,flags;
unsigned short off2;
}__attribute__((packed))idt;
int kmem;
/ *下面函數用於從kemem對應的檔案中位移量為off處讀取sz個位元組至記憶體m處*/
void readkmem(void *m,unsigned off,int sz) {………}
/*下面函數用於從src讀取count個位元組至dest處*/
void weitekmem(void *src,void *dest,unsigned int count) {………..}
unsigned sct; //用來存放sys_call_table地址
char buff[100]; //用於存放system_call函數的前100個位元組。
char *p;
if((kmem=open(“/dev/kmem”,O_RDONLY))<0)
return 1;
asm(“sidt %0” “:=m” (idtr)); //讀取idtr寄存器的值至idtr結構中
readkmem(&idt,idtr.base+8*0x80,sizeof(idt)) //將0x80描述符讀至idt結構中
sys_ call_off=(idt.off2<<16)|idt.off1; //得到system_call函數的地址。
readkmem(buff,sys_call_off,100) //讀取system_call函數的前100位元組至buff
p=(char *)memmem(buff,100,”xffx14x85”,3); //得到call語句對應機器碼的地址
sct=(unsigned *)(p+3) //得到sys_call_table的地址。
至此已經得到了sys_call_table在記憶體中的位置,這樣在根據系統調用號就能夠找到相應的系統調用對應的地址,修改該地址就可以使用新的系統調函數,具體的做法如下:
readkmem(&orig_getdents,sct+ SYS_getdents*4,4)//儲存原來的系統調用
readkmem(&orig_query_module,sct+SYS_query_module*4,4);
writekmem(hacked_getdents,sct+SYS_getdents*4,4);//設定新的系統調用
writekmem(hacket_query_module,sct+SYS_query_module*4,4);
2.5 其他的相關技術
上面已經完全解決了隱藏的相關技術問題,在實際應用中,可以把啟動模組或者進程的代碼做成指令碼加入到相應的啟動目錄中,假設你的Linux運行層級為3,則可以加到目錄rc3.d中(該目錄常存在於/etc/rc.d或者/etc目錄下),然後把該指令碼的名字改為可以隱藏的名字。另一種方法就是在一些啟動指令碼中加入啟動你的模組或者進程的代碼,但這樣比較容易被發現,一個解決思路就是進程或模組啟動以後馬上恢複正常的指令碼,由於系統關機時會向所有進程發送SIGHUP訊號,可以在進程或模組中處理該訊號,使該訊號發生時修改啟動指令碼,重新加入啟動模組的代碼,這樣當系統下次啟動時又可以載入這個的模組了,而且管理員察看啟動指令碼時也不會發現異常。
3.結束語
本文對Linux環境下的一些進階隱藏技術進行了分析研究,其中所涉及的技術不僅可以用在系統安全方面,在其他方面也有重要的借鑒意義。由於Linux的開放特性,使得攻擊者一旦獲得了root許可權就能夠對系統進行較多的修改,所以避免第一次被入侵是至關重要的。