Linux核心級後門的原理及簡單實戰應用(轉)

worldblog發表於2007-08-10
Linux核心級後門的原理及簡單實戰應用(轉)[@more@]

  以下程式碼均在linux i86 2.0.x的核心下面測試透過。它也許可以在之前的版本透過,但並沒有被測試過。因為從2.1.x核心版本就引入了相當大的改變,顯著地記憶體管理上的差別,但這些不是我們現在要討論的內容。

  使用者空間與核心空間

  linux是一個具有保護模式的作業系統。它一直工作在i386 cpu的保護模式之下。

  記憶體被分為兩個單元:核心區域和使用者區域。核心區域存放並執行著核心程式碼,當然,顧名思義,使用者區域也存放並執行使用者程式。當然,作為使用者程式來講它是不能訪問核心區域記憶體空間以及其他使用者程式的地址空間的。

  核心程式也有同樣的情況。核心程式碼也同樣不能訪問使用者區地地址空間。那麼,這樣做到底有什麼意義呢?我們假設當一個硬體驅動試圖去寫資料到一個使用者記憶體空間的程式裡的時候,它是不可以直接去完成的,但是它可以利用一些特殊的核心函式來間接完成。同樣,當引數需要傳遞地址到核心函式中時,核心函式也不能直接的來讀取該引數。同樣的,它可以利用一些特殊的核心函式來傳遞引數。

  這裡有一些比較有用的核心函式用來作為核心區與使用者區相互傳遞引數用。

  #include get_user(ptr)

  從使用者記憶體獲取給定的位元組,字,或者長整形。這只是一個宏(在核心程式碼裡面有此宏的詳細定義),並且它依據引數型別來確定傳輸數量。所以你必須巧妙地利用它。

  put_user(ptr)和get_user()非常相似,但是,它不是從使用者記憶體讀取資料,而是想使用者記憶體寫資料。

  memcpy_fromfs(void *to, const void *from,unsigned long n)

  從使用者記憶體中的*from複製n個位元組到指向核心記憶體的指標*to。

  memcpy_tofs(void *to,const *from, unsigned long n)

  從核心記憶體中的*from複製n個位元組資料到使用者記憶體中的*to。

  系統呼叫

  大部分的c函式庫的呼叫都依賴於系統呼叫,就是一些使使用者程式可以呼叫的簡單核心包裝函式。這些系統呼叫執行在核心本身或者在可載入核心模組中,就是一些可動態的載入解除安裝的核心程式碼。

  就象MS-DOS和其他許多系統一樣,linux中的系統呼叫依賴一個給定的中斷來呼叫多個系統呼叫。linux系統中,這個中斷就是int 0x80。當呼叫'int 0x80'中斷的時候,控制權就轉交給了核心(或者,我們確切點地說, 交給_system_call()這個函式), 並且實際上是一個正在進行的單處理過程。

  * _system_call()是如何工作的?

  首先,所有的暫存器被儲存並且%eax暫存器全面檢查系統呼叫表,這張表列舉了所有的系統呼叫和他們的地址資訊。它可以透過extern void *sys_call_table[]來被訪問到。該表中的每個定義的數值和記憶體地址都對應每個系統呼叫。大家可以在/usr/include/sys/syscall.h這個頭中找到系統呼叫的標示數。

  他們對應相應的SYS_systemcall名。假如一個系統呼叫不存在,那麼它在sys_call_table中相應的標示就為0,並且返回一個出錯資訊。否則,系統呼叫存在並在表裡相應的入口為系統呼叫程式碼的記憶體地址。這兒是一個有問題的系統呼叫例程:

  [root@plaguez kernel]# cat no1.c

#include

#include

#include

extern void *sys_call_table[];

sc()

