目的
把进来的漏洞堵上,防止其他人进入系统,尤其是阻止有可能威胁rootkit的代码。如Anit-rootkit,这个有些难度,我们先实现了基本的控制内核模块的加载。
例子:小偷潜入了银行,要保证偷偷在银行这段时间不会被警察给揪出来。
通知链机制
在实现我们如何控制内核模块加载前,我们先简要介绍一下linux的通知链机制。
大多数的内核子系统都是相互独立的,因此有可能某个子系统对其他子系统产生的事件感兴趣。为了满足这个需求,linux提供了通知链的机制,使得某个子系统在发生某个事件时通知其他的子系统。
通知链的数据结构如下:
struct notifier_block
{
int (*notifier_call)(struct notifier_block *self, unsigned long, void *);
struct notifier_block *next;
int priority;
};
所以这个数据结构是一个函数链表,每个节点都注册了一个函数,当某个事件发生时链表上的所有节点对应的函数就会被执行。
另外通知这个事件使所运行的函数由被通知方决定,就是说被通知方可以在链表上注册一个通知函数,在发生某个事件的时候,这些函数就会得到执行,从而通知到被通知方。
我们这里的事件是加载模块。所以现在我们来分析一下内核中加载模块的代码。首先是init_module,在init_module下函数会返回一个load_module函数,所以我们跟进到load_module函数,会发现有好多流程,比如检查签名、prepare_coming_module、使模块与sysfs发生联系、执行模块入口函数等等。我们关注的是prepare_coming_module,因为这里将会调用注册的通知处理函数。然后我们继续跟进到prepare_coming_module,会发现它有个blocking_notifier_call_chain函数传入了MODULE_STATE_COMING参数,还有通知链表module_notify_list。它会调用通知链中的通知处理函数。这里相当于内核告诉模块通知链的通知处理函数一个信:MODULE_STATE_COMING,即一个模块准备好了,同时把这个模块传递给处理函数。继续跟进,会发现有这样一行代码说明在这里调用我们的通知处理函数。
ret = notifier_call_chain(&nh->head, val, v, nr_to_call,nr_calls);
所以,加载模块这个事件使用了linux通知链的机制,即模块加载时会执行通知链表上的通知处理函数。于是我们就可以编写一个通知处理函数,当其他模块完成加载,开始初始化前的状态为module_state_coming时,我们篡改他的入口函数和出口函数,达到了其他模块伪加载的目的。
实现
- 实现通知处理函数
- 加锁
- 当模块状态为MODULE_STATE_COMING,我们篡改其入口函数与出口函数。
- 解锁
- 注册入口函数和注销出口函数
加锁的目的是为了保证篡改成功。
//保存中断状态加锁。
spin_lock_irqsave(&module_notifier_spinlock, flags);
switch (module->state) {
case MODULE_STATE_COMING:
printk("Replacing init and exit functions: %s.\n",
module->name);
// 篡改模块的初始函数与退出函数。
module->init = fake_init;
module->exit = fake_exit;
break;
default:
break;
}
// 恢复中断状态解锁。
spin_unlock_irqrestore(&module_notifier_spinlock, flags);
功能展示
guard目录下是阻止其他内核模块的加载,lamb目录下是一个简单的模块。
正常情况下lamb目录下模块的加载是这样的。
输入:
结果:
而当加载了module_block模块后,简单模块被其阻止加载了。
输入:
实验结果:
参考: