Linux IMA 启用与体验
简单认识 IMA
内核完整性子系统可用于检测文件是否被远程或本地更改。它会评估一个文件的度量值(哈希值),并将其与先前存储为扩展属性的“良好”值进行比较(在 ext3、ext4 等支持扩展属性的文件系统上)。其他安全技术(如 SELinux)也提供了类似但互补的机制,可以根据策略尝试保护文件完整性。
内核完整性子系统由两个主要组件组成:
- IMA(Integrity Measurement Architecture,完整性度量架构)能够基于自定义策略对通过
execve()
、mmap()
和open()
系统调用访问的文件进行度量,度量结果可被用于本地/远程证明,或者和已有的参考值比较以控制对文件的访问。 - EVM(Extended Verification Module,扩展验证模块)能将系统当中某个文件的安全扩展属性,包括
security.ima
、security.selinux
等合起来计算一个哈希值,然后使用 TPM 中存的密钥或其他可信环境中的密钥对其进行签名,签名之后的值存在security.evm
中,这个签名后的值是不能被篡改的,如果被篡改,再次访问的时候就会验签失败。EVM 的作用就是通过对安全扩展属性计算摘要和签名并将其存储在security.evm
中,提供对安全扩展属性的离线保护。
Linux IMA 子系统在 Linux 内核中引入了钩子,以支持在文件内容被读取或执行之前,在打开时创建和收集文件的哈希值。IMA 度量子系统于 Linux 2.6.30 中被引入。
根据 IMA wiki 的定义,内核完整性子系统的功能可以被分为三部分:
- 度量(measure):检测对文件的意外或恶意修改,无论远程还是本地。
- 评估(appraise):度量文件并与一个存储在扩展属性中的参考值作比较,控制本地文件完整性。
- 审计(audit):将度量结果写到系统日志中,用于审计。
如果系统中存在 TPM 芯片,度量结果将被扩展到 TPM 芯片的指定 PCR 寄存器中,由于 PCR 扩展的单向性以及 TPM 芯片的硬件安全性,用户无法修改已被扩展的度量结果,这就确保了度量结果的真实性。
如果任何被监视的文件被修改(例如系统更新时),可以进行 IMA 重新度量。为此,需要使用 i_version
选项挂载文件系统。要在文件更改后重新度量它,文件系统必须支持 i_version
。例如,要在根文件系统上启用 i_version
,可以这样编辑 /etc/fstab
文件:
1 | /dev/vda1 / ext4 noatime,iversion 1 2 |
IMA 评估
IMA 评估的目的是通过与标准参考值的比较,控制对本地文件的访问。相比于 IMA 度量作为一个“只记录不干涉”的观察员,IMA 评估更像是一位严格的保安人员,它的职责是拒绝对所有“人证不一”的程序的访问。
IMA 首先使用安全扩展属性 security.ima
和 security.evm
存储文件完整性度量的参考值:
security.ima
:存储文件内容的哈希值。security.evm
:存储文件扩展属性的哈希值签名。
访问受保护文件时,将会触发内核中的钩子,依次验证文件扩展属性和内容的完整性:
- 使用内核 keyring 中的公钥对文件
security.evm
扩展属性中的签名值验签,与当前文件扩展属性的哈希值比较,如果匹配就证明文件的扩展属性是完整的(包括security.ima
)。 - 在文件扩展属性完整的前提下,将文件
security.ima
扩展属性的内容与当前文件内容的哈希值比较,如果匹配就允许对文件的访问。
IMA 评估的文件范围和触发条件可以由用户通过 IMA 策略自行配置。
启用 IMA 评估需要两个步骤:
- 首先,使用内核命令行参数
ima_appraise_tcb
和ima_appraise='fix'
重新启动内核,为文件系统重新标记。接下来需要读取所有要评估的文件,这将需要一些时间。要重新标记整个文件系统,可以运行以下命令:
1 | time find / -fstype ext4 -type f -uid 0 -exec dd if='{}' of=/dev/null count=0 status=none \; |
一个比较新的 Ubuntu Server 22.04 系统需要花费的时间约为五分钟。
完成后,文件的扩展属性中就能显示存储的哈希值。例如:
1 | neko@ubuntu:~$ getfattr -m - -d /sbin/init |
- 随后使用
ima_appraise_tcp
和ima_appraise=enforce
内核命令行参数重启。
在开启 IMA 评估的情况下,每当访问一个可执行文件或动态库文件,就会调用内核中的钩子,计算文件内容和扩展属性的哈希值,并在内核哈希表中进行搜索,如果匹配就允许文件的执行,否则就拒绝访问。如果启用了审计,还会产生审计事件。
扩展 Linux 操作系统为其增加 IMA 功能
Ubuntu 官方的内核已默认编译 IMA/EVM,并且也已经自动挂载了 securityfs 文件系统,只需在内核启动参数中启用 IMA 即可。
编辑 /etc/default/grub
文件,在 GRUB_CMDLINE_LINUX
参数中可以添加 Linux 启动时的内核命令行参数,不同参数之间用空格分隔。要启用 IMA 只需要附加上 ima_tcb ima_appraise_tcb ima_appraise=fix
即可。如果原来没有参数,改完后应该是这样的:
1 | GRUB_CMDLINE_LINUX="ima_tcb ima_appraise_tcb ima_appraise=fix" |
与 IMA 相关的内核参数有如下几个:
ima=on
Fedora/RHEL 可能需要开启ima_appraise=<policy>
,IMA 评估策略,其中参数可填以下几个off
关闭 IMA 评估模式,在访问文件时不进行完整性校验,也不为文件生成新的参考值。enforce
开启 IMA 评估强制模式,在访问文件时进行完整性校验,即计算文件哈希值并与参考值比对,如果比对失败就拒绝对文件的访问。IMA 会为新文件生成新的参考值。fix
开启 IMA 修复模式,在该模式下允许更新受保护文件的参考值,允许系统在没有参考值的情况下启动。log
开启 IMA 评估日志模式,在访问文件时进行完整性校验,但即使校验失败也允许执行命令,只进行日志记录。
ima_policy=<policy>
,IMA 策略,其中参数可填以下几个tcb
度量所有文件执行、动态库映射、内核模块导入以及设备驱动加载,此外,root 用户读文件的行为也会被度量。appraise_tcb
评估 root 拥有的所有文件。secure_boot
对所有内核模块导入、硬件驱动加载、kexec 内核切换以及 IMA 策略进行评估,前提是这些文件都具有 IMA 签名。ima_policy
参数可以同时指定多个值,例如ima_policy=tcb|appraise_tcb
,启动后系统的 IMA 策略就是这两种参数对应的策略的总和。
ima_tcb
等价于ima_policy=tcb
ima_appraise_tcb
等价于ima_policy=appraise_tcb
ima_template=<template used>
IMA 度量扩展模板,默认值为ima-ng
,可填值为ima/ima-ng/ima-sig
ima_hash=<hash used>
IMA 摘要算法,ima
模板 默认为sha1
,Linux 3.13 默认值为sha256
,可填值为sha1/md5/sha256/sha512/wp512/…
ima_audit
审核日志记录- 0 基础完整性审计信息(默认)
- 1 额外完整性审计信息
修改后使用 sudo update-grub
自动生成文件更新/boot/grub/grub.cfg
,然后重启电脑。
重启后可使用 sudo cat /sys/kernel/security/ima/ascii_runtime_measurements
查看 IMA 记录的应用度量值,此文件即 IMA 维护的运行时度量列表。内容较长,这里截取一部分展示:
1 | 10 1d8d532d463c9f8c205d0df7787669a85f93e260 ima-ng sha1:0000000000000000000000000000000000000000 boot_aggregate |
表中各列为:
- 用于扩展度量结果的 PCR(Platform Configuration Register,平台配置寄存器),默认是 10,只在系统装了 TPM 芯片的情况下有意义。
- 模板哈希值:最终被用于扩展的哈希值,组合了文件内容哈希和文件路径的长度和值。
- 扩展度量值的模板(在本例中是
ima-ng
)。 - 由文件内容生成的哈希值。
- 被度量的文件路径。
编写程序观察 IMA 工作
普通程序
随便编写一个程序,编译执行,比如一个简单的 hello world 程序:
1 | // hello.c |
随后再次使用 sudo cat /sys/kernel/security/ima/ascii_runtime_measurements
查看 IMA 记录的应用度量值,仔细找找,可以在末尾几行中找到自己刚刚执行的文件。
1 | 10 6ff2505abba95a0f71dd83e6a32fbd39b1644c34 ima-ng sha1:383597be6110792b8553fb1463850670b69504ad /home/neko/a.out |
使用 cat /sys/kernel/security/ima/runtime_measurements_count
查看 IMA 日志条数,可以看到相比运行前会根据 IMA 记录的条目增加相应数量。
共享库
编写一个共享库,然后尝试使用这个共享库:
1 | // shared_hello.c |
1 | // use_hello.c |
使用 gcc -o libhello.so -fPIC -shared shared_hello.c
将 shared_hello.c
编译成动态共享库 libhello.so
。
-fPIC
参数声明链接库的代码段是可以共享的,-shared
参数声明编译为共享库。Linux 共享库的一个命名的惯例为 libxxx.so
。
使用 gcc -o hello -L. use_hello.c -lhello
编译 use_hello.c
。
-L
参数指定到哪个附加路径下面去寻找共享库,现在我们指定在当前目录下面寻找。-l
参数指定链接到哪个共享库上面,参数 -lhello
会自动链接到 libhello.so
这个共享库上面。
使用 export LD_LIBRARY_PATH=/home/neko:$LD_LIBRARY_PATH
将当前目录加入到共享库路径环境变量中,以便程序运行时动态查找并加载动态共享库。
随后再次使用 sudo cat /sys/kernel/security/ima/ascii_runtime_measurements
查看 IMA 记录的应用度量值,仔细找找,可以在末尾几行中找到自己刚刚执行的文件和对应的动态库。
1 | 10 0572cb41970bd27be429dd8e1640aadd788882d2 ima-ng sha1:4a030a72bf7ea3a03cf00b3b848f8ae9ad2f6cb1 /home/neko/libhello.so |
使用 cat /sys/kernel/security/ima/runtime_measurements_count
查看 IMA 日志条数,可以看到相比运行前会根据 IMA 记录的条目增加相应数量。
内核模块
编写一个简单的内核模块:
1 | // kmod_hello.c |
编写一个 Makefile 文件以管理模块的编译(注意 Makefile 文件的缩进需要使用 Tab,不能用空格):
1 | // Makefile |
将 kmod_hello.c
和 Makefile
放在同一目录下,执行 make
命令编译模块。
注意 Makefile
文件中的命令行(如上例中两句 make
)需要用 tab 制表符开头,不能使用空格。如果出现类似 Makefile:4: *** missing separator. Stop.
的错误,可能是因为这一原因,可以重新编辑 Makefile
文件,将命令行开头修改为 tab。
在开启 Secure Boot 后若模块未签名,则可能导致在使用 sudo insmod kmod_hello.ko
加载模块时报错 insmod: ERROR: could not insert module kmod_hello.ko: Operation not permitted
。使用 dmesg
查看内核信息时可在末尾发现类似 Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7
的错误,即因为模块未签名导致加载失败。对内核模块的签名具体请参考【译】如何为 Secure Boot 签名。
使用 sudo insmod kmod_hello.ko
加载模块,并使用 sudo dmesg | tail -5
查看 kernel message
1 | // sudo dmesg | tail -5 |
如果在加载模块时出现 insmod: ERROR: could not insert module kmod_hello.ko: Invalid module format
错误。使用 dmesg
查看内核信息时在末尾发现类似 kmod_hello: disagrees about version of symbol module_layout
的错误,是因为模块编译时对应的内核版本与当前运行内核版本不一致导致加载失败;若是类似 module: x86/modules: Skipping invalid relocation target, existing value is nonzero for type 1, loc 00000000981bd3a1, val ffffffffc0986000
的错误,可能是当前内核自身有问题,可以尝试使用包管理器重新安装内核或者使用自行编译的内核。
随后再次使用 sudo cat /sys/kernel/security/ima/ascii_runtime_measurements
查看 IMA 记录的应用度量值,仔细找找,可以在末尾几行中找到刚刚加载的内核模块。
1 | 10 56e7459158b8b239ac43a820800ba69eae766f6f ima-ng sha1:755d642ae4fab794747b3413643ba603a15fddc1 /home/neko/kmod_hello.ko |
使用 cat /sys/kernel/security/ima/runtime_measurements_count
查看 IMA 日志条数,可以看到相比运行前会根据 IMA 记录的条目增加相应数量。
使用 sudo rmmod kmod_hello.ko
卸载模块,并使用 sudo dmesg | tail -5
查看 kernel message
1 | // sudo dmesg |
随后再次使用 sudo cat /sys/kernel/security/ima/ascii_runtime_measurements
查看 IMA 记录的应用度量值,可以发现刚刚卸载的内核模块仍有记录,因为这是 IMA 度量的日志,并不会随着程序的结束或模块卸载被删去。
另外注意 IMA 只是起到度量并记录作用,并不会进行校验与认证。如果两次运行同一个文件名但文件内容不同,会被重新度量并记录。