サーバーレス関数Azure Functionsの探索: Hyper-Vは最終防衛ライン

A pictorial representation of many storage containers stacked together

This post is also available in: English (英語)

概要

Unit 42のリサーチャーは、Azureのサーバーレスアーキテクチャを調査し、サーバーレス関数から基盤ホストにエスケープできることを確認しました。さらに私たちは、自分たちのホストの実体がHyper-V仮想マシンであり、このホスト上にはほかにも複数のサーバーレス関数がホストされていることを確認しました。

Azureのサーバーレス関数(通称Azure Functions)は、サーバーレスコンピュートサービスの1つです。このサービスを使うと、インフラのプロビジョニングや管理を行わずに、ユーザーがイベントトリガー型のコードを実行できるようになります。トリガーベースのサービスなので、Azure Functionsはさまざまなイベントに応答してコードを実行します。今回の調査では、このイベントはWebページのリクエストでした。

私たちは、各関数ごとにホストが新たなコンテナを生成することを確認しました。各コンテナは数分後には終了・削除され、従来の「サービスとしてのコンテナ(Containers as a service)」とサーバーレス関数とを差別化しています。

そのホストは私たちのAzureユーザーがアクセスできる関数のみをホストしていたため実害はありませんでした。Microsoftがホストへのアクセス防止に努力を惜しまなかったことは明らで、まだ明らかにされていない発見がほかにもある可能性はあります。この仮想マシンには、見えてはいけない重要情報が存在する可能性があり、やる気がじゅうぶんな攻撃者にならそうした情報が発見されるかもしれません。

Microsoftはよくコンテナをセキュリティの強化に利用しますが、セキュリティ境界としては扱わないのが通常です。コンテナは本質的に仮想マシンほど安全ではないためです。このケースはセキュリティの多層化を狙って実装したもので、結果的にそれが効果的だったことになります。

Prismaのサーバーレスソリューションは、ほとんどのクラウドプロバイダについて、関数の発見と脆弱性のスキャンを行えます。また組織のサーバーレス関数にさらなる監視機能を提供し、サーバーレス関数に既知の脆弱性が確認された場合はアラートを発報します。

関連するUnit 42のトピック Container Escape, Serverless, Containers, Cloud Security, Azure

目次

技術概要
サーバーレス関数とは
サーバーレス関数の内部構造
コンテナ
特権昇格
コンテナエスケープ: I Am Root
考えられる影響と結論
付録: Prismaのソリューション

技術的概要

サーバーレス関数とは

サーバーレス関数はサーバーレスコンピューティング(一般に「サーバーレス」と略される)の一形態です。サーバーレスコンピューティングでは、クラウドサービスプロバイダが顧客にすべてのコンピューティングリソースをオンデマンドで供給し、クラウドインフラを含むすべてのアーキテクチャを管理してくれます。

理想的なサーバーレスアプリケーションの例としてはチャットボットがあげられます。たとえばSlackではmarbotと呼ばれるサーバーレスアプリケーションを使ってSlack経由でDevOpsチームに通知を送っています。

サーバーレスというのは少々誤解を招きがちな名前で、サーバーレスコンピューティングといえどもコードの実行にはやはり物理サーバーが必要です。サーバーレスコンピューティングと従来型コンピューティングとの大きな違いは「コードそのものに直接関係していないものはサーバーレスが抽象化して取り去ってくれる」という点にあります。対象のコードを実行するオペレーティングシステムから、実際にコードを実行するマシンのハードウェアまで、これらはすべてサーバーレスコンピューティングにより抽象化されるのです。

サーバーレス関数の内部構造

読者の皆さんが最初に思い浮かべる疑問はおそらく「そもそもサーバーレスプラットフォームってどうやって調査するの?」ということではないでしょうか。Azure Functionsを使ったことがあるひとなら、このサーバーレス関数にはつつける場所がほとんどないことをご存じでしょう。コードをちょっとアップロードしたり、設定をちょっといじったりはできますが、やれることはそれくらいしかありません(図1)。

画像1はAzure Functionsのスクリーンショット。異なるランタイムごとのフォームのフィールドを表示したところこのフィールドには「Instance Details(インスタンスの詳細)」と「Operation system(オペレーティングシステム)」の2つのセクションがある
図1. Azure Functionsで利用可能なすべての異なるランタイム

私たちはHTTPリクエストをトリガーとするLinux上のPython関数から調査を始めることにしました(余談ですが一部のランタイムではWindowsも利用できます)。

