Linuxカーネルのロックダウン機構を試してみる

投稿が遅くなってしまってすみません。[Linux Advent Calendar 2019](https://qiita.com/advent-calendar/2019/linux) 6日目の記事です。
今日はLinuxカーネルの[5.4](https://lkml.org/lkml/2019/11/24/187)から搭載された、カーネルロックダウン機構を試してみようと思います。

## カーネルロックダウン機能とは？

Linux(というかUNIX系OS)では、一般ユーザとrootユーザで権限を分離しておくことで、一般ユーザの不用意な操作でシステムの安定性が損なわれるようなケースを発生しにくくしています。しかしながら、rootユーザが不用意な操作を実行してしまった場合や、悪意のあるユーザからroot権限が奪取されてしまった場合には、この権限分離方法では対応できません。

カーネルロックダウン機構は、「rootユーザであってもシステムの変更を伴う操作に制限を付ける」機能となっており、カーネルロックダウンを設定したカーネルであれば、rootユーザであっても `/dev/mem` , `/dev/kmen` やCPU MSR(モデル固有レジスタ)へのアクセスがブロックされ、加えてカーネルの変更(＝カーネルモジュールによる機能の追加)が行えなくなります。また、この機能はシステムの操作に強い制限を課すことから、既存のシステムにそのまま適用すると動かなくなることが懸念されるため、デフォルトでは無効となっています。

## カーネルロックダウン機能を試してみる

機能の概要を把握したところで、さっそくカーネルロックダウン機能を試してみましょう。
まずはカーネルのビルドからです。今回はCentOS-7の環境でビルドしてみます。

```sh
# cat /etc/redhat-release
CentOS Linux release 7.5.1804 (Core)
```

### カーネルソースコードの取得

Linuxカーネルは5.x系の最新版(2019/12/08時点)である `5.4.2` で試してみます。以下の手順でカーネルソースコードのダウンロードと展開を行います。以降のビルド手順は[以前書いたLinuxカーネルのビルド手順](https://qiita.com/furandon_pig/items/379ceacf234134798df2)に沿って行っています。

```sh
$ 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_` から始まるコンフィグがロックダウン機能に関する設定のようです。

```sh
# 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カーネルのコンフィグを設定します。

```sh
$ 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をカーネルに組み込む設定にしておく必要があります)

```sh
# 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にビルドしたカーネルのエントリを追加します。

```sh
# 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` を選んで起動すれば準備完了です！

```sh
# 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ユーザであってもモジュールのロードが失敗(＝ブロックされる)します。

```sh
# 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` の設定により、カーネルモジュールは一つも読み込まれていない状態になっています。

```sh
# 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
```

上記のメッセージを出力しているのは以下の個所になります。

```c:security/lockdown/lockdown.c
 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` という定数で参照されるようです。

```c:security/lockdown/lockdown.c
 19 static const char *const lockdown_reasons[LOCKDOWN_CONFIDENTIALITY_MAX+1] = {
 ...
 21         [LOCKDOWN_MODULE_SIGNATURE] = "unsigned module loading",
```

`LOCKDOWN_MODULE_SIGNATURE` の実体はenum値となっています。

```c:include/linux/security.h
 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行目)。

```c:kernel/module.c
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()` を呼んでいます。

```c:security/security.c
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()` が呼び出される形になります。

```c:security/security.c
  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` 内でこのメンバ変数に関数ポインタが設定されます。

```c:include/linux/lsm_hooks.h
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" を出力しています。

```c:security/lockdown/lockdown.c
 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" が参照されます。

```c:security/lockdown/lockdown.c
 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` よりも大きな値で、これより小さい値(というか配列インデックス)の項目がカーネルロックダウンの要因として有効になるという挙動です。

```c:security/lockdown/lockdown.c
 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()` と組み合わせ、実際に動かしながら挙動を読み解くのが良さそうです。

## 参考URL

 * [Why lock down the kernel](https://events19.linuxfoundation.org/wp-content/uploads/2017/12/Why-lock-down-the-kernel_Matthew-Garrett.pdf)(pdf)
 * [Linuxにカーネル「ロックダウン」セキュリティ機能追加へ](https://japan.zdnet.com/article/35143344/)(ZDNet Japan)
 * [Linus Torvalds To Add “Lockdown” Security Feature In Linux 5.4](https://fossbytes.com/kernel-lockdown-linux-kernel-5-4/)
 * [第1回 知られざるセキュリティフレームワーク「LSM」の役割](https://www.atmarkit.co.jp/flinux/rensai/lsm01/lsm01a.html) * [第1回 知られざるセキュリティフレームワーク「LSM」の役割](https://www.atmarkit.co.jp/flinux/rensai/lsm01/lsm01a.html)(page2 )