Ubuntu Weekly Topicsの2016年10月21日号 でも伝えているように、再起動をすることなくカーネルを更新できる「Canonical Livepatch Service」が発表されました。今回はこのサービスの使い方と仕組みについて紹介します。
「Canonical Livepatch Service」とは
「Canonical Livepatch Service」は、カーネルを再起動することなくセキュリティフィックスを適用するサービスです。サービスそのものの概要はTopicsを参照してください。仕組みとしてはKernel 4.0 で実装され、Ubuntu 16.04 LTSから有効化されたCONFIG_LIVEPATCH
を使っています。
カーネルのLivepatch機能とサービス側の都合により、このサービスを利用するにはいくつかの制約が存在します。
Intel/AMDの64ビットアーキテクチャ(amd64)のみ
Ubuntu 16.04 LTSの公式カーネル(Linux 4.4)のみ
Ubuntuカーネルでいうところのgenericフレーバーとlow latencyフレーバーのみ
Ubuntu 16.04 LTSから標準でインストールされるようになったsnapdの2.15以降が必要
Canonical Livepatch Serviceサイト(https://livepatch.canonical.com:443)へのアクセス
Ubuntu Single Sign Onアカウント(Launchpadなどのアカウント)
snapdが必要なのは、Livepatch Serviceのクライアントをsnapパッケージとして提供しているためです。このクライアントは、デーモンとして常駐し必要に応じてサーバーからパッチモジュールをダウンロードした上で、カーネルに取り込みます。
Livepatchクライアントは、Canoncialの商用サポートサービスであるUbuntu Advantage の機能であり、システム管理ツールでもあるLandscape の一部として提供されます。そのためクライアントのソースコードは公開されていません。しかしながら使用している仕組みそのものは、もともとのカーネルにも実装されているものですし、Livepatch Serviceから提供するカーネルモジュールのソースコード自体は公開されています。そのため、本サービスを使用しなくても個人のマシンで同等の機能を実現することは可能です[1] 。
[1] もちろんテストの量や安定性を考えると、サービスを利用したほうが安全です。またビジネスで使うマシンであればUbuntu Advantageを購入して少しでもCanonicalの懐を潤していただければ、Canonicalの開発リソースが増え、その結果としてコミュニティ側にも恩恵があるはずです。なにとぞよろしくお願いいたします。
またセキュリティパッチそのものは、Livepatch Serviceとは関係なく提供されます。従来通り「sudo apt update && sudo apt upgrade
」の組み合わせで最新のカーネルがインストールされますので、再起動してそのカーネルに切り替えてください。Livepatch Serviceはあくまで、再起動することなくセキュリティパッチを適用できるというサービスです[2] 。
[2] Livepatchを利用できないパッチも存在しえます。またカーネル以外のパッケージの更新によって再起動を要求する場合もあります。このためパッケージの更新時にまったく再起動が不要になるというわけではありません。
Livepatch Serviceを使ってみる
まずはLivepatch Serviceをそのまま使ってみましょう。3台までならUbuntu Advantageを購入しなくても利用できます。あらかじめUbuntu Livepatch Service のサイトから、利用トークンを入手しておいてください。
図1 Ubuntu userにチェックを入れて、Get your tokenをクリックする
図2 トークンが表示されるので控えておく
次にsnap
コマンドを使ってクライアントをインストールします。
$ snap find livepatch
Name Version Developer Notes Summary
canonical-livepatch 5 canonical - Canonical Livepatch Client
$ sudo snap install canonical-livepatch
(中略)
$ snap list
Name Version Rev Developer Notes
canonical-livepatch 5 15 canonical -
ubuntu-core 16.04.1 423 canonical -
snap
はUbuntu 16.04 LTSから標準でインストールされるようになった、新しいパッケージフォーマットであるsnap を管理するためのコマンドです。snapフォーマットは、必要なソフトウェアをすべてそのパッケージに固めて提供するタイプのフォーマットであり、強制アクセス制御とシステムから独立したファイルツリーによるセキュリティ機構や、最近では言語ごとに存在するパッケージングシステムを流用しやすいパッケージングツールなどが特徴です。同様のアプリケーション配布システムとしてはFlatpak やAppImage が存在するものの、どちらかというとCoreOS やRHELのAtomic に近い仕組みを採用しています。
さて、snapでは最小限のルートファイルシステムをubuntu-coreとしてインストールします。個々のsnapパッケージはこのubuntu-coreの上にかぶせる形でルートファイルシステムを構築し、個々のコマンドを実行することになります。snapパッケージはすべて/snap
ディレクトリ以下に保存されます。
$ ls /snap/
bin canonical-livepatch ubuntu-core
$ ls /snap/canonical-livepatch/current/
canonical-livepatch command-canonical-livepatchd.wrapper etc meta usr
canonical-livepatchd command-canonical-livepatch.wrapper keys sbin
$ ps -fe | grep livepatch
root 1545 1 0 20:05 ? 00:00 /snap/canonical-livepatch/15/canonical-livepatchd
shibata 1587 1217 0 20:05 pts/25 00:00 grep --color=auto livepatch
canonical-livepatchをインストールすると、canonical-livepatchd
が立ち上がります。またcanonical-livepatch
コマンドを使ってこのデーモンと通信することになります。ちなみに、どちらのコマンドもGo言語で作られているようです。
Livepatch Serviceの初期設定
Livepatch Serviceを使うためには、環境によってはあらかじめ初期設定を行う必要があります。たとえばプロキシ経由でアクセスする場合やセキュアブートを利用している場合がそれに該当します。
Livepatch Serviceでは「livepatch.canonical.com:443」へアクセスする必要があります。プロキシ経由で接続する必要がある場合は、環境にあわせて次のような/var/snap/canonical-livepatch/config
ファイルを作成してください。
http-proxy: "http://proxy.example.co.jp:8080"
https-proxy: "https://proxy.example.co.jp:8080"
no-proxy: "example.co.jp"
設定を行ったら、canonical-livepatchd
サービスを再起動します。
$ sudo systemctl restart snap.canonical-livepatch.canonical-livepatchd.service
Livepatch Serviceではカーネルモジュールをダウンロードしてカーネルに取り込みます。セキュアブート環境においてはロードするカーネルモジュールも検証の対象になります。つまりこのモジュールの署名検証用の証明書をあらかじめ取り込んでおく必要があります。セキュアブート環境はまず、以下のコマンドを実行した上で、再起動してください。
$ sudo mokutil --import /snap/canonical-livepatch/current/keys/livepatch-kmod.x509
Livepatch Serviceを有効化する
一通りの設定が終わったら、canonical-livepatch
コマンドで、Livepatch Serviceの機能を有効化しましょう。
$ sudo canonical-livepatch enable トークン
Successfully enabled device. Using machine token: マシントークン
$ canonical-livepatch status --verbose
client-version: "5"
machine-id: マシンID
machine-token: マシントークン
architecture: x86_64
cpu-model: Intel Xeon E3-12xx v2 (Ivy Bridge)
last-check: 2016-10-22T20:08:49.442817865+09:00
boot-time: 2016-10-22T19:43:15+09:00
uptime: 27m20s
status:
- kernel: 4.4.0-45.66-generic
running: true
livepatch:
state: nothing-to-apply
version: ""
fixes: ""
status
サブコマンドを実行すると、現在の状態が表示されます。この記事を執筆している時点での最新カーネルパッケージである「4.4.0-45.66」には、緊急度の高い修正パッチは提供されていないため、「 nothing-to-apply」となっています。そこで、一つ前のカーネルバージョンに変更して起動してみましょう。まず、古いカーネルパッケージをインストールした上で、再起動します。
$ sudo apt install linux-image-4.4.0-43-generic linux-image-extra-4.4.0-43-generic
再起動するときにGRUBの画面で「Advanced options for Ubuntu」を選択し、そこから「Ubuntu, with Linux 4.4.0-43-generic」を選択してください。これで「4.4.0-43.63」カーネルになるはずです。この状態で再びstatus
サブコマンドを実行します。
$ uname -a
Linux ubuntu-desktop 4.4.0-43-generic #63-Ubuntu SMP Wed Oct 12 13:48:03 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
$ canonical-livepatch status --verbose
client-version: "5"
machine-id: マシンID
machine-token: マシントークン
architecture: x86_64
cpu-model: Intel Xeon E3-12xx v2 (Ivy Bridge)
last-check: 2016-10-22T20:30:04.652+09:00
boot-time: 2016-10-22T20:29:30+09:00
uptime: 45s
status:
- kernel: 4.4.0-43.63-generic
running: true
livepatch:
state: applied
version: "13.3"
fixes: '* CVE-2016-5195 LP: #1633547'
どうやらCVE-2016-5195 対応のパッチが適用されているようです。実際にlsmod
を実行すると、Livepatchのモジュールをロードしていることがわかります。
$ lsmod | grep livepatch
kpatch_livepatch_Ubuntu_4_4_0_43_63_generic_13 16384 1
カーネルモジュールは/var/snap
以下にダウンロードされます。
$ sudo ls /var/snap/canonical-livepatch/common/payload/
changelog copyright cvelist.yaml livepatch_Ubuntu_4_4_0_63_generic_13.ko meta
Dirty COW対応を検証する
CVE-2016-5195はいわゆる「Dirty COW 」とも呼ばれる問題で、COW(copy-on-write)の操作時に競合状態が発生し、本来書き込めないはずの領域に書き込めてしまうという問題です。すでにこの脆弱性に対する実証コード はいくつも公開されており、これを使えばローカル環境でも問題を再現できます。ためしに書き込み可能フラグを落としているはずのファイルに一般ユーザーが書き込めてしまうdirtyc0w.c
を試してみましょう。
$ wget https://raw.githubusercontent.com/dirtycow/dirtycow.github.io/master/dirtyc0w.c
$ sudo -s
$ echo this is not a test | sudo tee foo
$ sudo chown root: foo
$ sudo chmod 0404 foo
$ ls -lah foo
-r-----r-- 1 root root 19 Oct 22 18:03 foo
$ cat foo
this is not a test
$ gcc -pthread dirtyc0w.c -o dirtyc0w
$ ./dirtyc0w foo m00000000000000000
mmap 7f3cf7423000
madvise 0
procselfmem 1800000000
$ cat foo
this is not a test
dirtyc0w
コマンドは、指定したファイルをmmap()
し2つのスレッドを立ち上げ、片方ではひたすら/proc/self/mem
経由でmmap()
したアドレスに書き込みを行い、もう片方はmadvice()
にmmap()
したアドレスとMADV_DONTNEED
を渡し続けるというコードです。いずれも1億回同じ処理を繰り返すことで、どこかのタイミングで競合状態を発生させ、書き込みを成功させようとします。1億回繰り返すので、環境によってコマンドの完了までに相当時間がかかります。
脆弱性の存在するはずの4.4.0-43カーネルであるにも関わらず、Livepatch Serviceのカーネルモジュールをロードしているおかげで、foo
ファイルの内容は書き換えられていません。これがLivepatch Service導入前であれば、本来変更できないはずのfoo
の内容がdirtyc0w
コマンドに渡したm00000000000000000
になってしまいます。
Livepatch Serviceを無効化する
現在のところ、一度ロードしたLivepatchモジュールをアンロードする方法はありません 。rmmod
コマンドもエラーとなります。
$ sudo rmmod kpatch_livepatch_Ubuntu_4_4_0_43_63_generic_13
rmmod: ERROR: Module kpatch_livepatch_Ubuntu_4_4_0_43_63_generic_13 is in use
現時点で無効化する唯一の方法は、canonical-livepatch
コマンドからCanonical Livepatchのサービスそのものを停止することです。
$ sudo canonical-livepatch disable
Successfully disabled device. Removed machine-token: マシントークン
この状態で再起動すると、次回からはLivepatchがロードされなくなります。クライアントのソースが公開されていないため確実なことはわかりませんが、これによりマシントークンが一個なくなり、再び3台のマシンでLivepatch Serviceを有効化できるものと思われます。
当然のことながら「sudo apt update && sudo apt upgrade
」でカーネルパッケージを更新した上で再起動すれば、不要となったLivepatchモジュールはロードされなくなります。
自分でLivepatchモジュールをビルドしてみる
Livepatch機能そのものはオリジナルのカーネルに存在する機能です。このため、セキュリティ対応やCanonicalのLivepatchサービスとは関係なく、Livepatch対応のモジュールを作ることが可能です。
特定の関数をフックするモジュール
Livepatch機能はカーネルの関数トレーシング機能(CONFIG_FTRACE
)を利用して実装しています。この機能を使うと、特定の関数の呼び出しをフックして別の関数を呼び出すことが可能です。カーネルのソースコードにはLivepatch機能のサンプルとして、fs/proc/cmdline.c
のcmdline_proc_show()
をフックするlivepatch-sample.c
が存在します。cmdline_proc_show()
をフックすれば、/proc/cmdline
の出力結果を変更できます。
そこでこのサンプルコードをビルドしてみましょう。まずカーネルモジュールのビルドのために、カーネルヘッダーパッケージとカーネルビルドに必要なパッケージをインストールします[3] 。
[3] 今回はapt
のbuild-dep
サブコマンドを使ってLinuxカーネルをビルドするためのパッケージ一式をインストールしていますが、モジュールをビルドするだけならgcc
やmake
パッケージだけをインストールすれば事足りるはずです。
$ sudo apt install linux-headers-`uname -r`
$ sudo apt build-dep linux
$ mkdir livepatch && cd $_
$ wget http://kernel.ubuntu.com/git/ubuntu/ubuntu-xenial.git/plain/samples/livepatch/livepatch-sample.c
モジュールビルド用にKbuildとMakefile を作りましょう。
(Kbuildの内容)
obj-m := livepatch-sample.o
(Makefileの内容)
KDIR ?= /lib/modules/`uname -r`/build
default:
$(MAKE) -C $(KDIR) M=$$PWD
あとはmake
するだけです。
$ make
$ ls
Kbuild Module.symvers livepatch-sample.c livepatch-sample.mod.c livepatch-sample.o
Makefile built-in.o livepatch-sample.ko livepatch-sample.mod.o modules.order
ビルドしたlivepatch-sample.ko
をロードして、その前後の/proc/cmdline
の結果を比較してみましょう。
$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.4.0-43-generic.efi.signed (略)
$ sudo insmod livepatch-sample.ko
$ cat /proc/cmdline
this has been live patched
$ echo 0 | sudo tee /sys/kernel/livepatch/livepatch_sample/enable
$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-4.4.0-43-generic.efi.signed (略)
/proc/cmdline
の出力結果がlivepatch-sample.c
で指定した「this has been live patched」に変わっていることがわかります。さらにsysfs経由で当該モジュールを無効化すれば、/proc/cmdline
の出力結果も元に戻っています[4] 。
[4] Canonical Livepatch Serviceでロードしたパッチモジュールに対してsysfsで無効化すると、パッチに関連する処理を行っている時にカーネルパニックするようです。具体的な原因は不明ですが、Livepatch Service由来のモジュールについてはサービスを停止する形で無効化するようにしましょう。
差分ファイルからパッチモジュールを作る
前項では特定の関数を完全に置き換えるパッチモジュールを作成しました。しかしながら脆弱性対応という観点から言えば、既存の処理を少しだけ修正した形でモジュールを置き換えるほうが一般的です。つまり修正用の差分ファイルからパッチモジュールを作ると便利です。Red Hatが開発しているkpatch システムは、カーネルに対するパッチファイルからパッチモジュールを作成する仕組みです。UbuntuではこのkpatchをUbuntuカーネルの流儀にあわせて変更を加えた上でパッケージングしています。
kpatchの一部であるkpatch-buildをインストールすれば、前述のDirty COW対応モジュールのようなパッチモジュールを簡単に作ることが可能です[5] 。Canonical Livepatch Serviceも、基本的にこの方法で作ったカーネルモジュールを配布しています[6] 。そこでLivepatch Serviceで使われているカーネルへのパッチのコードリポジトリ を利用して、Dirty COW対応のパッチモジュールを作ってみましょう。
$ sudo apt build-dep linux
$ sudo apt install kpatch-build
$ git clone https://git.launchpad.net/~ubuntu-livepatch/+git/xenial-livepatches
$ cd xeinali-livepatches/Ubuntu-4.4.0-43.63
$ cp Ubuntu-4.4.0-43.63.diff livepatch-Ubuntu-4.4.0-43.63-generic_13.diff
kpatch-build
コマンドは内部でカーネルをビルドするため、前項と異なり今回はカーネルビルドに必要なパッケージをすべてインストールしておく必要があります。また本記事執筆時点での最新コミットである「commit:fa0c02eb4
」では、上記の最後のコマンドのように差分ファイルの名前を変えないといけませんでした。
あとはmake
を実行すれば、カーネルのソースコードなどをダウンロードして、内部でkpatch-build
を実行してくれます。かなり時間がかかりますので、気長に待ちましょう。
$ make
dget -u https://launchpad.net/ubuntu/+archive/primary/+files/linux_4.4.0-43.63.dsc
(中略)
wget -q https://launchpad.net/ubuntu/+archive/primary/+files/linux-image-4.4.0-43-generic-dbgsym_4.4.0-43.63_amd64.ddeb
(中略)
+ rm -rf /tmp/tmp.NlCVnRvZeW/buildroot
+ unset KCFLAGS
echo "Build complete for 4.4.0-43.63 generic amd64 revision 13"
Build complete for 4.4.0-43.63 generic amd64 revision 13
$ ls
Makefile linux-image-4.4.0-43-generic-dbgsym_4.4.0-43.63_amd64.ddeb livepatch-Ubuntu-4.4.0-43.63-generic_13.diff
Ubuntu-4.4.0-43.63.diff linux_4.4.0-43.63.diff.gz livepatch_Ubuntu_4_4_0_43_63_generic_13.ko
linux-4.4.0 linux_4.4.0-43.63.dsc vmlinux
linux-4.4.0.config linux_4.4.0.orig.tar.gz
上記のうちlivepatch_Ubuntu_4_4_0_43_63_generic_13.ko
が、パッチモジュールです。あとはこれをロードすれば、Livepatch Serviceの時と同様にDirty COWの問題を防ぐことができます。