気持ちとしてはWindows・Linuxにおけるセキュアブートと関連ツール(shim・MOK)、およびセキュアブートやTPMの効果について - turgenev’s blogの続きです。
概要
How to enable BitLocker when booting Windows 10 from a non-Microsoft boot manager? - Super UserこのQ&Aに非常に詳しく書いてある通り、LinuxなどとデュアルブートしているWindowsでBitlocker(Windows Homeの「デバイスの暗号化」含む)を普通に(セキュアブート・TPMを使って自動で復号されるかたちで)使おうとすると、以下のような問題があります。
- UEFIモードにおけるWindows側のブートマネージャー(bootmgfw.efi)には、Linuxの起動にも使えるようなデュアルブートの仕組みはない。
- たまにそれっぽいのを見かけるのは、多分Legacy BIOS時代
- 従ってWindowsとLinuxをデュアルブートするにはbootmgfw.efiを読み込む別のブートローダーを前段に入れる必要がある。
- これにより、Windowsがブートシーケンスの検証に用いるTPMのレジスタの1つであるPCR7の値が変化する。
- Windowsがこれを不正とみなし、Bitlockerの自動復号ができなくなる
- msinfo32.exeで「PCR7 バインドはサポートされていません」というような情報が表示される(※管理者権限必要)
- Bitlockerの機能制限版である「デバイスの暗号化」しかないHomeエディションについてはオプション自体が消滅する。
- Bitlockerが既に有効だった場合、回復キーの入力を求められる。
- Windows Proエディションの場合、gpeditかなんかでPCR7を使わずPCR0, 2, 4, 11を使うよう設定する(あるいは勝手にそうやってくれる?)ことでBitlockerがまともに使える場合もあるらしい。しかしHomeでは不可。詳細不明。
何せセキュリティ機能なので、前段に別のブートローダーが入っていると弾かれる、という部分はどうしようもなさそうです。
そこで考えられる回避手段が、Windowsを起動する際には、別のブートローダーから直接bootmgfw.efiを呼ぶのではなく、次回起動エントリにWindowsを選んで再起動してしまう、というものです。
systemd-bootやrEFIndを使う
実際、これは既に回避策としていくつかの既存のブートローダーに組み込まれています。
しかし一番ありそうなGRUBにはなぜかありません。あるのは、systemd-bootとrEFIndです。
まずsystemd-bootですが、「reboot-for-bitlocker」という名前そのまんまのオプションが存在します。
ただ、本当にこの名前まんまでしかなくて、つまり引数とかが指定できません。そうすると1台のPCにWindowsが2つあるとかそういう変な状況のときに困るはずで、実際reboot-for-bitlocker not working with bitlocker partition on 2nd drive · Issue #30470 · systemd/systemd · GitHubというようなissueが立っていました。従ってsystemd-bootは試しませんでした。
次にrEFIndですが、これとか公式ドキュメントのここで説明されているように、firmware_bootnumというのがあります。こちらは番号も指定できるので良さそうで、実際使ってみて問題なく動作しました。
以下のように書きます。
menuentry "Windows BootNext" {
icon /EFI/refind/icons/os_win8.png
firmware_bootnum 5
}
ただし若干不満な点として、再起動の動作がwarm rebootではなくcold rebootなので、一旦電源が切れるような動作になり、少し時間も掛かります。
UEFI Shellを使う
こちらが今回メインとして採用した方法です。
UEFI Shellというのは厳密な定義はよくわかっていませんがとにかくUEFIレベル(OS起動前)で動作するUEFI公式のシェルで、実装がこのへんにあるようです。UEFI BIOSやブートローダーに入っている場合もありそうです(rEFIndにも入っています)。
(非公式な)バイナリリリースはここにあります。配布されているバイナリがなぜ悪意のないものであると信じられるかの説明が長々と書いてあってそちらも読み物として面白いです。
起動するには、他のefiと同様、GRUBからchainloaderなどで呼び出せばOKです。基本的には対話的にコマンドを手打ちするためのものなのですが、efiが置いてあるのと同じファイルシステム(パーティション)にあるstartup.nshというスクリプトを5秒のタイムアウトののちに呼び出すようになっていて、これで自動化が可能です。
(バイナリ)パッチを当てる
UEFI Shellにもちょいと不満な点があります。まずさっき言った通りstartup.nshの呼び出し前に5秒のタイムアウトがあります。BootNextを書き換えて再起動しようにも5秒のロスが出てしまうのは不便です。
調べるとこことかにStartupDelayという変数を設定すればいけるという話が出てくるんですが、やってみてもダメでした。前述のソースコードを検索してもStartupDelayはヒットしないため、もしかしたらカスタムビルドなのかもしれません。
そこで、自分でソースコードを改変してタイムアウトを0にしてしまうことにします。
といっても、5を0にするだけのためにビルドし直すとして、じゃあ今度は10にしたくなったら(そんなことがあるのかわかりませんが)またビルドするのか?という気持ちになってきます。
そこで、適当な数値を入れてビルドしておいて後からバイナリを書き換えられるようにしてしまいます。
この5という数値の由来はedk2/ShellPkg/ShellPkg.decのPcdShellDefaultDelayのところで、UINT32と書かれています。そこで0xABBCCBBAというような特徴的な値にしておいて後からバイナリを検索して見つけられるようにします。バイト4つ分もあればランダムで出現するにしても(2^8)^4=2^32なので4GBに1回しか出現せず、しかも実際にはランダムではなく00やASCII文字(0x7F以下)の割合が結構多いので将来にわたってこれがバイナリの他の位置に出現する確率はかなり低いでしょう。
ところでタイムアウトの最大値がUINT16(0-65535)や何ならUINT8(0-255)だったとしても短くて困る人は現実にはいないと思います。UINT32にしてもらえたおかげで容易にこれができたのでありがたいことですが。
この部分の数値はビルドオプションで指定できるようになっており、自分でforkしてGithub Actionsをちょっと変えてリリースを作成しておきました(多分このバイナリも同様に信頼に値するはずです): Releases · ge9-2/UEFI-Shell-Custom-StartupDelay · GitHub。ビルドしてみると、最新版のx64バイナリでのBA CB BC AB(※リトルエンディアンなので逆順)の出現位置は0x7000バイト(29KB)付近でした。
さらに「startup.nsh」についてもバイナリをいじれば変えられます(もちろんビルド時に変えてもいいですが)。さすがに長さを変えたら壊れると思うのですが11文字あるので実用上は十分すぎるほどです。バイナリを探すと"startup.nsh"という文字列は(少なくとも現在最新版の25H2だと)edk2/ShellPkg/Application/Shell/Shell.cに由来するものとedk2/ShellPkg/Application/Shell/Shell.uniに由来するものの2つが含まれていますが後者はヘルプメッセージの一部であり実質的な機能があるのは前者だけです。バイト位置は0x8D000(576KB)付近でした。ワイド文字なのでバイナリを検索する際はs\0t\0a\0r\0t...のようにヌル文字を入れて検索しましょう。
これによってn秒のタイムアウトの後に好きな11文字の(シェルと同じパーティションにある)ファイルを起動してくれるUEFI Shellのバイナリが手に入ります。サイズは1Mちょいなので、大抵100MBくらいあるEFIパーティションなら10個くらいは平気で置けます。
スクリプトの中身
次にこのスクリプトでBootNextの書き換えと再起動をします。これはとても簡単で以下の2行を書くだけです。
setvar BootNext -bs -rt -nv =0500
reset -w
これで5番(efibootmgrとかでいう0005)のエントリが起動します。また-wを付けることでwarm rebootになります。
shim・GRUBでの設定
さらに、このUEFI Shellを呼び出すために前段のブートローダーを正しく設定しておく必要があります。
ここまでで準備したUEFI Shellにはセキュアブート用の署名は付いていないので、このバイナリのハッシュまたは(バイナリへと署名した上でそれに使った)鍵(または署名)をshimのMOKに入れる必要があります。基本的には、既存のものがなければopensslで鍵作成、sbsignで署名、mokutilでimport、という流れです。この部分に関しては多くの情報があるので他の記事を参照してください。
シェルを呼び出す部分に関しては特に難しい設定はなくて、以下のようにchainloadさせます。XXXX-YYYYはEFIパーティションのUUIDです(UEFI Shellとstartup.nshもここに置く場合)。
menuentry "Reboot to Windows" {
search.fs_uuid XXXX-YYYY root
chainloader /shellx64.efi
boot
}
これをいつも通り/boot/grubのcustom.cfgとか/etc/grub.dの40_customとかに書いておけばOKです。
起動時間について
warm rebootとはいえ起動を1からやり直しているのでそこそこ時間はかかります。手元のデスクトップPCで一回測ってみたらshimから直接Windowsを起動するのに比べて+14秒とかそんなもんでした。再起動が始まるまではほぼ一瞬(1秒くらい)です。
まあ状況としてはWindows側の落ち度でWindowsの起動時間が遅くなっているので自業自得であるという理解も可能です。迷惑な話ですが。
あとまあ冒頭の過去記事にも書いた通り、Bitlockerが対処できるリスクは①盗難②デュアルブートしている(かつBitlockerパーティションを利用しない)他OSからの攻撃、の2つなので、この2つを気にしなくていい場合は無理して使わなくてもいいと思います。当然暗号処理があるのでパフォーマンスの面でも一定のオーバーヘッドは不可避です。
余談: 本当に前からshimからの起動だとBitlockerは使えなかったのか…?
自分としてはshimからの起動だとBitlockerが使えないとは今まで思っていなくて、たまたまそうなっていたPC1台が気になってよく調査してみたら判明した感じでした。そもそもデスクトップ機とかでは最初からBitlocker切っていたというのと、たしか「たまにLinux側で何かアップデートするとBitlocker入力を求められるけど普段は大丈夫」ということがあった気がするのですが、それはPro機だったのかな…という感じです。が、前はもしかしたらそんなことなかったんじゃないの?という気もするんですよね。まあ何かわかったらまた書きます。
余談: compによる条件分岐
UEFI Shellではstartup.nshを起動していろいろやることができますが、それをGRUBなどから呼び出すにあたって、環境変数のようなものを普通の(OS上のような)形で設定することはできません。しかし、ファイルの読み書きは可能なので、ファイル経由で情報を伝達することはできます。例えば以下のようにcompコマンドを使うとUEFI Shellが置いてあるパーティションのルートディレクトリにあるファイルを比較して結果で条件分岐できます。
%homefilesystem%
comp test1.txt test2.txt
if %lasterror% == 0 then
echo equal
else
echo different
endif
UEFI Shellでは"FS1:"みたいなファイルシステム番号を打つことでそこをルートとして設定できる感じになっているのですが、homefilesystemという変数にシェルが置いてあるファイルシステムが入っているみたいなのでその変数をそのまま書いています。
このcomp以外にこうした条件分岐の方法があるかは不明です。ファイルの内容を変数に読み込む方法などは分かりませんでした。GRUB側でファイルに書き込む方法については省略します。