Introduction
Kernel Module還可以做一些比較cool的事情,比如劫持系統呼叫,增加我們自己的邏輯,在系統呼叫監聽、過濾和審計的場景使用。
系統呼叫概述
劫持系統呼叫是一件比較危險的事情,例如劫持open()系統呼叫,並且阻止一切open()的操作,那麼計算機將不能夠開啟任何檔案,甚至無法關閉計算機,唯一能做的事情只有冷重啟計算機。
通常來講,使用者程式不允許直接訪問核心,不能訪問核心記憶體,也不能使用核心函式,這由CPU架構來保證,無法改變。
為什麼是通常來講?因為有一個例外,那就是系統呼叫。發生系統呼叫時,使用者程式將所需的值(系統呼叫號、系統呼叫引數)壓入暫存器中,然後呼叫一條特殊的CPU指令使程式從使用者態切換到核心態繼續執行。這個特殊的CPU指令,在Intel X86架構下,就是所謂的interrupt 0x80,即80中斷。當系統呼叫結束後,通過同樣的方式,從核心態切換到使用者態,繼續執行程式。
程式切入到系統態後,會根據暫存器中的系統呼叫號,在系統呼叫表(sys_call_table)中,執行相應的系統呼叫處理函式。
系統呼叫劫持程式碼
劫持系統呼叫是一個高危操作,在2.6.24核心之前,可以簡單的替換sys_call_table中的系統呼叫函式地址。
下面給出2.6.24核心之前的劫持系統呼叫open()/__NR_open的程式碼:
#include <linux/kernel.h>
#include <linux/module.h>
...
extern void *sys_call_table[];
asmlinkage int (*original_open)(const char *, int, int);
asmlinkage int hijack_open(const char *filename, int flags, int mode) {
// do hijack logic, just print the parameter
printk(KERN_INFO "hijack: open(%s, %d, %d)
", filename, flags, mode);
return original_open(filename, flags, mode);
}
int init_module() {
original_open = sys_call_table[__NR_open];
sys_call_table[__NR_open] = hijack_open;
return 0;
}
void cleanup_module() {
sys_call_table[__NR_open] = original_open;
}
不幸的是,由於劫持系統呼叫產生的安全性問題(如監聽、竊取),Linux Kernel在2.6.24將sys_call_table的記憶體地址變為只讀,用上述的方法寫地址時會失敗。
稍後,有大神給出了一段程式碼,可以將只讀頁變成可讀,這樣就可以修改系統呼叫表了。
int set_page_ro(long unsigned int _addr) {
struct page *pg;
pgprot_t prot;
pg = virt_to_page(_addr);
prot.pgprot = VM_READ;
return change_page_attr(pg, 1, prot);
}
int set_page_rw(long unsigned int _addr) {
struct page *pg;
pgprot_t prot;
pg = virt_to_page(_addr);
prot.pgprot = VM_READ | VM_WRITE;
return change_page_attr(pg, 1, prot);
}
過了一段時間後,change_page_attr函式也被禁用了,不過還是有別的方法可以繞過,下面給出一個在3.19.0核心下仍然可用的程式碼:
#include <asm/unistd.h>
#include <linux/module.h>
#include <linux/highmem.h>
// 由於sys_call_table符號不再被匯出,需要hardcode地址,
// 地址需要在bash下鍵入下面命令進行查詢:
// $ grep sys_call_table /boot/System.map-3.19.0-25-generic
unsigned long *sys_call_table = (unsigned long*) 0xffffffff81600480;
asmlinkage int (*original_open)(const char *, int, int);
asmlinkage int hijack_open(const char *filename, int flags, int mode) {
// do hijack logic, just print the parameter
printk(KERN_INFO "hijack: open(%s, %d, %d)
", filename, flags, mode);
return original_open(filename, flags, mode);
}
int make_ro(unsigned long address) {
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte = pte->pte &~ _PAGE_RW;
return 0;
}
int make_rw(unsigned long address) {
unsigned int level;
pte_t *pte = lookup_address(address, &level);
if(pte->pte &~ _PAGE_RW) {
pte->pte |= _PAGE_RW;
}
return 0;
}
int init_module(void) {
make_rw((unsigned long)sys_call_table);
original_open = (void*)*(sys_call_table + __NR_open);
*(sys_call_table + __NR_open) = (unsigned long)hijack_open;
make_ro((unsigned long)sys_call_table);
return 0;
}
void cleanup_module(void) {
make_rw((unsigned long)sys_call_table);
*(sys_call_table + __NR_open) = (unsigned long)original_open;
make_ro((unsigned long)sys_call_table);
}
載入模組後,通過dmesg檢視日誌:
$ dmesg
...
[116465.803107] `hijack: open("/proc/33162/status", 80000, 0)
[116465.803107] `hijack: open("/etc/passwd", 0, 1B6)
...
Summary
通過編寫一個劫持open()系統呼叫的Linux Kernel Module,現在我們可以對系統呼叫監聽、過濾和審計了。