次のステップはこの関数内部で動く対話型シェルを得ることです。自分たちが相手にしているものがどんなものかについて理解を深め、コードを実行しているマシンの情報を集めるたかったのです。使い勝手を考えてここではリバースシェルを採用することにしました。またデータ転送ツールにはnetcatではなくsocat(図2)を使うことにしました。socatのほうが、あとあと調査で必要になりそうなプロトコルを幅広くサポートしていたからです。

画像3はVisual Studio Codeのプログラムのスクリーンショット。リバースシェルを起動するコードがsocatのどこに格納されているかを示している
図2. Visual Studio Codeで私たちのプロジェクト内のsocatバイナリを表示したところ

私たちはシンプルにVisual Studio Codeのプロジェクトディレクトリにsocatのバイナリをドロップして先ほど作成したサーバーレス関数に丸ごとデプロイしました。実際にリバースシェルを起動するコードは非常にシンプルです。すべてのロジックはsocatアプリケーションにあるからです。

画像3はVisual Studio Codeのプログラムのスクリーンショット。リバースシェルを起動するコードがsocatのどこに格納されているかを示している
図3. リバースシェルリスナーに接続する私たちのシンプルな関数のコード

リバースシェルを実行してみると、そこはユーザーが名前をつけたappの下の関数ディレクトリ内でした。今いる場所がコンテナ内であることはすぐにわかりました。確認には

というコマンドを使用しました。SandboxHostで始まっているマシンのホスト名(図4)も、ここがコンテナ内であることをうかがわせるものでした。

画像4はサーバーレス関数内のシェルのスクリーンショット。何行ものコードが表示されている
図4. サーバーレス関数内のシェル

たどり着いた作業ディレクトリにはさほど興味を引くものは入っていませんでした。このディレクトリには、アップロードしたすべてのファイルに加えて、PythonがAzureと通信するために必要なライブラリが入ったlibフォルダが1つ追加されていました。

コンテナ

この調査は「Prismaのサーバーレス保護の改善」以外には、とくにこれといった目標なくスタートしたものでした。ところがアーキテクチャについての理解が深まってくると、このコンテナをエスケープしてウサギの穴がどこへ続くのか見てみたいという欲求がわいてきました。

コンテナ、コンテナのファイル、自分たちのユーザーパーミッションなどに親しんだのち、私たちは環境変数を確認してみることにしました。環境変数には興味深い情報が含まれていることが多いですし、今回もその通りでした。なかでもとりわけ興味深かったのは、環境変数がイメージ名を教えてくれたことです(図5)。

画像5は3行のコードのスクリーンショットで、イメージ名を表示する環境変数を示している
図5. 環境変数にイメージ名が表示されている

このイメージ名でGoogleを検索してみると、Microsoft公式のイメージ名を記載したカタログにたどり着き、さらにそこからMicrosoftの全イメージを提供している公式レポジトリにたどり着きました。検索したイメージもここに含まれています。

イメージを手元において調査できるならコトはずっと簡単に運びます。私たちはそのイメージをローカルマシンに取り込んで調査をはじめたいと考えていました。

最初のアプローチはそのイメージの「ソースコード」(Dockerを使用している場合はDockerfile)を得ることでした。ところが意外にもこれが見た目ほどたやすい仕事ではありません。ちょっと調べてみるとDockerイメージを作成したDockerfileをリバースエンジニアリングするWhalerという便利なユーティリティツールがあることがわかりました。図6はこのプロセスを示したものです。

画像6は3行のコードを示している。ここでは、MicrosoftのイメージをリバースエンジニアリングするためにWhalerというツールを使用しているWhalerは、あるDockerイメージを作成元となったDockerfileにリバースエンジニアリングする
図6. WhalerでMicrosoftのイメージをリバースエンジニアリングしたところ

Whalerをラップしているイメージがたくさんあったおかげで、簡単なワンライナーから使えるようになりました。この方法で、十分使えるレベルのDockerfile生成にこぎつけることができました(図7)。このDockerfileでいちばん興味深かったのは最後の行です。

画像7には複数行のコードが表示されている。これらのコードでWhalerがリバースエンジニアリングしてDockerfileを生成した
図7 イメージからDockerfileを「リバースエンジニアリング」したもの

meshフォルダには多少興味深いファイルがあるようです。とくにlaunch.shスクリプトはコンテナ内で最初に実行されるもののようです。この時私たちはイメージ内からmeshフォルダを丸ごとコピーしてさらに調査を進めていきました。

