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函数,直接返回通过即可,这样即可测试。





后记


  通过阅读源码,感觉对内核各个模块的工作越来越熟悉了。

  但是越了解的多,越觉得未知越多。

参考文献




打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)
qrc_img

PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。

posted on 2024-07-14 19:12  SkyOnSky  阅读(17)  评论(0编辑  收藏  举报

导航