Linux內核級后門(mén)的原理及簡(jiǎn)單實(shí)戰應用
作者:BB 發(fā)文時(shí)間:2005.08.10
以下代碼均在linux i86 2.0.x的內核下面測試通過(guò)。它也許可以在之前的版本通過(guò),但并沒(méi)有被測試過(guò)。因為從2.1.x內核版本就引入了相當大的改變,顯著(zhù)地內存管理上的差別,但這些不是我們現在要討論的內容。
用戶(hù)空間與內核空間
linux是一個(gè)具有保護模式的操作系統。它一直工作在i386 cpu的保護模式之下。
內存被分為兩個(gè)單元:內核區域和用戶(hù)區域。內核區域存放并運行著(zhù)核心代碼,當然,顧名思義,用戶(hù)區域也存放并運行用戶(hù)程序。當然,作為用戶(hù)進(jìn)程來(lái)講它是不能訪(fǎng)問(wèn)內核區域內存空間以及其他用戶(hù)進(jìn)程的地址空間的。
核心進(jìn)程也有同樣的情況。核心代碼也同樣不能訪(fǎng)問(wèn)用戶(hù)區地地址空間。那么,這樣做到底有什么意義呢?我們假設當一個(gè)硬件驅動(dòng)試圖去寫(xiě)數據到一個(gè)用戶(hù)內存空間的程序里的時(shí)候,它是不可以直接去完成的,但是它可以利用一些特殊的核心函數來(lái)間接完成。同樣,當參數需要傳遞地址到核心函數中時(shí),核心函數也不能直接的來(lái)讀取該參數。同樣的,它可以利用一些特殊的核心函數來(lái)傳遞參數。
這里有一些比較有用的核心函數用來(lái)作為內核區與用戶(hù)區相互傳遞參數用。
#include <asm/segment.h>get_user(ptr)
從用戶(hù)內存獲取給定的字節,字,或者長(cháng)整形。這只是一個(gè)宏(在核心代碼里面有此宏的詳細定義),并且它依據參數類(lèi)型來(lái)確定傳輸數量。所以你必須巧妙地利用它。
put_user(ptr)和get_user()非常相似,但是,它不是從用戶(hù)內存讀取數據,而是想用戶(hù)內存寫(xiě)數據。
memcpy_fromfs(void *to,const void *from,unsigned long n)
從用戶(hù)內存中的*from拷貝n個(gè)字節到指向核心內存的指針*to。
memcpy_tofs(void *to,const *from,unsigned long n)
從核心內存中的*from拷貝n個(gè)字節數據到用戶(hù)內存中的*to。
系統調用
大部分的c函數庫的調用都依賴(lài)于系統調用,就是一些使用戶(hù)程序可以調用的簡(jiǎn)單核心包裝函數。這些系統調用運行在內核本身或者在可加載內核模塊中,就是一些可動(dòng)態(tài)的加載卸載的核心代碼。
就象MS-DOS和其他許多系統一樣,linux中的系統調用依賴(lài)一個(gè)給定的中斷來(lái)調用多個(gè)系統調用。linux系統中,這個(gè)中斷就是int 0x80。當調用‘int 0x80‘中斷的時(shí)候,控制權就轉交給了內核(或者,我們確切點(diǎn)地說(shuō), 交給_system_call()這個(gè)函數), 并且實(shí)際上是一個(gè)正在進(jìn)行的單處理過(guò)程。
* _system_call()是如何工作的?
首先,所有的寄存器被保存并且%eax寄存器全面檢查系統調用表,這張表列舉了所有的系統調用和他們的地址信息。它可以通過(guò)extern void *sys_call_table[]來(lái)被訪(fǎng)問(wèn)到。該表中的每個(gè)定義的數值和內存地址都對應每個(gè)系統調用。大家可以在/usr/include/sys/syscall.h這個(gè)頭中找到系統調用的標示數。
他們對應相應的SYS_systemcall名。假如一個(gè)系統調用不存在,那么它在sys_call_table中相應的標示就為0,并且返回一個(gè)出錯信息。否則,系統調用存在并在表里相應的入口為系統調用代碼的內存地址。這兒是一個(gè)有問(wèn)題的系統調用例程:
[root@plaguez kernel]# cat no1.c#include <linux/errno.h>#include <sys/syscall.h>#include <errno.h>extern void *sys_call_table[];sc(){ // 165這個(gè)系統調用號是不存在的。 __asm__( "movl $165,%eax int $0x80");}main(){ errno = -sc(); perror("test of invalid syscall");}[root@plaguez kernel]# gcc no1.c[root@plaguez kernel]# ./a.outtest of invalid syscall:Function not implemented[root@plaguez kernel]# exit
系統控制權就會(huì )轉向真正的系統調用, 用來(lái)完成你的請求并返回。 然后_system_call()調用_ret_from_sys_call()來(lái)檢查不同的返回值, 并且最后返回到用戶(hù)內存。
* libc
這int $0x80 并不是直接被用作系統調用; 更確切地是,libc函數,經(jīng)常用來(lái)包裝0x80中斷,這樣使用的。
libc通常利用_syscallX()宏來(lái)描述系統調用,X是系統調用的總參數個(gè)數。
舉個(gè)例子吧, libc中的write(2)就是利用_syscall3這個(gè)系統調用宏來(lái)實(shí)現的,因為實(shí)際的write(2)原型需要3個(gè)參數。在調用0x80中斷之前,這個(gè)_syscallX宏假定系統調用的堆棧結構和要求的參數列表,最后,當 _system_call()(通過(guò)int &0x80來(lái)引發(fā))返回的時(shí)候,_syscallX()宏將會(huì )查出錯誤的返回值(在%eax)并且為其設置errno。
讓我們看一下另一個(gè)write(2)例程并看看它是如何進(jìn)行預處理的。
[root@plaguez kernel]# cat no2.c#include <linux/types.h>#include <linux/fs.h>#include <sys/syscall.h>#include <asm/unistd.h>#include <sys/types.h>#include <stdio.h>#include <errno.h>#include <fcntl.h>#include <ctype.h>_syscall3(ssize_t,write,int,fd,const void *,buf,size_t,count);/*構建一個(gè)write調用*/main(){ char *t = "this is a test.\n"; write(0, t, strlen(t));}[root@plaguez kernel]# gcc -E no2.c > no2.C[root@plaguez kernel]# indent no2.C -krindent:no2.C:3304: Warning:old style assignment ambiguity in"=-". Assuming "= -"[root@plaguez kernel]# tail -n 50 no2.C#9 "no2.c" 2ssize_t write(int fd,const void *buf, size_t count){ long __res; __asm__ __volatile("int $0x80":"=a" (__res):"0"(4), "b"((long) (fd)), "c"((long) (buf)), "d"((long) (count))); if (__res >= 0) return (ssize_t) __res; errno = -__res; return -1;};main(){ char *t = "this is a test.\n"; write(0, t, strlen(t));}[root@plaguez kernel]# exit
注意那個(gè)write()里的"0"這個(gè)參數匹配SYS_write,在/usr/include/sys/syscall.h中定義。
* 構建你自己的系統調用。
這里給出了幾個(gè)構建你自己的系統調用的方法。舉個(gè)例子,你可以修改內核代碼并且加入你自己的代碼。一個(gè)比較簡(jiǎn)單可行的方法,不過(guò),一定要被寫(xiě)成可加載內核模塊。
沒(méi)有一個(gè)代碼可以象可加載內核模塊那樣可以當內核需要的時(shí)候被隨時(shí)加載的。我們的主要意圖是需要一個(gè)很小的內核,當我們需要的時(shí)候運行insmod命令,給定的驅動(dòng)就可以被自動(dòng)加載。這樣卸除來(lái)的lkm程序一定比在內核代碼樹(shù)里寫(xiě)代碼要簡(jiǎn)單易行多了。
* 寫(xiě)lkm程序
一個(gè)lkm程序可以用c來(lái)很容易編寫(xiě)出來(lái)。它包含了大量的#defines,一些函數,一個(gè)初始化模塊的函數,叫做init_module(),和一個(gè)卸載函數:cleanup_module()。
這里有一個(gè)經(jīng)典的lkm代碼結構:
#define MODULE#define __KERNEL__#define __KERNE_SYSCALLS__#include <linux/config.h>#ifdef MODULE#include <linux/module.h>#include <linux/version.h>#else#define MOD_INC_USE_COUNT#define MOD_DEC_USE_COUNT#endif#include <linux/types.h>#include <linux/fs.h>#include <linux/mm.h>#include <linux/errno.h>#include <asm/segment.h>#include <sys/syscall.h>#include <linux/dirent.h>#include <asm/unistd.h>#include <sys/types.h>#include <stdio.h>#include <errno.h>#include <fcntl.h>#include <ctype.h>int errno;char tmp[64];/* 假如,我們要用到ioctl調用 */_syscall3(int, ioctl, int, d, int, request, unsigned long, arg);int myfunction(int parm1,char *parm2){ int i,j,k; /* ... */}int init_module(void){ /* ... */ printk("\nModule loaded.\n"); return 0;}void cleanup_module(void){ /* ... */}
檢查代碼中的
#defines (#define MODULE, #define __KERNEL__)和#includes (#include <linux/config.h> ...)
一定要注意的是我們的lkm講要被運行在內核狀態(tài),我們就不能用libc包裝的函數了,但是我們可以通過(guò)前面所討論的_syscallX()宏來(lái)構建系統調用。你可以這樣編譯你的模塊‘gcc -c -O3 module.c‘ 并且利用‘insmod module.o‘來(lái)加載。
提一個(gè)建議,lkm也可以用來(lái)在不完全重建核心代碼的情況下來(lái)修改內核代碼。舉個(gè)例子, 你可以修改write系統調用讓它隱藏一部分給定的文件,就象我們把我們的backdoors放到一個(gè)非常好的地方:當你無(wú)法再信任你的系統內核的時(shí)候會(huì )怎么樣呢?
* 內核和系統調用后門(mén)
在簡(jiǎn)單介紹了上述理論,我們主要可以用來(lái)做什么呢。我們可以利于lkm截獲一些對我們有影響的系統調用,這樣可以強制內核按照我們的方式運行。例如:我們可以利用ioctl系統調用來(lái)隱藏sniffer所造成的網(wǎng)卡PROMISC模式的顯示。非常有效。
去改變一個(gè)給定的系統調用,只需要在你的lkm程序中增加一個(gè)定義extern void *sys_call_table[],并且利用init_module()函數來(lái)改變sys_call_table里的入口來(lái)指向我們自己的代碼。改變后的調用可以做我們希望它做的一切事情,利用改變sys_call_table來(lái)導出更多的原系統調用。
(T117)