このフォルダにはさまざまなシナリオでほかのスクリプトを呼び出すスクリプトがいくつか含まれていました。このなかで興味深かったのはinitというバイナリで、このバイナリはすべてのAzureサーバーレスコンテナのバックグラウンドで実行されています。うまいぐあいにこのバイナリにはシンボルがついていたので、リバースエンジニアリングが楽でした。

関数と文字列のリストを調査してみるととくに興味深い関数 init_server_pkg_mount_BindMount (図8aおよび8b)が見つかりました。リバースエンジニアリングしてみると、この関数はコンテナ内のパスを同じくコンテナ内にある別のパスにバインドしていることがわかりました。この関数もユーザーに特権を要求してこないのですがなんとrootのコンテキストで実行されていました。あるファイルをべつのファイルにバインドしたければ、コンテナ内の正しいポート上で正しい引数をつけてHTTPクエリを実行するだけでよいのです。

画像8aは関数init_server_pkg_mount_BindMountのシグネチャを示す3行のコードのスクリーンショット。
図8a. init_server_pkg_mount_BindMountの関数シグネチャ
画像8bは、関数init_server_pkg_mount_BindMountがHTTPリクエストからsourcePathとtargetPathをパースする方法を示す複数のコード行を示したスクリーンショット。
図8b. init_server_pkg_mount_BindMount関数。HTTPリクエストからsourcePathtargetPathをパースする

この部分の調査をするなかで私たちはAzureのサーバーレスアーキテクチャのしくみの舞台裏に関する情報もたくさん見つけました。本稿では割愛しますが、この部分についてもまたべつの記事で掘り下げてみるのもよさそうです。

特権昇格

ここまでの流れを簡単におさらいすると、私たちはコンテナ内に非特権ユーザーとして存在しており、かつ、rootとして任意の1ファイルをべつのファイルにバインドすることができる、という状態です。この時点での私たちの目標はコンテナ内でrootに特権昇格することでした。そのための方法はいくつかあると思いますが、ここでは、/etc/shadowファイルを自分たちの作成したもの(図9)に置き換えてユーザーをrootに変更する、という方法をとりました。

画像9はOpenSSLを使用して/etc/shadowを生成する方法を示す数行のコードのスクリーンショット
図9 OpenSSLで/etc/shadowを生成

このために次の手順を実行しました(図10)。

  1. rootユーザーの既知のパスワードで/etc/shadowファイルを生成する
  2. Functionコンテナ内のローカルディレクトリに生成したファイルをアップロードする
  3. 実行中のinitサービスとBindMount関数を使い、http://localhost:6060に対するクエリを実行し、ローカルディレクトリにアップロードしたshadowファイルを実際の/etc/shadowファイルにバインドする
  4. su - rootコマンドでユーザーをrootに変更する
画像10は、ローカルのshadowファイルと実際の/etc/shadowファイルをHTTPクエリを実行してバインドし、特権昇格した方法を示した数行のコードのスクリーンショット
図10 特権昇格

コンテナエスケープ: I Am Root

次のステップは新たに獲得したrootアクセスを活用しコンテナからのエスケープを図ることでした。私たちはFelix Wilhelm氏が何年か前に見つけた古いエクスプロイトを使うことにしました。


図11 コンテナエスケープの概念実証(PoC)

このエクスプロイトを使用するのはそう簡単ではなく、いくつか動作条件を満たさねばなりません。

  • コンテナ内でrootとして動作していること
  • コンテナにSYS_ADMINケイパビリティがあること
  • 対象のコンテナがAppArmorプロファイルを持っていないかmountシステムコールを許可していること
  • cgroup v1の仮想ファイルシステムがコンテナ内で読み書き可能でマウントされていること

すでに1つ目の要件は達成済みです。意外にも、私たちのコンテナは残りの条件も満たしていました。なぜこれが意外なのかというと、デフォルトのDockerコンテナは制限されたケイパビリティセットで起動され、SYS_ADMINケイパビリティは無効のはずですし、通常のDockerはデフォルトでAppArmorポリシーを指定してコンテナを起動するので、たとえコンテナがSYS_ADMINケイパビリティつきで実行されてもmountシステムコールは使えないはずだからです。私たちは/proc/PID/statusの下にある自分のシェルステータスファイルに対してcapshコマンドを実行することで、必要なケイパビリティがぜんぶ揃っていることを確認しました(図12)。

画像12は特権ユーザーのケイパビリティ確認にsedスクリプトでcapshを実行したさいの数行のコードのスクリーンショット
図12. capshとsedスクリプトで特権ユーザーのケイパビリティを出力したところ

