系統調用
迄今為止,我們做的唯一的事就是用好已定義的內核機制去登記 /proc 文件和設備驅動處理程序。如果你想做內核程序員認為你想做的,例如寫設備驅動程序,這就對了。但是如果你想做一些不平常的事,在某些地方改變系統的行為呢?那麼這大多取決你自己。
這是內核編程中變得危險的地方。當寫下下面的例子,我殺死了 open 系統調用。這意味著我不能打開任何文件,不能運行任何程序,甚至不能 shutdown 計算機。我不得不按電源開關。幸運的,沒有文件消失。為了確保不失去任何文件,在你做insmod 和 rmmod之前請運行 sync 。
忘記 /proc 文件,忘記設備文件。它們只是次要的細節。 所有進程都要使用的和內核通信的真正的方法是系統調用。當一個進程請求內核的服務時(例如打開文件,分支一個新進程,請求更多內存),這是被使用的機制。如果你想改變內核的行為方式,這是你要做的地方。順便說一下,如果你想看看一個程序使用了什麼系統調用,運行 strace <命令> <參數列表>。
通常,一個進程不能訪問內核。它不能訪問內核的內存和調用內核的函數。CPU硬件強迫這個(那就是為什麼叫‘保護模式’的原因)。系統調用是對這個通常的規則的例外。所發生的是進程用適當的值填充寄存器然後調用跳到內核中先前已定義的區域的特定的指令(當然,該區域是用戶進程可讀但不可寫的)。在 Intel CPU下,使用中斷 0x80 做這個。硬件知道一旦你跳到這個區域,你就不再是運行在嚴格的用戶模式而是操作系統內核--因此你就可以做任何你想做的。
內核中那個進程可以跳到的區域被稱為system_call。 在該區域的程序檢查系統調用數,該數告訴內核進程請求什麼服務。然後,它在系統調用表(sys_call_table)中查找調用的內核函數的地址。然後它調用該函數並在該函數返回後做一些系統檢查,再返回那個進程(或者如果該進程的時間運行完了就返回到一個不同的進程)。如果你想讀這個代碼,它在源文件arch//kernel/entry.S中的 ENTRY(system_call)行後。
因此,如果你想改變某個系統調用的工作方法,我們所需要做的是寫一個自己的函數以實現它(通常是加一些我們的代碼然後再調用原來的函數)並且改變 sys_call_table 中的指針指向我們的函數。因為我們可能隨後要移除它而我們不想留下一個不穩定的系統,所以在 cleanup_module 中將那個表恢復成原來的狀態是很重要的。
這兒的源代碼是這樣一個內核模塊的例子。我們想“偵察”某個用戶,並在該用戶打開一個文件的時候 printk 一個信息。朝著這個目標,我們用我們自己的被稱為our_sys_open的函數代替原來的系統調用去打開文件。這個函數檢查當前進程的UID(用戶的ID)而如果它等於我們要偵察的UID,它就調用 printk顯示要打開的文件名。然後,它用相同的參數調用原來的 open 函數做實際的打開文件的工作。
init_module 代替sys_call_table 中合適的區域並且將原來的指針保存在一個變量中。 cleanup_module 函數使用該變量將沒件事恢復成通常的狀態。這個方法是危險的,因為兩個內核模塊同時改變同一個系統調用是可能的。想象我們有兩個內核模塊 A 和 B。A 的打開系統調用將是 A_open 而B 的將是 B_open 。現在,當 A 被插入內核,系統調用被 A_open 代替,當它完成時將調用原來的 sys_open 。接著,B 被插入內核,它將用 B_open 代替系統調用,當它完成時將調用它認為的原來的系統調用 A_open。
現在,如果 B 被先移除,所有的事情將是好的--它將簡單的恢復系統調用為將恢復原始的系統調用的 A_open。然而,如果A 被移除然後 B 才被移除,系統將崩潰。A 的移除將恢復系統調用為原始的 sys_open,將 B 排除出那個環。然後,當 B 被移除,它將恢復系統調用為 它 認為是原始的系統調用的不再存在於內存的 A_open。咋看起來我們好象可以通過檢查系統調用是否等於我們的函數及是否根本不去改變它(因此 當 B 被移除時不會改變系統調用)來解決這個問題,但那會制造更嚴重的問題。當 A 被移除,看似系統調用變為 B_open ,因此它不再指向 A_open,因而在它被從內存移除前它不會將它恢復成 sys_open。不幸的, B_open 將仍然試圖調用不再存在的 A_open ,因此即使不移除 B 系統也會崩潰。 (譯者認為無論是否進行檢查,系統都會在A被先移除的情況下在B還未移除時使系統崩潰,因為從作者假設的情況看,B會調用“它”認為的原始的系統調用來完成其功能,在沒有檢查的情況下,B一樣在其存儲原系統調用的變量中存儲A的函數A_OPEN並進行調用而使系統崩潰。即使B不調用“它”認為的原始的系統調用來完成其功能,系統也會崩潰,因為它無法恢復系統調用。)
我可以想到兩個預防的辦法。第一個是恢復調用為原始值 sys_open。不幸的, sys_open 不是內核系統表 /proc/ksyms 中的一部分,因此不能訪問它。另一個就是使用引用計數來防止一旦模塊被加載就可以隨便被 rmmod。這對產品模塊是一個好辦法,但不適合做教育性的范例--這也是我為什麼不在這做的原因。
ex syscall.c
/* syscall.c * * 系統調用“偷竊”范例 */ /* Copyright (C) 1998-99 by Ori Pomerantz */ /* 必要頭文件 */ /* 標准頭文件 */ #include /* 內核工作 */ #include /* 明確指定是模塊 */ /* 處理 CONFIG_MODVERSIONS */ #if CONFIG_MODVERSIONS==1 #define MODVERSIONS #include #endif #include /* 系統調用列表 */ /* 為了當前進程結構,我們需要這個知道當前用戶是誰 */ #include /* 在 2.2.3 版/usr/include/linux/version.h 包含該宏但 2.0.35 不包含 * 加入以備需要 */ #ifndef KERNEL_VERSION #define KERNEL_VERSION(a,b,c) ((a)*65536+(b)*256+(c)) #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) #include #endif /* 系統調用表(函數表)。我們只將定義為外部的即可,當我們insmod的時候內核會為我們填充它 */ extern void *sys_call_table[]; /* 我們想偵察的UID - 將從命令行填充 */ int uid; #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) MODULE_PARM(uid, "i"); #endif /* 原始的系統調用的指針。我們保存它而不是調用原始函數是因為其他某人可能在我們之前可能已經代替了 * 原始的系統調用(sys_open)。注意這並不是100%安全的,因為如果另一個模塊在我們之前代替了 * sys_open,然後當我們被插入內核,我們將調用那個模塊裡的函數-它可能在我們之前又被移除了。 * * 另一個原因是我們不能得到 sys_open。 它是靜態變量,因此是不可導出的。 */ asmlinkage int (*original_call)(const char *, int, int); /* 因為某些原因,在 2.2.3 版中current->uid 為0而非真正的用戶 ID。我試圖找出什麼地方錯了,但 * 不能在短時間內完成,而且我很懶-因此我僅僅使用系統調用得到UID,這是進程使用的方法。 * * 因為某些原因,在我重新編譯內核後這個問題沒有了。 */ asmlinkage int (*getuid_call)(); /* 我們將用來代替 sys_open 的函數 (當你調用 open 系統調用時這個函數被調用)。為了得到精確的原型 * 連同參數的數目和類型,我們首先找到了原始的函數(在 fs/open.c 中)。 * * 理論上,這意味著我們將局限於內核的當前版本。實際上,系統調用幾乎從來沒有變化 * (這將制造一場浩劫並且需要將所有的程序重新編譯,因為系統調用是內核和進程的接口)。 */ asmlinkage int our_sys_open(const char *filename, int flags, int mode) { int i = 0; char ch; /* 檢查是否是我們要偵察的用戶 */ if (uid == getuid_call()) { /* getuid_call 是 getuid 系統調用,它給出調用我們的系統調用的進程的用戶的UID。 */ /* 如果相關則報告文件 */ printk("Opened file by %d: ", uid); do { #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,2,0) get_user(ch, filename+i); #else ch = get_user(filename+i); #endif i++; printk("%c", ch); } while (ch != 0); printk("\n"); } /* 調用原始的 sys_open - 否則,我們將失去打開文件的能力 */ return original_call(filename, flags, mode); } /* 初始化模塊 - 代替系統調用 */ int init_module() { /* 警告 - 現在可能太遲了,但可能對下次... */ printk("I'm dangerous. I hope you did a "); printk("sync before you insmod'ed me.\n"); printk("My counterpart, cleanup_module(), is even"); printk("more dangerous. If\n"); printk("you value your file system, it will "); printk("be \"sync; rmmod\" \n"); printk("when you remove this module.\n"); /* 將原始的函數指針保存在 original_call,然後用我們的our_sys_open代替系統調用表中的相應系統調用 */ original_call = sys_call_table[__NR_open]; sys_call_table[__NR_open] = our_sys_open; /* 為了得到系統調用foo的函數地址,使用 sys_call_table[__NR_foo] 。 */ printk("Spying on UID:%d\n", uid); /* 得到 getuid 系統調用*/ getuid_call = sys_call_table[__NR_getuid]; return 0; } /* 清除 - 從/proc中注銷相關的文件 */ void cleanup_module() { /* 將系統調用恢復原狀 */ if (sys_call_table[__NR_open] != our_sys_open) { printk("Somebody else also played with the "); printk("open system call\n"); printk("The system may be left in "); printk("an unstable state.\n"); } sys_call_table[__NR_open] = original_call; }