如果在命令行執行kill -9 1,那么結果是沒有反應,連個提示都沒有,實際上init進程是殺不死的,到底為何呢?kill指令實際上是發信號,如果一個進程對一個信號沒有反應那么 原因可能有以下三點:1.該進程屏蔽了此信號;2.該進程是內核線程,手動屏蔽了此信號;3.內核忽略了此信號.我們看看init進程,它不是內核線程 (實際上在rest_init之初的init是內核線程,只是它馬上exec到用戶空間了),而且SIGKILL(9)是用戶線程所不能忽略和屏蔽的,因 此只有第三種可能,內核忽略了此信號,找找代碼,看看下面的函數就得到了確切的答案。
int?get_signal_to_deliver(siginfo_t?*info,?struct?k_sigaction?*return_ka,?struct?pt_regs?*regs,?void?*cookie) { ... ?????????spin_lock_irq(¤t->sighand->siglock); ?????????for?(;;)?{ ... ?????????????????if?(current->pid?==?1)//由pid判斷是否傳送信號,執行到此,信號只可能是SIGKILL或STOP之類的巨猛信號了,如果是init進程那么忽略,不向它傳送 ?????????????????????????continue; ... ?????????????????do_group_exit(signr);????????????? ?????????} ?????????spin_unlock_irq(¤t->sighand->siglock); ?????????return?signr; }因此我們知道內核只是通過進程的pid來識別init進程的,如果我們找到init,然后修改它的pid,使之不再為1,是否就可以殺死init進程了呢?理論是這樣,難道真的是這么簡單嗎?于是我寫下以下的模塊:
#include? #include? static?__init?int?test_init(void) { ????task_t?*task=find_task_by_pid(1);??//找到1號進程的task_struct結構指針????? ????????task->pid?=?3314;??//將init的pid改為一個沒有使用的pid ????????force_sig(SIGKILL,task);??//然后殺死它,就是殺死init進程 ????????return?0; } static?__exit?void?test_exit(void) { ????return?; } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("Dual?BSD/GPL"); MODULE_AUTHOR("Zhaoya"); MODULE_DESCRIPTION("kill?init"); MODULE_VERSION("Ver?0.1");結果如何呢?當然是系統崩潰,linux臨死前coredump,調用堆棧中有choose_new_parent函數,找到choose_new_parent函數看了一下:
static?inline?void?choose_new_parent(task_t?*p,?task_t?*reaper,?task_t?*child_reaper) { ?????????BUG_ON(p?==?reaper?||?reaper->state?>=?TASK_ZOMBIE); ?????????p->real_parent?=?reaper; ?????????if?(p->parent?==?p->real_parent)??//這里出錯,p是init的孩子,p->parent當然是init,而reaper也是init,所以if為真,BUG乎! ?????????????????BUG(); }那里為何出錯呢?實際上SIGKILL信號真的發給了init,然后init就do_exit了,當中調用了以下函數:參數的father就是init本身,該函數過繼了init的孩子們給一個新的父親。
static?inline?void?forget_original_parent(struct?task_struct?*?father,?struct?list_head?*to_release) { ?????????struct?task_struct?*p,?*reaper?=?father; ?????????struct?list_head?*_p,?*_n; ?????????do?{ ?????????????????reaper?=?next_thread(reaper); ?????????????????if?(reaper?==?father)?{ ?????????????????????????reaper?=?child_reaper;//child_reaper在初始化時設置為init進程,于是到此為止father和reaper是一樣的。 ?????????????????????????break; ?????????????????} ?????????}?while?(reaper->state?>=?TASK_ZOMBIE); ?????????list_for_each_safe(_p,?_n,?&father->children)?{ ?????????????????int?ptrace; ?????????????????p?=?list_entry(_p,struct?task_struct,sibling); ?????????????????ptrace?=?p->ptrace; ?????????????????BUG_ON(father?!=?p->real_parent?&&?!ptrace); ?????????????????if?(father?==?p->real_parent)?{ ?????????????????????????choose_new_parent(p,?reaper,?child_reaper);//到了此點。 ?????????????????????????reparent_thread(p,?father,?0); ?????????????????}?else?{ ... }因此init進程不可殺并不是用戶空間的策 略,而是內核的機制,是操作系統的一部分,操作系統用這個機制實現了很多事情,比如僵尸進程管理回收問題,多用戶安全問題等等,不要指望殺死init了, 即使通過rootkit做到了,試問有意義嗎?就是個兒戲罷了,沒有實用性的。
?? 下面談談內核野指針問題,這其實是一個爭論的話題,爭論主題就是要將不用的指針清零還是采用懶惰策略,待下次使用的時候再清零。前者更安全,后者更高效, 兩全不得其美,必選其一。如果說發生了內核錯誤,我的期望是馬上出錯馬上崩潰,不然等到以后出錯的時間就是不確定的了,那會很不安全,同樣給調試帶來困 難,安全性永遠比性能重要。
?? 我采用了一個很極端的例子,在一個進程執行的時候釋放掉其task_struct,為了使事情簡單,我的進程如下:
int?main() { ????while(1){} }編譯后運行,ps后它的pid是2732,然后寫如下模塊:
#include? #include? static?__init?int?test_init(void) { ????task_t?*task=find_task_by_pid(2732);?? ????free_task(task); ????return?0; } static?__exit?void?test_exit(void) { ?????????return?; } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("Dual?BSD/GPL"); MODULE_AUTHOR("Zhaoya"); MODULE_DESCRIPTION("free?task"); MODULE_VERSION("Ver?0.1");加 載模塊后,只要不要動鍵盤,一點問題沒有,程序依舊完好地運行,內核沒有崩潰,但是一旦敲入ps,內核立馬崩潰,讓我們來看看這是為什么。簡單起見,我用 2.6.9內核分析。free_task實際調用kmem_cache_free(task_struct_cachep, (task)),而后者就是:
static?inline?void?__cache_free?(kmem_cache_t?*cachep,?void*?objp) { ?????????struct?array_cache?*ac?=?ac_data(cachep); ?????????check_irq_off(); ?????????objp?=?cache_free_debugcheck(cachep,?objp,?__builtin_return_address(0)); ?????????if?(likely(ac->avail?limit))?{ ?????????????????STATS_INC_FREEHIT(cachep); ?????????????????ac_entry(ac)[ac->avail++]?=?objp; ?????????????????return; ?????????}?else?{ ?????????????????STATS_INC_FREEMISS(cachep); ?????????????????cache_flusharray(cachep,?ac); ?????????????????ac_entry(ac)[ac->avail++]?=?objp; ?????????} }僅此而已,操作task_struct的字段了嗎?沒有,實際上 task_struct還是那個task_struct,一點沒變,內核要想取該task_struct的某個字段,照取不誤,還是原來的地址(內核的前 896m與物理內存一一對應),只是操作了slab的指針。因此進程依舊完好運行,那為何一旦ps就崩潰了呢?或者ls也崩潰。我們知道,你執行ps或 ls以及任何一個命令的時候,shell都要fork出一個新進程,而fork要分配一個task_struct,該task_struct當然在 slab分配,我們看看分配代碼:
static?inline?void?*?__cache_alloc?(kmem_cache_t?*cachep,?int?flags) { ?????????unsigned?long?save_flags; ?????????void*?objp; ?????????struct?array_cache?*ac; ?????????cache_alloc_debugcheck_before(cachep,?flags); ?????????local_irq_save(save_flags); ?????????ac?=?ac_data(cachep); ?????????if?(likely(ac->avail))?{ ?????????????????STATS_INC_ALLOCHIT(cachep); ?????????????????ac->touched?=?1; ?????????????????objp?=?ac_entry(ac)[--ac->avail]; ?????????}?else?{ ?????????????????STATS_INC_ALLOCMISS(cachep); ?????????????????objp?=?cache_alloc_refill(cachep,?flags); ?????????} ?????????local_irq_restore(save_flags); ?????????objp?=?cache_alloc_debugcheck_after(cachep,?flags,?objp,?__builtin_return_address(0)); ?????????return?objp; }看看這個--ac->avail,再對 比一下前面釋放時的ac->avail++,這里最新分配的task_struct就是最新被釋放的task_struct,而這個 task_struct就是那個我的模塊中被變態釋放的task_struct了,于是新進程一產生,原來的被釋放的進程的task_struct的所有 字段幾乎都要被重置,如果我敲入了ps,那么那個while循環進程的task_struct將和ps的task_struct一樣,因此亂套崩潰在情理 之中,為何亂套?最簡單的例子,內核要調度while,切出while循環,但是他們的task_struct一樣,如果你是內核你該咋辦,絕對崩潰,要 你放下一個桔子拿起另一個桔子,而這兩個桔子是一個桔子,你難道不崩潰嗎?呵呵。
?? 因此,我認為因該在slab對象初始化時只初始化公共部分,只要釋放一個slab對象到slab中,那么就把非公共部分清零,這樣才安全,把一切清零,不 要什么工公共部分更安全,當然這樣slab的ctor構造函數也就沒有必要了,不過這樣的效率會小低一些,僅僅小低一些。還是為了簡單,我寫下如下模塊, 釋放后隨即將整個結構清零,不保留任何公共部分:
#include? #include? static?__init?int?test_init(void) { ????task_t?*task=find_task_by_pid(1824);?? ????free_task(task); ????memset(task,0,sizeof(task_t));??//將task_struct清零,實際上就是將其內部的字段清零,包括指針變量,由此消除了野指針 ????return?0; } static?__exit?void?test_exit(void) { ?????????return?; } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("Dual?BSD/GPL"); MODULE_AUTHOR("Zhaoya"); MODULE_DESCRIPTION("zero?task"); MODULE_VERSION("Ver?0.1");還 是以上面的while循環作試驗,加載模塊后,內核立即崩潰,達到了目的。但是內核是怎么崩潰的呢?很簡單,典型的有兩個時機,一個是時鐘中斷,一個是調 度,當然還有別的很多,只是這兩個更具有典型性,在這兩個時機場景的處理時候會用到current,而它就是被釋放那的task_struct,字段全是 0,這樣很容易就崩潰了,可能訪問了0指針也就是空指針,也可能是別的。在應用程序設計時,我們大談特談野指針的危害,內核中也要關注它,不過不必像用戶 空間那么過分關注它,畢竟內核中做的事情應該形成一種約定,這樣就不用浪費資源去驗證這驗證那了,內核做的事情要少而有效,最重要的是保證安全。
task_struct來自slab,而slab有ctor構造函數和dtor析構函數,dtor我們想想實際上是沒有什么用的,于是在最新的內核中就去掉了,不再為slab對象提供析構函數了(參考slub)。
附:2.6內核模塊的編譯1.寫好模塊c文件,文件名為:XX.c;
2.寫Makefile文件,內容:obj-m += XX.o
3.make -C /lib/modules/`uanem -r`/build/ SUBDIRS=$PWD modules
評論
查看更多