hardening のための sysctl と カーネル起動オプションと kconfig

[Linux advent カレンダー](https://qiita.com/advent-calendar/2021/linux) の二日目です。一日目は hoglet氏の [Linux入門 1-2 各コマンドとviコマンドの基本](https://qiita.com/hoglet/items/edb0cb47b7705570492d)です。基本的にemacsを使っていますが、viも１億行程度のテキストファイルでも問題なく編集できるので重宝しています。

 Hardening とは堅牢化です。非root一般ユーザーがroot権限を奪取したり、アカウントを持たない攻撃者がネットワーク経由でLinuxを操作する手段を減らすのが主な目的です。Debian を前提にしていますが他のディストロでも使えることは多いはずです。Linux **5.15**とARM64とIntel/AMD64で使えることを確認済み。性能の低下に気づくようなhardeningは避けています。以下の設定をラズパイ４を用いて、1Gbit光回線で[IPv4 over IPv6変換器](https://qiita.com/kakinaguru_zo/items/2764dd8e83e54a6605f2)を作っても、 https://minsoku.net を用いて回線速度を無駄なく使い切れてることを確認しています。

# sysctl を用いたhardening
以下の内容を `/etc/sysctl.d/local.conf` に書いておくと次回の起動以降で有効になります。

## TCP/IP以外
<dl>
<dt>kernel.randomize_va_space=2</dt>
<dd><a href="https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E7%A9%BA%E9%96%93%E9%85%8D%E7%BD%AE%E3%81%AE%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E5%8C%96">アドレス空間配置のランダム化</a> を行う</dd>
<dt>vm.mmap_rnd_bits=32<br />vm.mmap_rnd_compat_bits=16</dt><dd>アドレス空間配置をよりランダムにする</dd>
<dt>vm.mmap_min_addr=32678</dt><dd>アドレス空間の最初の32 KiBを使わないようにする</dd>
<dt>net.core.bpf_jit_harden=2<br />kernel.unprivileged_bpf_disabled=1</dt><dd>BPFを安全にする</dd>
<dt>vm.unprivileged_userfaultfd=0</dt><dd>root以外がuser fault fd機能を使わないなら0にしたほうがよい</dd>
<dt>fs.protected_hardlinks=1<br />fs.protected_symlinks=1<br />fs.protected_fifos=2<br />fs.protected_regular=2</dt><dd>/tmp などの誰でも書けるディレクトリにあるファイルやFIFOを保護する</dd>
</dl>

## TCP/IP関連

```config
net.ipv4.conf.default.rp_filter=1
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.accept_source_route=0
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.default.arp_announce=1
net.ipv4.conf.all.arp_announce=1
net.ipv4.conf.default.arp_ignore=2
net.ipv4.conf.all.arp_ignore=2
net.ipv6.conf.default.accept_source_route=0
net.ipv6.conf.all.accept_source_route=0
```

説明は https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html を参照

# カーネル起動オプションを用いたhardening
`grub` を用いて起動している場合(AMD/Intel) ときは `/etc/default/grub` の内容を編集して `update-grub` を実行すると次回起動時のカーネル起動オプションに反映される。ラズパイの場合は起動パーティションにある `cmdline.txt` を編集する。

<dl>
<dt>init_on_alloc=1</dt><dd>あるプロセスが新たに獲得したメモリ領域をゼロで初期化し、前に書き込まれた情報が漏れないようにする</dd>
<dt>slab_nomerge<br />page_alloc.shuffle=1</dt><dd>カーネルがユーザープロセスに割り付けるメモリ領域のアドレスをよりランダムにする</dd>
<dt>panic=1<br />oops=panic</dt><dd>OOPSが起きたらすぐにパニックする。おかしな状態になっているのに無理やり実行を続けることは付け込まれる危険性を増やすため</dd>
<dt>kpti=1 (ARM) または pti=on (AMD/Intel)<dt><dd>PTI (Page Table Isolation) を行いユーザープロセスに漏れるメモリに関する情報を減らす</dd>
<dt>vsyscall=none (AMD/Intelのみ)</dt><dd>危険なので今は避けるべきvsyscallを禁止する</dd>
</dl>

# カーネルを自分でコンパイルしなおして行うhardening
ディストロが提供するカーネルを用いて自動更新を有効にしておけば放っておいてもセキュリティホールが修正されていく。自分でカーネルをコンパイルする場合には、安全性を保つためにこまめに再コンパイルを行う必要があることに注意。以下の記事などを参考にして、www.kernel.org などから持ってきたカーネルのソースをコンパイルできる環境を整備しておく

* [Linuxカーネルビルド大全](https://qiita.com/progrunner/items/d2ab0a85b3881a4b7ed8)
* [Debian/Ubuntuでカーネルの設定(.config)を変えて再構築（ビルド）する](https://qiita.com/kakinaguru_zo/items/0a01e081be1e7b08dea1)
* [clangでカーネルコンパイル (Debian)](https://qiita.com/kakinaguru_zo/items/3a6bd2684240f9852dd9)

`-march=native` (AMD/Intel), `-mcpu=native` (ARM) などを付けてCPUに合わせた最適化をCコンパイラにさせるとよい。例えば `make KCFLAGS="-march=native -pipe" bindeb-pkg` などとする。

## キツイ最適化
最適化レベルを高めてカーネルをコンパイルすると起動しないことがよくあるが、以下のようなきつい最適化を掛けても手元のカーネルは取り敢えず起動している…(clang13 + Linux 5.15.2)

```shell
sed -i 's/-O2/-O3/g' Makefile
sed -i 's/-Os/-Oz/g' Makefile
sed -i 's/-flto/-flto -fwhole-program-vtables -fvirtual-function-elimination/g' Makefile
yes '' | make KCFLAGS="-fintegrated-cc1 -mcpu=native -faddrsig -fforce-emit-vtables -frtti -frtti-data -pipe -mllvm -polly -mllvm -polly-ast-use-context -mllvm -polly-invariant-load-hoisting -mllvm -polly-opt-fusion=max -mllvm -polly-run-inliner -mllvm -polly-vectorizer=stripmine -mllvm -polly-run-dce"  LLVM=1 LLVM_IAS=1 bindeb-pkg
```

## gcc で使えるKconfig項目
<dl>
<dt>CONFIG_ZERO_CALL_USED_REGS=y</dt><dd>関数から戻る際に使用したレジスタの内容を消す</dd>
<dt>CONFIG_GCC_PLUGINS=y</dt><dd>以下の設定項目を使えるようにする</dd>
<dt>CONFIG_GCC_PLUGIN_LATENT_ENTROPY=y</dt><dd>/dev/randomで使える乱数を増やす</dd>
<dt>CONFIG_GCC_PLUGIN_STACKLEAK=y</dt><dd>ユーザープロセスに制御を戻す前にカーネルスタックの内容を消す</dd>
<dt>CONFIG_GCC_PLUGIN_STRUCTLEAK_BYREF_ALL=y</dt><dd>カーネルが使うスタック領域を使う前に内容を消す</dd>
</dl>

## clang で使える項目
clangを用いてカーネルを作るためには `make LLVM=1 LLVM_IAS=1 bindeb-pkg` などとする

<dl>
<dt>CONFIG_INIT_STACK_ALL_ZERO=y</dt><dd>カーネル内でスタック領域を使用する前に内容を消す</dd>
<dt>CONFIG_SHADOW_CALL_STACK=y</dt><dd>関数の呼び出し元アドレスなどを通常の変数とは別のスタックに格納し書き換えられにくくする</dd>
<dt>CONFIG_LTO_CLANG_FULL=y</dt><dd>LTO (Link Time Optimization)する。以下の項目にはLTOが必要</dd>
<dt>CONFIG_TRIM_UNUSED_KSYMS=y</dt><dd>LTOによる最適化の効果を高める</dd>
<dt>CONFIG_CFI_CLANG=y<br />CONFIG_CFI_CLANG_SHADOW=y</dt><dd>CFI (control flow integrity) を用いてcontrol flowが勝手に書き換えられておかしなコードが実行されることを検出する</dd>
</dl>

## gcc, clang に共通のhardening項目
`linux-config-5.15` パッケージに含まれる、 `/usr/src/linux-config-5.15` に置いてあるDebian の標準 Kconfig で既に適切に設定されている項目については触れていない。Debianで既に適切に選ばれている項目も含めた一覧は https://kernsec.org/wiki/index.php/Kernel_Self_Protection_Project/Recommended_Settings ならびに https://github.com/a13xp0p0v/linux-kernel-defence-map がよい

<dl>
<dt>CONFIG_UBSAN=n<br />
CONFIG_UBSAN_BOUNDS=y<br />
CONFIG_UBSAN_ONLY_BOUNDS=y<br />
CONFIG_UBSAN_ARRAY_BOUNDS=y<br />
CONFIG_UBSAN_LOCAL_BOUNDS=n<br />
CONFIG_UBSAN_SANITIZE_ALL=y<br />
CONFIG_UBSAN_MISC=n<br />
CONFIG_UBSAN_UNREACHABLE=y<br />
CONFIG_UBSAN_SHIFT=y<br />
CONFIG_UBSAN_OBJECT_SIZE=n</dt><dd>未定義動作サニタイザ(UBSAN)。C言語で言うところの未定義動作がカーネル内部で生じているのは明らかにおかしいので、実行時に検出する。false positiveが出る項目といきなりトラップ処理に入る項目はｎにしている。目に見える性能低下は見られない</dd>
<dt>CONFIG_RANDOMIZE_KSTACK_OFFSET_DEFAULT=y</dt><dd>カーネルスタックの場所をランダム化する</dd>
<dt>CONFIG_ARM64_SW_TTBR0_PAN=y</dt><dd>PAN (Privileged Access Never)のエミュレーション (ARMのみ)</dd>
<dt>CONFIG_DEBUG_STACK_USAGE=y</dt><dd>スタックオーバーフローを検査する</dd>
<dt>CONFIG_CRYPTO_JITTERENTROPY=y</dt><dd>/dev/randomが使える乱数を増やす</dd>
<dt>CONFIG_WQ_WATCHDOG=y</dt><dd>work queueが固まったことを検出する</dd>
<dt>CONFIG_MCORE2=y<br />CONFIG_MK8=y<br />CONFIG_MATOM=yなど</dt><dd>(AMD/Intelのみ) Intel ATOMなどの新しいCPUに合わせたカーネルを作る。対象のCPUに合わせて適切に選択する。</dd>
</dl>

## あまり使われない項目の削除
自分の用途で使わない機能は攻撃者の踏み台になるだけなので、自分でカーネルをコンパイルしなおす場合削除することが望ましい。そういう意味で `make yes2modconfig` してすべての設定項目をモジュールに変更してから作ったカーネルで再起動し、 `make localmodconfig` で使わないモジュールを削るのも良い

<dl>
<dt>CONFIG_DEVKMEM=n<br />
CONFIG_DEVMEM=n<br />
CONFIG_DEVPORT=n</dt><dd>これらは /dev/kmem, /dev/mem, /dev/port を有効にするが今どきそれらを使うプログラムは無い</dd>
<dt>CONFIG_KEXEC=n</dt><dd>カーネルを置き換える危険なkexecシステムコールを禁止</dd>
<dt>CONFIG_USERFAULTFD=n</dt><dd>User Fault FDはオラクルのデータベースくらいでしか使われず過去の攻撃で使われているため禁止</dd>
<dt>CONFIG_UIO=n<br />CONFIG_VFIO=n</dt><dd>ユーザースペースのアプリケーションからデバイスを操作する枠組みだが、ユーザースペースからデバイスを操作されると当然危険なので使わないなら禁止</dd>
<dt>CONFIG_VGA_SWITCHEROO=n<br />CONFIG_VGA_ARB=n</dt><dd>今はもう使われないハードウェア</dd>
<dt>CONFIG_SND_OSSEMUL=n</dt><dd>ALSAの前身であったOSSは今はもう使われない</dd>
<dt>CONFIG_HYPERVISOR_GUEST=n<br />
CONFIG_KVM_GUEST=n<br />
CONFIG_HYPERV=n<br />
CONFIG_VBOXGUEST=n<br />
CONFIG_PARAVIRT=n<br />
CONFIG_XEN=n<br />
CONFIG_VIRTIO=n<br />
CONFIG_VIRTIO_PCI_LIB=n<br />
CONFIG_VIRTIO_PCI_LIB_LEGACY=n<br />
CONFIG_VIRTIO_MENU=n<br />
CONFIG_VIRTIO_PCI=n</dt><dd>仮想マシンのゲストとして動作しないならこれらは不要</dd>
<dt>CONFIG_NUMA=n<br />
CONFIG_HOTPLUG_CPU=n<br />
CONFIG_HOTPLUG_PCI=n<br />
CONFIG_HOTPLUG_PCI_PCIE=n</dt><dd>NUMAやCPU・PCIホットプラグは普通の計算機には無い</dd>
<dt>CONFIG_NR_CPUS=個数</dt><dd>「nproc」コマンドの表示結果を書いておく。NR_CPUSの値によってRCUで用いられる処理が変わったりする</dd>
</dl>

# /dev/random から読み出せる乱数の増加
セキュリティ関連のアプリケーションでは `/dev/random` からの読み出しを行うが、乱数が足りないと読み出しでアプリケーションが停止する。これを防ぐには

```config:/etc/environment
SYSTEMD_RANDOM_SEED_CREDIT=true
```

と書いておくこと、ならびに、ハードウェア乱数生成器 (`/dev/hwrng` など) が付いている場合には [rngd](https://packages.debian.org/ja/stable/rng-tools5) が有効である。