Linux驱动加载源码分析(安全加载 、签名、校验)
PS:要转载请注明出处,本人版权所有。
PS: 这个只是基于《我自己》的理解,
如果和你的原则及想法相冲突,请谅解,勿喷。
环境说明
无
前言
很久很久以前,在android上面移植linux驱动的时候,由于一些条件限制,导致我们测试驱动非常的麻烦。其中有一个麻烦就是驱动校验失败,然后内核拒绝加载驱动。
原则上来说,只要你对驱动进行签名或者配置,就能加载成功,但是当时赶时间验证,就想着直接把驱动校验部分的代码直接屏蔽了,达到了我们测试的目的。
现在过了许久了,现在有经历来重温一下当初的问题,看看根源是什么,于是我们得了解驱动加载的通用流程,查看我们的驱动到底因为哪些原因加载失败。
linux驱动加载流程
首先,我们知道linux驱动有两个关键入口函数,一般被module_init()/module_exit()宏进行处理。当我们想加载一个linux驱动的时候,一般我们使用insmod/modprobe来加载驱动,下面我们来看看执行insmod/modprobe时,到底发生了什么?
经过简单的查询资料,驱动的处理涉及两个linux系统调用,他们是:
int syscall(SYS_init_module, void module_image[.len], unsigned long len,
const char *param_values);
int syscall(SYS_finit_module, int fd,
const char *param_values, int flags);
根据man手册介绍,SYS_init_module三个参数分别是内核驱动文件内容、文件内容长度、内核驱动参数。
下面我们深入内核看看,执行SYS_init_module时,到底发生了什么?
根据linux v6.9.6 kernel/module/main.c文件
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
// ... ...
err = copy_module_from_user(umod, len, &info);
// ... ...
return load_module(&info, uargs, 0);
}
这里最重要的就是通过copy_module_from_user给struct load_info赋值。
然后到了load_module函数(根据linux v6.9.6 kernel/module/main.c文件):
static int load_module(struct load_info *info, const char __user *uargs,
int flags)
{
struct module *mod;
bool module_allocated = false;
long err = 0;
char *after_dashes;
/*
* Do the signature check (if any) first. All that
* the signature check needs is info->len, it does
* not need any of the section info. That can be
* set up later. This will minimize the chances
* of a corrupt module causing problems before
* we even get to the signature check.
*
* The check will also adjust info->len by stripping
* off the sig length at the end of the module, making
* checks against info->len more correct.
*/
err = module_sig_check(info, flags);
if (err)
goto free_copy;
/*
* Do basic sanity checks against the ELF header and
* sections. Cache useful sections and set the
* info->mod to the userspace passed struct module.
*/
err = elf_validity_cache_copy(info, flags);
if (err)
goto free_copy;
err = early_mod_check(info, flags);
if (err)
goto free_copy;
/* Figure out module layout, and allocate all the memory. */
mod = layout_and_allocate(info, flags);
if (IS_ERR(mod)) {
err = PTR_ERR(mod);
goto free_copy;
}
// ... ...
return do_init_module(mod);
// ... ...
}
在 load_module 中,我们找到了3个重要的验证接口,一个是签名验证、一个是elf文件验证、一个是模块本身的信息验证。其中签名验证、模块本身的信息验证就是本文要关注的地方。经过了一系列的验证和初始化后,调用了do_init_module接口。
static noinline int do_init_module(struct module *mod)
{
int ret = 0;
struct mod_initfree *freeinit;
//... ...
/* Start the module */
if (mod->init != NULL)
ret = do_one_initcall(mod->init);
if (ret < 0) {
goto fail_free_freeinit;
}
if (ret > 0) {
pr_warn("%s: '%s'->init suspiciously returned %d, it should "
"follow 0/-E convention\n"
"%s: loading module anyway...\n",
__func__, mod->name, ret, __func__);
dump_stack();
}
//... ...
}
看这里的do_one_initcall(mod->init),就相当于调用了我们通过module_init()定义的接口了。
但是这里有一个问题?那就是mod->init是module_init()定义的接口,那它是怎么赋值的呢?要回答这个问题,还要回到我们创建一个ko文件的时候,有两个地方我们需要关注,这里我们随便创建一个helloworld的驱动为例:
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, World!\n");
return 0;
}
module_init(hello_init);
上面可以看到,我们通过module_init()这个宏,我们声明了一个叫做init_module函数,且此函数是hello_init的别名(alias是gcc的扩展用法),换句话说我们调用init_module就等于调用了hello_init。
此外,在我们生成ko文件的时候,还会看到一个被创建的xxx.mod.c的文件,里面有一个地方定义很重要:
__visible struct module __this_module
__section(.gnu.linkonce.this_module) = {
.name = KBUILD_MODNAME,
.init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
.exit = cleanup_module,
#endif
.arch = MODULE_ARCH_INIT,
};
注意看这里的__this_module这个变量,这个变量其成员有init_module这个函数的地址信息,也就有了hello_init的地址信息,且这个__this_module变量被放到了.gnu.linkonce.this_module这个section里面。
如果了解elf文件格式的,一定对section这个东西不陌生,其存放了很多elf相关内容,在这里,我们只需要关注.gnu.linkonce.this_module小节,就是__this_module的地址,这个会在驱动加载的时候用上。
上面我们知道了init_module被放置到__this_module.init字段去了,那么执行do_one_initcall(mod->init)时,mod->init是怎么初始化的呢?下面我们接着分析mod->init的赋值,首先我们要回到SYS_init_module调用时,有一个load_module函数,在load_module函数中,有一个elf_validity_cache_copy()函数:
static int elf_validity_cache_copy(struct load_info *info, int flags)
{
unsigned int i;
Elf_Shdr *shdr, *strhdr;
int err;
unsigned int num_mod_secs = 0, mod_idx;
unsigned int num_info_secs = 0, info_idx;
unsigned int num_sym_secs = 0, sym_idx;
//... ...
for (i = 1; i < info->hdr->e_shnum; i++) {
shdr = &info->sechdrs[i];
switch (shdr->sh_type) {
// ... ...
default:
// ... ...
if (strcmp(info->secstrings + shdr->sh_name,
".gnu.linkonce.this_module") == 0) {
num_mod_secs++;
mod_idx = i;
} else if (strcmp(info->secstrings + shdr->sh_name,
".modinfo") == 0) {
num_info_secs++;
info_idx = i;
}
// ... ...
}
}
// ... ...
info->index.mod = mod_idx;
/* This is temporary: point mod into copy of data. */
info->mod = (void *)info->hdr + shdr->sh_offset;
/// ... ...
}
这里其实就是遍历section数组,然后得到.gnu.linkonce.this_module在section数组中的idx,并记录到info->index.mod中。(此处如果不明白,建议可以简单看看elf格式介绍,本文不分析这个)
然后在load_module函数中的layout_and_allocate()中,会处理info->index.mod:
static struct module *layout_and_allocate(struct load_info *info, int flags)
{
struct module *mod;
unsigned int ndx;
int err;
// ... ...
/* Module has been copied to its final place now: return it. */
mod = (void *)info->sechdrs[info->index.mod].sh_addr;
kmemleak_load_module(mod, info);
return mod;
}
在此函数对mod赋值的过程中,就把ko文件的__this_module变量的地址,绑定给了mod,然后mod往后面传,就可以执行mod->init函数了,也就是执行hello_init。
驱动校验加载
对上文我们提到的load_module中有三个驱动校验相关的函数:
- module_sig_check
- elf_validity_cache_copy
- early_mod_check
其中elf_validity_cache_copy是对驱动二进制格式进行校验的,一般我们正常的驱动是满足条件的。因此,我们主要是去解决module_sig_check和early_mod_check的问题。
对于module_sig_check来说,就是利用签名算法(可参考之前文章《常用加密及其相关的概念、简介(对称、AES、非对称、RSA、散列、HASH、消息认证码、HMAC、签名、CA、数字证书、base64、填充)》 https://www.cnblogs.com/Iflyinsky/p/18076852 ),保证内核驱动使用了内核认可的私钥进行签名,然后内核使用公钥进行验证。
对于early_mod_check来说,就是校验内核版本信息、模块信息等等,这里就不详细介绍了。
总的来说,如果我们要关闭内核的相关校验,可以通过以下的配置,或者直接处理module_sig_check、early_mod_check两个函数即可达到我们的目的。
CONFIG_MODULE_SIG=y
CONFIG_MODULE_SIG_FORCE=y
CONFIG_MODULE_SIG_ALL=y
CONFIG_MODULE_SIG_SHA256=y
CONFIG_MODVERSIONS=y
特别注意,如果是在android系统里面,有些情况下(例如qcom的源码),你关闭了这些检测,会导致android系统编译失败,因为android kernel配置的安全检测无法通过。所以需要直接修改module_sig_check和early_mod_check函数,直接返回通过即可,这样即可测试。
后记
通过阅读源码,感觉对内核各个模块的工作越来越熟悉了。
但是越了解的多,越觉得未知越多。
参考文献
- 《常用加密及其相关的概念、简介(对称、AES、非对称、RSA、散列、HASH、消息认证码、HMAC、签名、CA、数字证书、base64、填充)》 https://www.cnblogs.com/Iflyinsky/p/18076852
PS: 请尊重原创,不喜勿喷。
PS: 要转载请注明出处,本人版权所有。
PS: 有问题请留言,看到后我会第一时间回复。