乐于分享
好东西不私藏

Linux驱动加载源码分析(安全加载 、签名、校验)

Linux驱动加载源码分析(安全加载 、签名、校验)

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

PS: 这个只是基于《我自己》的理解,

如果和你的原则及想法相冲突,请谅解,勿喷。

环境说明

  无

前言


  很久很久以前,在android上面移植linux驱动的时候,由于一些条件限制,导致我们测试驱动非常的麻烦。其中有一个麻烦就是驱动校验失败,然后内核拒绝加载驱动。

  原则上来说,只要你对驱动进行签名或者配置,就能加载成功,但是当时赶时间验证,就想着直接把驱动校验部分的代码直接屏蔽了,达到了我们测试的目的。

  现在过了许久了,现在有经历来重温一下当初的问题,看看根源是什么,于是我们得了解驱动加载的通用流程,查看我们的驱动到底因为哪些原因加载失败。

linux驱动加载流程


  首先,我们知道linux驱动有两个关键入口函数,一般被module_init()/module_exit()宏进行处理。当我们想加载一个linux驱动的时候,一般我们使用insmod/modprobe来加载驱动,下面我们来看看执行insmod/modprobe时,到底发生了什么?

  经过简单的查询资料,驱动的处理涉及两个linux系统调用,他们是:

intsyscall(SYS_init_module, void module_image[.len], unsignedlong len,
constchar *param_values)
;
intsyscall(SYS_finit_module, int fd,
constchar *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,
unsignedlong, len, constchar __user *, uargs)
{
int err;
structload_infoinfo = { };

// ... ...

    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文件):

staticintload_module(struct load_info *info, constchar __user *uargs,
int flags)

{
structmodule *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 intdo_init_module(struct module *mod)
{
int ret = 0;
structmod_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)));


staticint __init hello_init(void)
{
    printk(KERN_INFO "Hello, World!\n");
return0
}

module_init(hello_init);

  上面可以看到,我们通过module_init()这个宏,我们声明了一个叫做init_module函数,且此函数是hello_init的别名(alias是gcc的扩展用法),换句话说我们调用init_module就等于调用了hello_init。

  此外,在我们生成ko文件的时候,还会看到一个被创建的xxx.mod.c的文件,里面有一个地方定义很重要:

__visible structmodule __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()函数:

staticintelf_validity_cache_copy(struct load_info *info, int flags)
{
unsignedint i;
    Elf_Shdr *shdr, *strhdr;
int err;
unsignedint num_mod_secs = 0, mod_idx;
unsignedint num_info_secs = 0, info_idx;
unsignedint 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;
            } elseif (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:

staticstruct module *layout_and_allocate(struct load_info *info, int flags)
{
structmodule *mod;
unsignedint 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: 有问题请留言,看到后我会第一时间回复。

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Linux驱动加载源码分析(安全加载 、签名、校验)

评论 抢沙发

1 + 8 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