投稿が遅くなってしまってすみません。Linux Advent Calendar 2019 6日目の記事です。
今日はLinuxカーネルの5.4から搭載された、カーネルロックダウン機構を試してみようと思います。
カーネルロックダウン機能とは?
Linux(というかUNIX系OS)では、一般ユーザとrootユーザで権限を分離しておくことで、一般ユーザの不用意な操作でシステムの安定性が損なわれるようなケースを発生しにくくしています。しかしながら、rootユーザが不用意な操作を実行してしまった場合や、悪意のあるユーザからroot権限が奪取されてしまった場合には、この権限分離方法では対応できません。
カーネルロックダウン機構は、「rootユーザであってもシステムの変更を伴う操作に制限を付ける」機能となっており、カーネルロックダウンを設定したカーネルであれば、rootユーザであっても /dev/mem
, /dev/kmen
やCPU MSR(モデル固有レジスタ)へのアクセスがブロックされ、加えてカーネルの変更(=カーネルモジュールによる機能の追加)が行えなくなります。また、この機能はシステムの操作に強い制限を課すことから、既存のシステムにそのまま適用すると動かなくなることが懸念されるため、デフォルトでは無効となっています。
カーネルロックダウン機能を試してみる
機能の概要を把握したところで、さっそくカーネルロックダウン機能を試してみましょう。
まずはカーネルのビルドからです。今回はCentOS-7の環境でビルドしてみます。
# cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core)
カーネルソースコードの取得
Linuxカーネルは5.x系の最新版(2019/12/08時点)である 5.4.2
で試してみます。以下の手順でカーネルソースコードのダウンロードと展開を行います。以降のビルド手順は以前書いたLinuxカーネルのビルド手順に沿って行っています。
$ curl -O https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.4.2.tar.xz
$ cd /usr/src
$ sudo bash
# tar Jxf /home/centos/work/linux_build/linux-5.4.2.tar.xz
カーネルコンフィグでロックダウン機能を有効化する
カーネルソースコードが展開できたので、ロックダウン機能に関連するコンフィグを見てみましょう。
LOCK_DOWN_KERNEL_FORCE_
から始まるコンフィグがロックダウン機能に関する設定のようです。
# find . -type f | grep Kconfig | xargs grep LOCK_DOWN
./security/lockdown/Kconfig: default LOCK_DOWN_KERNEL_FORCE_NONE
./security/lockdown/Kconfig:config LOCK_DOWN_KERNEL_FORCE_NONE
./security/lockdown/Kconfig:config LOCK_DOWN_KERNEL_FORCE_INTEGRITY
./security/lockdown/Kconfig:config LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY
LOCK_DOWN_KERNEL_FORCE_INTEGRITY
を見てみると、(カーネルロックダウンを有効化した場合のデフォルトでは)"integrity"モードで稼働し、実行時におけるカーネルへの変更は無効化されるようです。
config LOCK_DOWN_KERNEL_FORCE_INTEGRITY
bool "Integrity"
help
The kernel runs in integrity mode by default. Features that allow
the kernel to be modified at runtime are disabled.
ということは、この設定を有効にすると、カーネルモジュールのロード時にエラーで弾かれるような動作になるような気がします。さっそくカーネルをビルドして試してみましょう。
以下の手順でLinuxカーネルのコンフィグを設定します。
$ cd linux-5.4.2
$ make defconfig
$ make menuconfig
LOCK_DOWN_KERNEL_FORCE_INTEGRITY
の設定箇所は以下の場所にあるようです。
-> Security options
-> Basic module for enforcing kernel lockdown
-> Kernel default lockdown mode
make defconfig
(Linuxカーネルのデフォルトコンフィグ)との差分は以下になります。
(CentOS-7以降の環境ではファイルシステムがXFSであるため、XFSをカーネルに組み込む設定にしておく必要があります)
# diff -ur .config.ORIG .config | grep -v '^ '
--- .config.ORIG 2019-12-07 16:45:20.264339000 +0900
+++ .config 2019-12-07 23:17:48.150270000 +0900
@@ -725,13 +725,22 @@
+CONFIG_MODULE_SIG_FORMAT=y
-# CONFIG_MODULE_SIG is not set
+CONFIG_MODULE_SIG=y
+# CONFIG_MODULE_SIG_FORCE is not set
+CONFIG_MODULE_SIG_ALL=y
+CONFIG_MODULE_SIG_SHA1=y
+# CONFIG_MODULE_SIG_SHA224 is not set
+# CONFIG_MODULE_SIG_SHA256 is not set
+# CONFIG_MODULE_SIG_SHA384 is not set
+# CONFIG_MODULE_SIG_SHA512 is not set
+CONFIG_MODULE_SIG_HASH="sha1"
@@ -3890,7 +3899,13 @@
-# CONFIG_XFS_FS is not set
+CONFIG_XFS_FS=y
+# CONFIG_XFS_QUOTA is not set
+# CONFIG_XFS_POSIX_ACL is not set
+# CONFIG_XFS_RT is not set
+# CONFIG_XFS_ONLINE_SCRUB is not set
+# CONFIG_XFS_WARN is not set
+# CONFIG_XFS_DEBUG is not set
@@ -4109,7 +4124,11 @@
-# CONFIG_SECURITY_LOCKDOWN_LSM is not set
+CONFIG_SECURITY_LOCKDOWN_LSM=y
+# CONFIG_SECURITY_LOCKDOWN_LSM_EARLY is not set
+# CONFIG_LOCK_DOWN_KERNEL_FORCE_NONE is not set
+CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY=y
+# CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY is not set
@@ -4332,6 +4351,7 @@
+CONFIG_MODULE_SIG_KEY="certs/signing_key.pem"
@@ -4369,7 +4389,7 @@
-# CONFIG_LIBCRC32C is not set
+CONFIG_LIBCRC32C=y
これで必要なカーネルコンフィグは設定できました。あとは make
を実行し、GRUBにビルドしたカーネルのエントリを追加します。
# make
# make modules_install \
&& cp -f arch/x86_64/boot/bzImage /boot/vmlinuz-5.4.2.x86_64 \
&& mkinitrd --force /boot/initramfs-5.4.2.x86_64.img 5.4.2 \
&& grub2-mkconfig -o /boot/grub2/grub.cfg
再起動し、GRUBから Linux-5.4.2
を選んで起動すれば準備完了です!
# uname -a
Linux linuxadvcal 5.4.2 #7 SMP Sat Dec 7 22:04:12 JST 2019 x86_64 x86_64 x86_64 GNU/Linux
カーネルモジュールの読み込みはバッチリブロックされる
試しに適当なカーネルモジュールをロードしてみると、rootユーザであってもモジュールのロードが失敗(=ブロックされる)します。
# insmod /usr/lib/modules/5.4.2/build/net/netfilter/xt_nat.ko
insmod: ERROR: could not insert module /usr/lib/modules/5.4.2/build/net/netfilter/xt_nat.ko: Operation not permitted
この時、コンソールには以下のメッセージが出力されます。
[ 459.212341] Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7
LOCK_DOWN_KERNEL_FORCE_INTEGRITY
の設定により、カーネルモジュールは一つも読み込まれていない状態になっています。
# lsmod
Module Size Used by
#
dmesg
の内容をみると、Linuxの起動直後に"Kernel is locked down from Kernel configuration;"というメッセージが出力されているので、「起動後のカーネル変更は無効化する」という LOCK_DOWN_KERNEL_FORCE_INTEGRITY
の設定に沿った挙動になっています。
# dmesg -T | head -n1 ; dmesg -T | egrep '(Kernel configuration|kernel_lockdown)'
[日 12月 8 00:38:21 2019] Linux version 5.4.2 (root@linuxadvcal) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-28) (GCC)) #7 SMP Sat Dec 7 22:04:12 JST 2019
[日 12月 8 00:38:20 2019] Kernel is locked down from Kernel configuration; see man kernel_lockdown.7
[日 12月 8 00:38:21 2019] Lockdown: swapper/0: hibernation is restricted; see man kernel_lockdown.7
[日 12月 8 00:38:55 2019] Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7
せっかくだからソースコードも見てみる
カーネルロックダウン機能を簡単に試してみました。挙動は何となくわかったのですが、実際にソースコードも眺めてみたくなるのが人情(?)です。さっそくソースコードも簡単に見てみましょう。
カーネルモジュールをロードした時のブロッキングはどういう流れになっている?
まずは insmod
した時にエラーにしている(モジュールのロードをブロッキングしている)箇所を探してみましょう。調査の手掛かりになるのはコンソールに出力された以下のメッセージでしょう。
[日 12月 8 00:38:55 2019] Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7
上記のメッセージを出力しているのは以下の個所になります。
79 /**
80 * lockdown_is_locked_down - Find out if the kernel is locked down
81 * @what: Tag to use in notice generated if lockdown is in effect
82 */
83 static int lockdown_is_locked_down(enum lockdown_reason what)
84 {
...
89 if (kernel_locked_down >= what) {
90 if (lockdown_reasons[what])
91 pr_notice("Lockdown: %s: %s is restricted; see man kernel_lockdown.7\n",
92 current->comm, lockdown_reasons[what]);
93 return -EPERM;
94 }
メッセージ中の"unsigned module loaded"の部分は、 lockdown_readsons[what]
から取得される文字列で、どうやら LOCKDOWN_MODULE_SIGNATURE
という定数で参照されるようです。
19 static const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX+1] = {
...
21 [LOCKDOWN_MODULE_SIGNATURE] = "unsigned module loading",
LOCKDOWN_MODULE_SIGNATURE
の実体はenum値となっています。
104 enum lockdown_reason {
105 LOCKDOWN_NONE,
106 LOCKDOWN_MODULE_SIGNATURE,
...
LOCKDOWN_MODULE_SIGNATURE
が参照されている箇所は、 security/lockdown/lockdown.c
(いまソースを追いかけている箇所)と kernel/module.c
の2箇所のみのようです。
kernel/module.c
では mod_verify_sig()
の返り値で処理を分岐しており、そこで LOCKDOWN_MODULE_SIGNATURE
を引数にした security_locked_down()
を呼び出す構造になっています(2882行目)。
2839 #ifdef CONFIG_MODULE_SIG
2840 static int module_sig_check(struct load_info *info, int flags)
2841 {
...
2851 if (flags == 0 &&
2852 info->len > markerlen &&
2853 memcmp(mod + info->len - markerlen, MODULE_SIG_STRING, markerlen) == 0) {
2854 /* We truncate the module to discard the signature */
2855 info->len -= markerlen;
2856 err = mod_verify_sig(mod, info);
2857 }
2858
2859 switch (err) {
...
2871 case -ENOPKG:
2872 reason = "Loading of module with unsupported crypto";
2873 goto decide;
...
2876 decide:
2877 if (is_module_sig_enforced()) {
2878 pr_notice("%s is rejected\n", reason);
2879 return -EKEYREJECTED;
2880 }
2881
2882 return security_locked_down(LOCKDOWN_MODULE_SIGNATURE);
2883
2884 /* All other errors are fatal, including nomem, unparseable
2885 * signatures and signature check failures - even if signatures
2886 * aren't required.
2887 */
2888 default:
2889 return err;
2890 }
security_locked_down()
は kernel/module.c
で定義されており、そこからさらに call_int_hook()
を呼んでいます。
2402 int security_locked_down(enum lockdown_reason what)
2403 {
2404 return call_int_hook(locked_down, 0, what);
2405 }
2406 EXPORT_SYMBOL(security_locked_down);
call_int_hook()
は同じCファイル内で関数マクロとして定義されており、 security_hook_heads.FUNC()
を呼び出しています。これはマクロ展開されるため、 struct security_hook_hands->locked_down()
が呼び出される形になります。
38 struct security_hook_heads security_hook_heads __lsm_ro_after_init;
...
657 #define call_int_hook(FUNC, IRC, ...) ({ \
658 int RC = IRC; \
659 do { \
660 struct security_hook_list *P; \
661 \
662 hlist_for_each_entry(P, &security_hook_heads.FUNC, list) { \
663 RC = P->hook.FUNC(__VA_ARGS__); \
664 if (RC != 0) \
665 break; \
666 } \
667 } while (0); \
668 RC; \
669 })
struct security_hook_hands
のメンバ変数として locked_down
が定義されており、 security/lockdown/lockdown.c
内でこのメンバ変数に関数ポインタが設定されます。
1823 struct security_hook_heads {
...
2062 struct hlist_head locked_down;
2063 } __randomize_layout;
関数ポインタとして設定した lockdown_is_locked_down()
で引数 what
で渡したインデックス値が配列 lockdown_reasons[]
として参照可能な場合に、先述した "Lockdown: insmod: unsigned module loading is restricted; see man kernel_lockdown.7" を出力しています。
83 static int lockdown_is_locked_down(enum lockdown_reason what)
84 {
85 if (WARN(what >= LOCKDOWN_CONFIDENTIALITY_MAX,
86 "Invalid lockdown reason"))
87 return -EPERM;
88
89 if (kernel_locked_down >= what) {
90 if (lockdown_reasons[what])
91 pr_notice("Lockdown: %s: %s is restricted; see man kernel_lockdown.7\n",
92 current->comm, lockdown_reasons[what]);
93 return -EPERM;
94 }
95
96 return 0;
97 }
98
99 static struct security_hook_list lockdown_hooks[] __lsm_ro_after_init = {
100 LSM_HOOK_INIT(locked_down, lockdown_is_locked_down),
101 };
このケースでは、引数 what
の値は LOCKDOWN_MODULE_SIGNATURE
になっており、 "unsigned module loading" が参照されます。
19 static const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX+1] = {
20 [LOCKDOWN_NONE] = "none",
21 [LOCKDOWN_MODULE_SIGNATURE] = "unsigned module loading",
...
41 [LOCKDOWN_CONFIDENTIALITY_MAX] = "confidentiality",
42 };
lockdown_lsm_init()
内でカーネルコンフィグとして設定した LOCK_DOWN_KERNEL_FORCE_INTEGRITY
による条件コンパイルが行われ、ロックダウンレベルが LOCKDOWN_CONFIDENTIALITY_MAX
に設定されます。これは上記の配列 lockdown_reasons[]
において、 LOCKDOWN_MODULE_SIGNATURE
よりも大きな値で、これより小さい値(というか配列インデックス)の項目がカーネルロックダウンの要因として有効になるという挙動です。
51 static int lock_kernel_down(const char *where, enum lockdown_reason level)
52 {
53 if (kernel_locked_down >= level)
54 return -EPERM;
55
56 kernel_locked_down = level;
57 pr_notice("Kernel is locked down from %s; see man kernel_lockdown.7\n",
58 where);
59 return 0;
60 }
...
103 static int __init lockdown_lsm_init(void)
104 {
105 #if defined(CONFIG_LOCK_DOWN_KERNEL_FORCE_INTEGRITY)
106 lock_kernel_down("Kernel configuration", LOCKDOWN_INTEGRITY_MAX);
107 #elif defined(CONFIG_LOCK_DOWN_KERNEL_FORCE_CONFIDENTIALITY)
108 lock_kernel_down("Kernel configuration", LOCKDOWN_CONFIDENTIALITY_MAX);
109 #endif
110 security_add_hooks(lockdown_hooks, ARRAY_SIZE(lockdown_hooks),
111 "lockdown");
112 return 0;
113 }
ざっくりとした解説ですが、カーネルコンフィグで LOCK_DOWN_KERNEL_FORCE_INTEGRITY
を指定した場合の挙動の流れをソースコードから眺めることができました。
まとめ
Linuxのカーネルロックダウン機構について紹介してみました。セキュリティ周りのカーネルモジュールはそれ単体で挙動が完結するものではなく、モジュールによって設定された値や関数が他のソースから利用されるため、ソースコードを読み解くのが少々難しく感じます。 pr_notice()
と組み合わせ、実際に動かしながら挙動を読み解くのが良さそうです。