Blog posts by @retrage

mirror of https://retrage.github.io/

UEFI向け9P File Systemを作ってクラウドからネットワークブートできるようにした

UEFI向け9P File Systemを実装した. これにより9Pサーバからネットワークブートができるようになった. さらにFUSEと組み合わせることで少ない労力で9Pサーバ経由で クラウドからネットワークブートができるようになった.

ソースコードと発表資料と発表の録画は以下で公開している.

ネットワークブート

通常OSを起動するときBIOSはローカルディスクにアクセスしてブートイメージをロードする. ネットワークブートではローカルディスクの代わりにネットワーク上のサーバからブートイメージをロードする. これを実現するためにBIOSはネットワークスタックを持っている. 既存のネットワークブートにはPXE BootとHTTP Bootがある.

PXEはPre-boot eXecution Environmentの略である. UEFI以前のLegacy BIOSから使われており,現在でも広く利用されている. PXEは標準化されており,プロプライエタリな実装以外にもiPXEというオープンソースな実装も存在する. PXEではTFTPというプロトコルでファイルの転送を行う. TFTPはPXE以外では一般的にあまり使われておらずPXE Bootのために特別にサーバを用意する必要がある.

HTTP Bootはその名の通りHTTPを使ってファイルの転送を行うネットワークブートである. このためApache HTTP Serverなどの一般的なHTTPサーバが利用できる. HTTP Bootは2015年にUEFI 2.5より標準化されており,DNSのサポートやTLSのサポートもあり, 比較的セキュアかつモダンなやり方でブートができるようになっている[0],

ここでUEFIのHTTPのインターフェースを示す,

f:id:retrage01:20200615183255p:plain
EFI HTTP Protocol

Configure()で設定を行い,Request()でリクエストを送信しResponse()でレスポンスを受け取る. これらを用いてHTTP Bootのbootloaderを要約して次のように書くことができる.

EFI_STATUS HttpBootLoader()
{
  // Send request
  Status = Http->Request (Http, TxToken);
  // Recieve response
  Status = Http->Response (Http, RxToken);
  // Start loaded image
  Status = gBS->StartImage (ImageHandle, NULL, NULL);
  return Status;
}

UEFIの拡張性

UEFIUnified Extensible Firmware Interfaceの略であり,Extensibleとあるように拡張性がある, UEFIはモジュール性のあるデザインとなっている.モジュールの一つの単位をProtocolという. UEFIはProtocolをロードする関数を中核となる機能に持っている. EFI_BOOT_SERVICES.InstallProtocolInterface()Interfaceに ロードされたProtocolを渡すことでUEFIが外部のProtocolを導入できる.

f:id:retrage01:20200615183301p:plain
EFI_BOOT_SERVICES.InstallProtocolInterface()

UEFIのProtocolの例としてSimple File System Protocolを紹介する. このProtocolはファイルシステム非依存のファイルへの操作のインターフェースを提供する. Simple File System ProtocolとFile Protocolの関数を示す.

f:id:retrage01:20200615183313p:plain
EFI Simple File System Protocol

Simple File System ProtocolのOpenVolume()によりボリュームを開き, そのルートディレクトリを表すFile ProtocolであるRootが得られる. File ProtocolにはOpen()Read()Write()などのファイルへの操作が ファイルシステム非依存な形式のインターフェースで用意されている.

しかし,このようなファイルシステム非依存なインターフェースを持っている一方でUEFIはデフォルトではFATしか対応していない. Simple File System Protocolによる非FATなファイルシステムを使った例としてここではUEFI Rootkitsを挙げる. Rootkitsはカーネルファームウェアをターゲットとしたマルウェアであり,UEFI RootkitsはUEFIをターゲットとしたものである. Rootkitsの役割はシステムに感染後,他のRootkitsや監視を行うエージェントなどをインストールすることである. こうしたUEFI Rootkitsの例としてHacking TeamのrkloaderとLoJaxを挙げる. Hacking TeamはイタリアにHQがあるセキュリティ企業であるが2015年に攻撃されて内部で使われていたソースコードが流出した. そうしたソースコードの中に含まれていたのはrkloader[2]である. 一方LoJax[3]は2018年に初めてin-the-wildで報告されたUEFI Rootkitsである. rkloaderとLoJaxの共通点としてNTFSUEFI Driverを持っている点が挙げられる. これによりターゲットのWindowsに対してKernel Rootkitsなどを埋め込むことができる. このNTFSにはNTFS-3GというNTFSオープンソースの実装をUEFIに移植しており, インターフェースとして先に挙げたSimple File System Protocolを持っている. ここでrkloaderでのエージェントの埋め込みがSimple File System Protocolによっていかに簡単に行われているかを示す.

