Linux 核心級後門的原理和簡單實戰

來源:互聯網
上載者:User

使用者空間與核心空間
---------------------------

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

記憶體被分為兩個單元: 核心地區和使用者地區。(譯者註:我覺得還是這樣叫比較順口)核心地區存放並運行著核心代碼,當然,顧名思義,使用者地區也存放並運行使用者程式。當然,作為使用者進程來講它是不能訪問核心地區記憶體空間以及其他使用者進程的地址空間的。

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

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

#include <asm/segment.h>

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。

/*譯者註:這四個函數足以在2.0.x中解決核心和使用者區的參數傳遞問題,在2.0.x以上
的版本有新的實現,即copy_user_to(...)以及copy_user_from(...)根據核心版本這些
特殊函數會有不同,請關注核心代碼的實現方法。*/

系統調用

大部分的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 <linux/errno.h>
#include <sys/syscall.h>
#include <errno.h>

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 <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);/*構建一個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 -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.n";
    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 <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講要被運行在核心狀態,我們就不能用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來匯出更多的原系統調用,並且。。。。

譯者後話:這篇文章相對比較淺顯易懂,所以可以作為大家入門lkm編程來用,它著重講述了linux系統調用system call的原理,以及我們如何通過lkm來截獲它並換成我們想要的代碼來建立後門程式。再次強調本文的依據是linux核心版本2.0.x,大家在自己系統實現時請對比核心代碼來做改變。

上一篇:《【linux 編程】linux/unix 進程的建立》相關文檔:《LINUX的系統核心空間的保護》
下一篇:《如何編寫 linux 裝置驅動程式》

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.