如果在命令行执行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 < ac->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