まずOpen()によりファイルを開き,Write()により書き込みを行い,Close()で閉じる. インストールといっても非常に簡潔に記述されていることがわかる.

EFI_STATUS
EFIAPI
InstallAgent(
  IN EFI_FILE_HANDLE CurDir,
  IN CHAR16 *  FileNameUser
  )
{
  // Open FileNameScout as FileHandle
  Status = CurDir->Open (CurDir, &FileHandle, FileNameScout, EFI_FILE_MODE_READ|EFI_FILE_MODE_WRITE|EFI_FILE_MODE_CREATE, 0);
  // Write pSectiondata to FileHandle
  Status = FileHandle->Write(FileHandle,&VirtualSize,(UINT8*)(pSectiondata));
  // Close FileHandle
  Status = FileHandle->Close(FileHandle);
  return EFI_SUCCESS;
}

Plan 9 File Protocol

Simple File System Protocolの有効性を示せたところで最初の話題であるネットワークブートについて考えてみる. 既存のネットワークブートではネットワークを意識せざるを得ず柔軟性に欠ける,というのがここでの指摘である. そこでネットワーク透過なファイルシステムプロトコルがあればファイルシステムの持つ柔軟性を確保しながらネットワークブートができると考えられる.

そこで登場するのがPlan 9 File Protocol (9P)[8]である,

Plan 9 from Bell Labs (Plan 9)[7]はオリジナルのUnixのベル研の開発者らによって開発された Unix後継のOSである.Unix哲学の一つとして"Everything is a file."というのがあるが, Plan 9の特徴の一つとしてそれを押し進めた設計となっていることが挙げられる. Plan 9は商業的には失敗とされたものの,その成果として得られた規格や実装は多い. 9PもそうしたPlan 9の成果の一つである. Plan 9ではファイルがローカル/リモートのどちらでも同様に扱うことができるようにするために9Pを開発した. 以下に9Pでのファイルの読み込みまでの流れを示す.

f:id:retrage01:20200615183248p:plain
9P Flow

version, attachにより接続しルートディレクトリのファイルディスクリプタを得る. walkにより目的のファイルまでディレクトリを探索し,openによりそのファイルを開き, readによりファイルを読み込む.このように9Pではファイルへの操作がほぼ一対一対応したプロトコルとなっている.

9Pは明快かつシンプルなプロトコルであるためPlan 9と無関係なところでも多く利用されている. 例えばLinux kernelではv9fsという9Pのクライアントファイルシステムを持っている[4]. またVirtIOではvirtio-9pという9Pサーバが実装されており, これによりホストのファイルシステムをゲストに対して共有する際に用いられている[5]. また,最近Windowsにアップデートが入りWindow Subsystem for Linux 2 (WSL2)が正式に提供されるようになった[6]. これはWSL1と違い,VM上にゲストのLinuxを動かすものである. 通常VMのディスクイメージの実体はモノリシックなファイルであり, 内部のファイルへのアクセスをホストのファイルと同様に行うことは難しい. そこでWSL2ではホストからゲストへのアクセスに9Pを用いている. ホストのWindowsは9Pクライアントを持っており, ゲストのLinuxのファイルへアクセスする際には9Pクライアントを経由してアクセスする. ゲストのLinuxは9Pサーバを持っており,ホストからの要求を処理してファイルを共有する.

9pfsPkg

このように現在でも9Pは広く用いられている. そこで,9PのクライアントファイルシステムUEFI向けに実装したので紹介する.

9pfsPkgはSimple File System Protocolをインターフェースに持つ9PクライアントファイルシステムUEFI driverである. 先に示したように9Pではネットワーク透過にファイルシステムを扱うことができるため9pfsPkgでもその利点を活用できる, これによりUEFI Shellなどの既存のネットワークを意識しないツールでも 一切の変更をせずにそのままネットワーク経由のファイル操作をができる. また,PXE Bootにみられるような専用のサーバを用意する必要がないというのも利点として挙げられる.

9P Boot

9Pによるネットワークブート(9P Boot)についてみていく,以下に9P Bootでの構成を示す.

f:id:retrage01:20200615183240p:plain
9P Boot Overview

まずサーバ側で9Pサーバを適当なディレクトリ(e.g. /tmp/9)をexported directoryに指定して起動しておく. 次にクライアント側で9pfsPkgをロードする.これにより新しくボリュームが追加される. 作成されたボリュームへの操作は9pfsPkgが処理を行い, 9pfsPkgはUEFIのネットワークスタックを通じて9Pサーバと通信を行い,必要なファイル操作を行う.

これを実際に動かしてみると次のようになる.

最初のUEFI Shellが起動した段階ではローカルのファイルシステムFS0:のみが見える.

Mapping table
      FS0: Alias(s):HD0a65535a1:;BLK1:
          PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)
     BLK0: Alias(s):
          PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)
     BLK2: Alias(s):
          PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)