このエクスプロイトの詳細解説は本稿の範疇を超えるので割愛しますが、この技術への理解を深めたいかたは「Digging into cgroups Escape」を一読されることをお勧めします。要するにこのエクスプロイトは以下のような手順となります。

  1. 何かしらのcgroupコントローラへのアクセスを見つけるか作成する(このエクスプロイトは大半のケースでRDMA cgroupが使われています)
  2. そのコントローラ内で新たにcgroupを作成する
  3. 新たに作成したcgroupのnotify_on_release1に設定する
  4. release_agentをコンテナとホストの両方からアクセス可能なファイルに設定する
  5. そのcgroup内ですぐ終了するプロセスを開始し、その終了時にrelease_agentの実行をトリガーさせる

ここでは、ps auxを実行してその結果をコンテナのps auxと比較することで、ホストの実行コンテキストを得られたことを実証することにしました。本節冒頭のビデオで今回のエクスプロイトが実際どのように行われたかを最初から最後まで確認できます。ひとつ重要なのは、このコンテナをホストしているマシンはHyper-V仮想マシンであって物理サーバーではないということです。

考えられる影響と結論

今回私たちの仮想マシンがホストしていたリソースは、私たちの使っていたAzureユーザーからアクセスできるものばかりでした。その意味では、今回のエスケープはただちに実害をもたらすようなものではありませんでした。クラウドプロバイダの緩和策が意図した通りに機能した例といえます。この事例では、セキュリティ境界をコンテナ頼みにせず、多層の防御を講じていたMicrosoftの選択が賢明であったことがわかります。

ただしこの問題はたとえば仮想マシン自体に別の脆弱性が見つかった場合に大きな影響を与える可能性があります。またMicrosoftはそれが脆弱性と呼ばれないものであっても同様の問題をこれまで修正していることが知られています。

どうもこのあたりが頃合いのように思われましたので、ここで調査にひと区切りつけ、仮想マシンの徹底的な調査はよして、結果を投稿しておくことにしました。この仮想マシンにサーバーレス関数のユーザーや攻撃者が目にすべきでない重要情報や秘密が含まれている可能性はあります。Microsoftがこの問題に対応するまでの間、クラウドコミュニティのリサーチャーの皆さんがここからさらに調査を進めてくれることでしょう。

弊社の調査結果を受け、お客様の安全確保にむけてMicrosoftは以下の多層防御策を検討しました。

  1. バインドマウントAPIの検証を追加しオーバーマウントを防ぐ
  2. 可能な限りバイナリからシンボルを削除する

付録: Prismaのソリューション

Prismaのサーバーレスソリューションは、ほとんどのクラウドプロバイダについて、関数の発見と脆弱性のスキャンを行えます。これにより組織のサーバーレス関数にさらなる監視機能を実現し、サーバーレス関数に既知の脆弱性が確認された場合はアラートを発報します。

Prismaは、とくにAWSについて、Serverless Radarによる深い可視化、コンプライアンス遵守、過剰な特権をもつロールの検出、サーバーレスWAASを提供します。

さらにPrismaのServerless Defenderは、サーバーレス関数をランタイムでも保護し、自組織の関数が設計した通りに実行されるかどうかを監視します。関数ごとのポリシーでは以下をコントロールできます。

  • プロセスのアクティビティ: 起動されたサブプロセスをポリシーに照合して検証できます。
  • ネットワーク接続: インバウンド接続・アウトバウンド接続の検証を有効にし、明示的に許可されたドメインへのアウトバウンド接続を許可します。
  • ファイルシステムアクティビティ: ファイルシステムのどの部分に関数がアクセスできるかを制御します。

現時点では、Prisma CloudはAWS Lambda関数(Linux)とAzure Functions(Windowsのみ)をサポートしています。

AWS Lambdaでは以下のランタイムがサポートされています。

  • C# (.NET Core) 3.1
  • Java 8, 11
  • Node.js 12.x, 14.x
  • Python 3.6, 3.7, 3.8, 3.9
  • Ruby 2.7

Azure Functionsでは以下のランタイムがサポートされています(Windowsのみ)。

  • v3 - C# (.NET Core) 3.1, 5.0
  • v4 - C# (.NET Core) 4.8, 6.0
画像13はPrisma Cloudでサーバーレス関数の防御を有効にする方法を示した画面です。
図13. Prismaでサーバーレス関数の防御を有効化