Linux内核安全模块深入剖析【2.8】

发布时间:2026/5/24 18:26:42

Linux内核安全模块深入剖析【2.8】 12.3 伪文件系统IMA/EVM 使用 securityfs 文件系统构建内核和用户态接口。securityfs 通常挂载于/sys/kernel/security。IMA/EVM 在其中创建了两个文件和一个子目录。1evm用户态程序通过这个文件通知内核 evm 子系统密钥“evm-key”已经准备好。写此文件需要能力“CAP_SYS_ADMIN”只能向这个文件写入 ASCII 字符“1”。写入一次后再写就会得到“EPERM”错误。读文件会得到当前 evm 的状态1 表示就绪0 表示还没有初始化。2ima这是一个目录其下的文件都和 ima 有关。1binary_runtime_measurements2ascii_runtime_measurements这两个文件都是只读文件用来显示内核中管理的度量文件列表。binary_runtime_ measurements 以二进制的形式输出列表内容而 ascii_runtime_measurements 是文本形式。下面简要叙述一下 ascii_runtime_measurements 文件的内容每行一个记录对应一个文件的完整性度量。第一个记录特殊对应的是引导程序显示文件名的地方是“boot_aggregate”计算哈希值的输入是 TPM 硬件的 8 个寄存器 pcr0pcr7。每行的第一 列是分配 给 IMA 使用的 PCR 寄存器由 编译选项 CONFIG_IMA_ MEASURE_PCR_IDX 决定取值范围 814缺省为 10。第二列是哈希值第三列是模板名称第四列是模板对应的文件信息文件内容的哈希值、文件名、数字签名。需要解释一下模板了。IMA 有三个模板名称分别为ima、ima-ng、ima-sig。在 ima 模板中计算哈希时的输入消息包括文件内容和文件名。ima-ng 和 ima 类似也是针对文件内容和文件名计算哈希但是可使用的算法更多。ng 就是 next generation下一代的意思。ima-sig 和 ima-ng 类似但是计算哈希的输入多了一个数字签名。3runtime_measurements_count这是只读文件显示内核中管理的度量文件列表中文件的数量。4violations只读文件显示 violation 的次数。针对的是这样一种问题若一个进程以读模式打开文件另一个进程以写模式打开文件则这个文件的度量和使用就会存在冲突。5policy只写文件一行对应一条策略。12.4 命令行参数IMA/EVM 使用的内核启动命令行参数有些多下面逐一介绍。1evmfix进程访问文件时内核为访问到的文件的安全相关扩展属性生成相应的 HMAC 值存入文件的扩展属性 security.evm 之中。如果文件已经有扩展属性 security.evm并且其中保存的是数字签名那么内核不会生成 HMAC 值存入 security.evm。下面这条命令为所有属主是 root 的文件生成扩展属性 security.evmfind / -fstype ext4 -type f -uid 0 -exec head -n 1 {} /dev/null \;2ima_appraiseoff|fix进程访问文件时内核为访问到的文件生成哈希值存入扩展属性 security.ima 之中。如果扩展属性 security.ima 已经有值并且值是数字签名则不会生成哈希值存入扩展属性security.ima。3ima_tcb有此参数表示使用内核 IMA 代码中预先定义的度量规则。用户可以再通过伪文件接口/sys/kernel/security/ima/policy 添加规则。4ima_appraise_tcb有此参数表示使用内核 IMA 代码中预先定义的评估规则。用户可以再通过伪文件接口/sys/kernel/security/ima/policy 添加规则。5ima_hashima_hashmd4|md5|sha1|rmd160|sha256|sha384|sha512|sha224|rmd128|rmd256| rmd320|wp256|wp384|wp512|tgr128|tgr160|tgr192为 IMA 选择一种哈希算法。在 ima_template 为“ima”的情况下只能选择“sha1”或者“md5”。6ima_templateima|ima-ng|ima-sig为 IMA 选择一种模板。7integrity_audit0|1是否产生一些说明性的审计消息。12.5 总结IMA/EVM 被认为是 TCG 开发标准中的一个组成部分并且 IMA/EVM 使用了 TPM 提供的功能所以本章从可信计算开始讲述。理想中的可信如空中楼阁现实中的可信不得不降格为完整性。而真正意义的完整性同样是高高在上现实中完整性又降格为哈希计算。而哈希计算的对象也仅仅是文件。虽然计算的内容包括文件内容、文件名甚至数字签名虽然文件在系统中很重要但是这么做只是覆盖了系统的一部分谈不上像 SELinux 那样全系统覆盖而更重要的是这么做只做到了保护静态完整性至于动态的完整性即进程的完整性则无从谈起。IMA 的评估功能和 EVM 都依赖于扩展属性这两项功能主要是依赖扩展属性中存储的值来验证完整性但是扩展属性中的值是什么时候存入的呢它很难在系统本次运行中写入需要在之前的某次系统启动时填入。这就给系统升级带来了困难。12.6 参考资料读者可参考以下资料。K. J. Biba. Integrity Considerations for Secure Computer Systems, 1975. http://seclab. cs.ucdavis.edu/projects/history/papers/biba75.pdf http://nchc.dl.sourceforge.net/project/linux-ima/linux-ima/Integrity_overview.pdf http://sourceforge.net/p/linux-ima/wiki/Home/http://linux-ima.sourceforge.net/evmctl.1.html习题IMA 可以使用 TPM 提供的功能来保障完整性。在没有 TPM 的情况下IMA 使用文件的扩展属性。有些文件系统如 MS-DOS 的 FAT不支持扩展属性。设计一种方案使 IMA 可以在没有扩展属性的情况下发挥完整性保护的作用。第 13 章 dm-verity13.1 Device Mapperdm-verity 是内核子系统 Device Mapper 的一个子模块所以在介绍 dm-verity 之前先要介绍一下 Device Mapper 的基础知识。Device Mapper 为 Linux 内核提供了一个从逻辑设备到物理设备的映射框架通过它用户可以定制存储资源的管理策略。当前 Linux 中逻辑卷管理器如 LVM2Linux Volume Manager 2、EVMSEnterprise Volume Management System、dmraid 等都是基于该机制实现的。Device Mapper 架构如图 13-1 所示。其中有三个重要概念映射设备Mapped Device、映射表、目标设备Target Device。映射设备是一个逻辑块设备用户可以像使用其他块设备那样使用映射设备。映射设备通过映射表所描述的映射关系和目标设备建立映射。对映射设备的读写操作最终要映射成对目标设备的操作。而目标设备本身不一定是一个实际的物理设备它可以是另一个映射设备如此循环往复理论上可以无限迭代下去。映射关系本质上就是表明映射设备中的地址对应到哪个目标设备的哪个地址。下面看一个例子root #echo -e 0 1024 linear /dev/sda 0\\n\1024 1024 linear /dev/sdb 0\\n\2048 1024 linear /dev/sdc 0\\n\3072 1024 linear /dev/sdd 0 \| dmsetup create test-linear产生的设备架构如图 13-2 所示。映射设备名为“test-linear”映射到 4 个目标设备映射关系是第 0 个 block 到第 1023 个 block 映射到第一个目标设备第 1024 个 block 到第 2047 个block 映射到第二个目标设备第 2048 个 block 到第 3071 个 block 映射到第 3 个目标设备第3072 个 block 到第 4095 个 block 映射到第 4 个目标设备。映射设备中的块读写要由具体的目标设备实现如何实现又依赖于目标设备的类型。这个例子中目标设备的类型都是“linear”。linear需要额外两个参数设备名和块block起始地址在这个例子中所有目标设备的块起始地址都是 0。linear 是一种比较简单的类型就是简单的线性对应。这个例子中针对每个目标设备都是从 0 起始的 1024 个 block 被分配给了名为“test-linear”的映射设备。Device Mapper 是一个灵活的架构映射设备映射一个或多个目标设备每个目标设备属于一个类型类型不同对 I/O 的处理不同构造目标设备的方法也不同。虽然上面的例子中所有的目标设备都是一个类型——linear但这并不是硬性要求映射设备可以映射为多个不同类型的目标设备。有的类型有额外要求比如本章讲述的 dm-verity 规定只能有两个目标设备一个是数据设备Data Device另一个是哈希设备Hash Device。13.2 dm-verity 简介dm-verity 是 Device Mapper 架构下的一种目标设备类型。通过它来保障设备或设备分区的完整性。它的典型架构如图 13-3 所示。dm-verity 类型的设备需要两个“底层”设备一个是数据设备顾名思义用来存储数据实际上就是要保障完整性的设备另一个是哈希设备用来存储哈希值在校验数据设备完整性时需要。图 13-3 中表示的是 dm-verity 的一种典型应用也是简单直接的应用。图中映射设备和目标设备是一对一关系对映射设备的读操作被映射成对目标设备的读操作在目标设备中 dm-verity 又将读操作映射为对数据设备Data Device的读操作。但是在读操作的结束处 dm-verity 加了一个额外的校验操作对读到的数据计算一个哈希值用这个哈希值和存储在哈希设备Hash Device中的值比较如果不同则本次读操作被标记为错误。下面介绍一下哈希值的存储和使用如图 13-4 所示。假设数据设备和哈希设备中每块大小均为 4KB再假设使用哈希算法 SHA256即每块数据的哈希值为 32B256 bits则哈希设备中的每块4KB存储有 4096/32128 个哈希值。所以在 layer 0 中一个哈希设备的块对应数据设备的 128 个块。到这里似乎完整了数据设备中存储数据哈希设备中存储哈希值。在读取数据时计算数据的哈希值和存储在哈希设备中的值比较根据结果决定读操作是否成功。这还不够dm-verity 还要防备哈希设备中存储的哈希值被篡改的情况。所以要加上 layer 1在 layer 1 中的每块数据对应 layer 0 中的 128 个块当然也可以比 128 少layer 1 中的数据就是对 layer 0 中数据计算哈希值如果 layer 1 中只有一块那么就此停止否则继续增加 layer直到 layer nlayer n 中只有一块。最后对 layer n 再计算哈希值称这个哈希值为 root hash。这个哈希值就是对数据设备中各块和哈希设备中各块——layer 0 到 layer n——进行了一个复杂的哈希运算。因此数据设备和哈希设备中数据的变化就会反映为 root hash 的变化。通过验证 root hash 就可以检验数据是否被篡改。最后提一下dm-verity 设备必须被只读使用。这个不难理解。但是有一个问题就是哈希设备中的数据是如何建立的。答案是内核不负责建立要先由用户态工具比如 veritysetup “格式化”相应的设备。然后内核在其上建立 dm-verity 设备。13.3 代码分析13.3.1 概况目标设备的类型体现为结构体 target_type其中定义了若干函数指针分别对应构建、解构、映射 I/O、结束 I/O、暂停、恢复等操作。而 dm-verity 对应的 target_type 的定义是drivers/md/dm-verity.c static struct target_type verity_target { .name verity, .version {1, 2, 0}, .module THIS_MODULE, .ctr verity_ctr, .dtr verity_dtr, .map verity_map, .status verity_status, .ioctl verity_ioctl, .merge verity_merge, .iterate_devices verity_iterate_devices, .io_hints verity_io_hints, }; target_type 的定义为 include/linux/device-mapper.h struct target_type { uint64_t features; const char *name; struct module *module; unsigned version[3]; dm_ctr_fn ctr; dm_dtr_fn dtr; dm_map_fn map; dm_map_request_fn map_rq; dm_endio_fn end_io; dm_request_endio_fn rq_end_io; dm_presuspend_fn presuspend; dm_postsuspend_fn postsuspend; dm_preresume_fn preresume; dm_resume_fn resume; dm_status_fn status; dm_message_fn message; dm_ioctl_fn ioctl; dm_merge_fn merge; dm_busy_fn busy; dm_iterate_devices_fn iterate_devices; dm_io_hints_fn io_hints; /* For internal device-mapper use. */ struct list_head list; }结构体 target_type 的主要成员是很多函数指针。本章分析其中最常用的 map映射和 ctr 构建函数指针在 verity_target 中这两个函数指针指向 verity_map 和 verity_ctr。13.3.2 映射函数verity_map下面要分析三个层次的代码逻辑。第一层是块设备第二层是 Device Mapper第三层是作为 Target Device 的 dm-verity。1.块设备在执行之前代码逻辑先要在块设备架构中转上一大圈。首先看看块设备中的接口函数block/blk-core.c void generic_make_request(struct bio *bio) { ... do { struct request_queue *q bdev_get_queue(bio-bi_bdev); q-make_request_fn(q, bio); bio bio_list_pop(current-bio_list); } while (bio); current-bio_list NULL; /* deactivate */ }函数 generic_make_request 的函数主体是一个循环循环处理每一个 bio处理 bio 的工作 由函数指针 make_request_fn 所指向的函数完成。函数指针 make_request_fn 的赋值是在函数 blk_queue_make_request 中完成的 block/blk-settings.c void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn) { q-nr_requests BLKDEV_MAX_RQ; q-make_request_fn mfn; ... } 2.Device Mapper 上面是块设备的代码逻辑下面看看在 Device Mapper 中如何赋值这个函数指针 drivers/md/dm.c static void dm_init_md_queue(struct mapped_device *md) { ... blk_queue_make_request(md-queue, dm_request); ... } Device Mapper 要将 make_request_fn 指针赋值为 dm_request。dm_request 的定义如下 drivers/md/dm.c static void dm_request(struct request_queue *q, struct bio *bio) { struct mapped_device *md q-queuedata; if (dm_request_based(md)) blk_queue_bio(q, bio); else _dm_request(q, bio); } 代码逻辑从 dm_request 开始经过一系列函数调用最终会调用__map_bio drivers/md/dm.c static void __map_bio(struct dm_target_io *tio) { ... r ti-type-map(ti, clone); ... } 从这里代码逻辑就进入了具体的 Target Device。 3. dm-verity “map”是一个函数指针对于 dm-verity 设备它指向函数 verity_map。代码还是很简单的 drivers/md/dm-verity.c static int verity_map(struct dm_target *ti, struct bio *bio) { struct dm_verity *v ti-private; struct dm_verity_io *io; bio-bi_bdev v-data_dev-bdev; bio-bi_iter.bi_sector verity_map_sector(v,bio-bi_iter.bi_sector); ... io dm_per_bio_data(bio, ti-per_bio_data_size); io-v v; io-orig_bi_end_io bio-bi_end_io; io-orig_bi_private bio-bi_private; ... bio-bi_end_io verity_end_io; bio-bi_private io; ... verity_submit_prefetch(v, io); generic_make_request(bio); return DM_MAPIO_SUBMITTED; }它主要做了两件事第一件是将 bio 的 bi_end_io 函数指针和数据成员 bi_private 换掉将原有值保存以便以后恢复。第二件事是调用块设备的函数 generic_make_request 启动对dm-verity 的 data device 的操作。按照块设备的代码逻辑在 io 操作之后bi_end_io 函数指针会被调用对于 dm-verity 来说就是 verity_end_io。下面看看 bi_end_io 指针所指向的verity_end_io就知道 verity 要干什么了drivers/md/dm-verity.c static void verity_end_io(struct bio *bio, int error) { struct dm_verity_io *io bio-bi_private; if (error) { verity_finish_io(io, error); return; } INIT_WORK(io-work, verity_work); queue_work(io-v-verify_wq, io-work); }如果读写出错就调用 verity_finish_io此函数恢复原有的 bi_end_io 指针和 bi_private 指针 drivers/md/dm-verity.c static void verity_finish_io(struct dm_verity_io *io, int error) { struct dm_verity *v io-v; struct bio *bio dm_bio_from_per_bio_data(io, v-ti-per_bio_data_size); bio-bi_end_io io-orig_bi_end_io; bio-bi_private io-orig_bi_private; bio_endio_nodec(bio, error); } 重要的工作在 verity_work 函数中 drivers/md/dm-verity.c static void verity_work(struct work_struct *w) { struct dm_verity_io *io container_of(w, struct dm_verity_io, work); verity_finish_io(io, verity_verify_io(io)); } verity_work 会调用 verity_verify_io此函数完成最重要的工作——校验。 drivers/md/dm-verity.c static int verity_verify_io(struct dm_verity_io *io) { … for (b 0; b io-n_blocks; b) { … if (likely(v-levels)) { … int r verity_verify_level(io, io-block b, 0, true); if (likely(!r)) goto test_block_hash; if (r 0) return r; } memcpy(io_want_digest(v, io), v-root_digest, v-digest_size); for (i v-levels - 1; i 0; i--) { int r verity_verify_level(io, io-block b, i, false); if (unlikely(r)) return r; } test_block_hash: … result io_real_digest(v, io); r crypto_shash_final(desc, result); if (r 0) { DMERR(crypto_shash_final failed: %d, r); return r; } if (unlikely(memcmp(result, io_want_digest(v, io), v-digest_size))) { DMERR_LIMIT(data block %llu is corrupted, (unsigned long long)(io-block b)); v-hash_failed 1; return -EIO; } } return 0; }verity_verify_io 函数的定义比较长这里省略了许多。函数的主体是一个循环对 io 中所有的 block 进行校验。在循环体中又分为两部分在标号 test_block_hash 之前的部分是对哈希设备进行校验在标号 test_block_hash 之后的部分是对数据设备进行校验。回顾一下dm-verity需要两个“底层”设备一个是数据设备用来存储数据另一个是哈希设备存储的是数据的校验值。参考图 13-4在哈希设备中存储是分层的。最底层是 0 层每个块中存储的是数据设备中对应块的数据的校验值其上每层都存储着下一层的校验值。在此函数中首先检验第 0 层数据的正确性。这里用了一个技巧检验的结果是被缓存了的如果以前检验的结果为正确 verity_verify_level(io, io-block b, 0, true)就会返回 0然后函数就直接到 test_block_hash 之后去执行校验数据设备的操作。如果之前没有做过校验就要老老实实地验证哈希设备的完整性。0 层由 1 层校验1 层由 2 层校验……顶层由 root hash 校验。函数又用了一个技巧它是自顶向下做校验的因为一开始“好的”校验值只有一个就是 root hash。经过 root hash 校验后 levels-1 层存储的校验值就成了“好的”校验值又可以用它们来校验 levels-2 层了以此类推直到第 0 层。校验操作无非是算出一个校验值用此值和一个事先存储的校验值比较。函数中将好的校验值存储在缓冲区 io_want_digest(v, io)之中。首先在本函数 verity_verify_io中将 root hash 填入这个缓冲区然后在函数 verity_verify_level 中做验证验证通过后就会将新的校验值填入这个缓冲区。在标号 test_block_hash 之前第 0 层的哈希设备的数据一定是被存储在了缓冲区 io_want_ digest(v, io)之中的。标号 test_block_hash 之后的部分所做的工作就是读出数据设备中的数据块内容算出校验值和 io_want_digest(v, io)之中的内容进行比较如果相同就通过了校验。

相关新闻