{ // 165這個系統呼叫號是不存在的。

__asm__(

"movl $165,%eax

int $0x80");

}

main() {

errno = -sc();

perror("test of invalid syscall");

}

[root@plaguez kernel]

# gcc no1.c

[root@plaguez kernel]

# ./a.out test of invalid syscall:

Function not implemented

[root@plaguez kernel]

# exit

  系統控制權就會轉向真正的系統呼叫, 用來完成你的請求並返回。 然後_system_call()呼叫_ret_from_sys_call()來檢查不同的返回值, 並且最後返回到使用者記憶體。

  * libc

  這int $0x80 並不是直接被用作系統呼叫; 更確切地是,libc函式,經常用來包裝0x80中斷,這樣使用的。

  libc通常利用_syscallX()宏來描述系統呼叫,X是系統呼叫的總引數個數。

  舉個例子吧, libc中的write(2)就是利用_syscall3這個系統呼叫宏來實現的,因為實際的write(2)原型需要3個引數。在呼叫0x80中斷之前,這個_syscallX宏假定系統呼叫的堆疊結構和要求的引數列表,最後,當 _system_call()(透過int &0x80來引發)返回的時候,_syscallX()宏將會查出錯誤的返回值(在%eax)並且為其設定errno。

  讓我們看一下另一個write(2)例程並看看它是如何進行預處理的。

  [root@plaguez kernel]# cat no2.c

#include

#include

#include

#include

#include

#include

#include

#include

#include

_syscall3(ssize_t,write,int,fd,const void *,buf,size_t,count);

/*構建一個write呼叫*/

main()

{

char *t = "this is a test. ";

write(0, t, strlen(t));

}

[root@plaguez kernel]

# gcc -E no2.c > no2.C

[root@plaguez kernel]

# indent no2.C -kr

indent:no2.C:3304: Warning:

old style assignment ambiguity in

"=-". Assuming "= -"

[root@plaguez kernel]# tail -n 50 no2.C

#9 "no2.c" 2 ssize_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. ";

write(0, t, strlen(t));

}

[root@plaguez kernel]# exit

  注意那個write()裡的"0"這個引數匹配SYS_write,在/usr/include/sys/syscall.h中定義。

  * 構建你自己的系統呼叫。

  這裡給出了幾個構建你自己的系統呼叫的方法。舉個例子,你可以修改核心程式碼並且加入你自己的程式碼。一個比較簡單可行的方法,不過,一定要被寫成可載入核心模組。

  沒有一個程式碼可以象可載入核心模組那樣可以當核心需要的時候被隨時載入的。我們的主要意圖是需要一個很小的核心,當我們需要的時候執行insmod命令,給定的驅動就可以被自動載入。這樣卸除來的lkm程式一定比在核心程式碼樹裡寫程式碼要簡單易行多了。

  * 寫lkm程式

  一個lkm程式可以用c來很容易編寫出來。它包含了大量的#defines,一些函式,一個初始化模組的函式,叫做init_module(),和一個解除安裝函式:cleanup_module()。

  這裡有一個經典的lkm程式碼結構:

  #define MODULE

#define __KERNEL__

#define __KERNE_SYSCALLS__

#include

#ifdef MODULE

#include

#include

#else

#define MOD_INC_USE_COUNT

#define MOD_DEC_USE_COUNT

#endif

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

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(" Module loaded. ");

return 0;

}

void cleanup_module(void)

{

/* ... */

}

  檢查程式碼中的

  #defines (#define MODULE, #define __KERNEL__) 和 #includes (#include ...)

  一定要注意的是我們的lkm講要被執行在核心狀態,我們就不能用libc包裝的函式了,但是我們可以透過前面所討論的_syscallX()宏來構建系統呼叫。你可以這樣編譯你的模組'gcc -c -O3 module.c' 並且利用'insmod module.o'來載入。

  提一個建議,lkm也可以用來在不完全重建核心程式碼的情況下來修改核心程式碼。舉個例子, 你可以修改write系統呼叫讓它隱藏一部分給定的檔案,就象我們把我們的backdoors放到一個非常好的地方:當你無法再信任你的系統核心的時候會怎麼樣呢?

  * 核心和系統呼叫後門

  在簡單介紹了上述理論,我們主要可以用來做什麼呢。我們可以利於lkm截獲一些對我們有影響的系統呼叫,這樣可以強制核心按照我們的方式執行。例如:我們可以利用ioctl系統呼叫來隱藏sniffer所造成的網路卡PROMISC模式的顯示。非常有效。

  去改變一個給定的系統呼叫,只需要在你的lkm程式中增加一個定義extern void *sys_call_table[],並且利用init_module()函式來改變sys_call_table裡的入口來指向我們自己的程式碼。改變後的呼叫可以做我們希望它做的一切事情,利用改變sys_call_table來匯出更多的原系統呼叫。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-940282/,如需轉載,請註明出處,否則將追究法律責任。

相關文章