次にload 9pfs.efiによりUEFI driverをロードする.

FS0:\> load 9pfs.efi
Image 'FS0:\9pfs.efi' loaded at 7E2E7000 - Success

ロードされると新しくファイルシステムFS1:が追加される.

Mapping table
      FS0: Alias(s):HD0a65535a1:;BLK1:                                          
          PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)
      FS1: Alias(s):F1:
          PciRoot(0x0)/Pci(0x2,0x0)/MAC(525400123456,0x1)
     BLK0: Alias(s):
          PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)
     BLK2: Alias(s):
          PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)

ここで注目したいのがファイルシステムのdevice pathである. ローカルのFS0:ではPciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x0,0xFFFF,0x0)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)と なっているのに対し, リモートのFS1:ではPciRoot(0x0)/Pci(0x2)/MAC(525400123456,0x1)となっており, ファイルシステムでは通常ありえないdevice pathとなっている.

この状態でも動作し,fs1:で移動しgrubx64.efiによりGRUBを実行すると以下のように起動する.

FS0:\> fs1:
FS1:\> grubx64.efi
                             GNU GRUB  version 2.02

   Minimal BASH-like line editing is supported. For the first word, TAB
   lists possible command completions. Anywhere else TAB lists possible
   device or file completions.


grub>

このとき,UEFI ShellやGRUBからはローカルのファイルシステムを操作しているのと同じように扱われており, ネットワークブート固有の処理などはなされていないことを強調しておきたい.

Proxy Boot

9P Bootによりネットワーク透過なファイルシステムによるネットワークブートが可能になった. しかし,これはあくまで利用するプロトコルが変わっただけであり,9Pを利用することによる恩恵が感じられにくい. そこで9pfsPkgの応用としてサーバを踏み台としてその先にあるサーバのファイルからブートする方法(Proxy Boot)を提示する. ここではより複雑な方法で共有されているファイルを簡単に扱うことができることを示すためにクラウドストレージからのブートを行う. Proxy Bootの概要を以下に示す.

f:id:retrage01:20200615183325p:plain
Proxy Boot Overview

クラウドストレージとしてGoogle Cloud Storage (GCS)を用いる. あらかじめGCSのbucketを作成してブートイメージをアップロードしておく. サーバ側にはgcsfuse[9]というGCSのbucketをマウントできるファイルシステムを用いてマウントしておく. 9Pサーバのexported directoryにはgcsfuseのマウントポイント(e.g. /mnt/gcs)を指定しておく. あとは9P Bootと同様にクライアント側でボリュームをマウントするだけである. これにより,UEFIからはローカルのファイルと同じようにクラウドストレージにあるファイルを ネットワークを意識せずに操作することができる.

実際に動かしてみると次のようになる.

まず,GCSのbucketを作成しブートイメージをアップロードしておく. ここでは比較的規模が大きく実用的なものとしてBitVisorを起動することとする. loadvmm.efiがBitVisorローダであり,bitvisor.elfが本体である. これらはネットワークブートのための修正などは一切行われていないものであることを強調しておく.

f:id:retrage01:20200615183321p:plain
GCS Bucket

次にサーバ側でgcsfuseを/mnt/gcsをマウントポイントとしてマウントしておく.

$ sudo -E gcsfuse proxy-boot /mnt/gcs
 Using mount point: /mnt/gcs
Opening GCS connection...
Opening bucket...
Mounting file system...
File system has been successfully mounted.

先ほどと同じようにload 9pfs.efiによりUEFI driverをロードしてfs1:に移動, loadvmm.efiでBitVisorが起動する.

Shell> fs0:
FS0:\> load 9pfs.efi
FS0:\> map -u
FS0:\> fs1:
FS1:\> loadvmm.efi
Starting BitVisor...
Copyright (c) 2007, 2008 University of Tsukuba
All rights reserved.

再度強調するが,UEFI Shellもloadvmm.efiもあくまでローカルのファイルと全く同じように操作しており, 操作しているファイルがクラウドストレージにあるということは一切考慮していない.

まとめ

既存のネットワークブートはネットワークを意識せざるを得ず柔軟性に欠けるというのがここでの指摘であった. 9Pは現在でも広く使われており,UEFI向けの9Pクライアントファイルシステム(9pfsPkg)を実装することで ネットワーク透過なファイルシステムによるネットワークブート(9P Boot)が可能となった. これによりネットワークを意識しないでファイルを扱うことができるようになった. さらに9pfsPkgの応用としてサーバを踏み台としてクラウドストレージを間接的にマウントしてブート (Proxy Boot)できるようになった.これによりクラウドからのブートも非常に少ない労力で行えるようになった.

参考文献