気になったことのあれこれ。この広告は、90日以上更新していないブログに表示しています。当該脆弱性を悪用して細工したコンテナをユーザが実行した場合、ホスト上のruncバイナリが上書きされ、コンテナが起動しているホスト上でroot権限でコマンドが実行される恐れがある。目的はデフォルト若しくはハーデニングされた環境(制限された権限とシステムコール)でホスト環境を汚染することである。以下のアタックベクタが考えられる。悪意あるDockerイメージコンテナ内の悪性あるプロセス(Docker内でルートとして動作しているサービス)最終的にホスト上で、全ての権限がある状態でのコード実行が成功した(ルート権限)。これは以下のいずれかによって引き起こされる。execを実行する悪意あるDockerイメージを実行するDockerのデフォルト設定Dockerはサンドボックスとして知られている訳ではないが、デフォルトの設定ではホストのリソースはコンテナ内で動作しているプロセスのアクセスからは守られているとされている。Dockerコンテナのinitプロセスはルートとして動作し、いくつかのメカニズムから限られた最小権限しかない状態に遷移する。デフォルトではDockerコンテナは限られた権限しか保持しておらず、それ故コンテナ内のルートユーザは事実上の一般ユーザとなる。ホストへの影響を制限するために、コンテナプロセスに対して一部のシステムコールをブロックしたり環境変数のフィルターなどを行う。コンテナ化されたプロセスからホストのファイルシステムに対するアクセスを制限する。加えてホスト及びコンテナ間でのプロセスの参照を制限する。プロセスグループのリソースに対する制限や管理を行う。明示的に全てのメカニズムを無効にすることや一部の機能を指定して使用することは可能である。それらの機能を無効にすることで簡単にコンテナからエスケープすることは容易である。よって今回はデフォルトの設定で動作しているコンテナを見ていく。失敗したアプローチ当該脆弱性を見つけるまでに様々なアプローチを行なったが、それらのほとんどは制限に用いられているseccompのフィルターや制限された権限によって回避された。名前空間に新たに参加したプロセスがホストのリソースにアクセスできるのか、特に使用している名前空間に参加する前にそのプロセスにアクセスできる方法がないかを確認した。処理は以下のように進行する。net)に参加もしプロセスが見えた段階でptraceできれば、残りの名前空間への参加を回避しホストのファイルシステムなどにアクセスが可能となる。ptraceするために必要とされる権限を保持しないことは、コンテナのinitプロセスで行われるユーザ名前空間のunshareによってバイパスすることが可能(よって新たな名前空間で全ての権限を保持する)。runCでは必要な名前空間に参加した後、forkしていることがわかった、これはこのアタックベクタを回避するものである。加えてDockerのデフォルト設定では名前空間に関連するシステムコールは全て無効になっている。次に目を付けたのはprocfsである。これは特別で頻繁に名前空間の境界をまたぐ。興味深いのは以下の点である。充分に情報は取得できず既に悪性あるプロセスと同じPID名前空間にいる必要がある。プロセスが完全にコンテナに参加する前(名前空間に入った後だがルートディレクトリやカレントディレクトリを切り替える前)、これらのファイルはホストのファイルシステムを指している。これはアクセスの可能性があると考えたがrunCのプロセスではダンプが不可能だったため、これらのファイルは使用できなかった。それ自体では使用できなかったが、最終的なエクスプロイトでは使用する方法を発見した。いくつかのファイルディスクリプタは以前の名前空間(特にmount名前空間)の情報リークもしくは親プロセスと子プロセスのやりとりを妨害できる可能性を考えたが、ローカルソケットとの同期は終了しており(再利用は不可)興味深いものは特に発見できなかった。興味深いベクタである。runCが対象のバイナリを実行する前(当該プロセスは見えておりPID名前空間には属している)の段階では、全てのエントリはホストファイルシステムを参照していた(当該プロセスはホスト名前空間で生成されたため)が、SYS_ADMIN権限無しでは当該プロセス内からでさえも、それらリンクを参照することが不可能であることがわかった。/evil_binaryの記述があるテキストファイルに置き換える)。そのバイナリは/proc/self/exeを書き換え、それ故ホストのld.soをも書き換える。当該アプローチは前述のSYS_ADMIN権限が無く失敗に終わった。サイドノート2上記の実験をしている際にカーネル内でデッドロックが生じることを発見した。通常プロセスが/proc/self/map_files/any-existing-entryを実行しようとするとデッドロックが起こる。(他のプロセスから/proc/that-process-pid/mapsを開くとハングする。ロックが取得されているため)成功したアプローチ最終的に成功した試みは前述の/proc/self/map_filesのアイディアに類似している。コードインジェクトションが可能な間に/proc/self/exe(ホストのdocker-runcバイナリ)を実行する(libc.soなどの共有ライブラリを変更し、libc_start_mainやグローバルコンストラクタ内で用意したコードを実行する)。これは/proc/self/exe(ホストのdocker-runCバイナリ)を書き換えることを可能とし、次にホスト上でdocker-runcを実行する時に全ての権限を保持したルートアクセスが可能となる。汚染されたイメージの作成または動作中のコンテナへの汚染。execの一部)を/proc/self/exeのシンポリックリンクにする。docker-runcが使用する共有ライブラリをグローバルコンストラクタを持った共有ライブラリで置き換える。その関数は/proc/self/exe(docker-runc)を読み取りモードで開く(当該バイナリが実行中であるため書き込みモードでは開くことができない)。次にその関数は/proc/self/fd/3を用いて当該ディスクリプタに対応したファイルを書き込みモードで開き他のバイナリを実行する、これはdocker-runcがもう実行されていないため成功する。その実行したバイナリのコードはホスト上のdocker-runcを書き換え、書き換えたニセのdocker-runcを選択しグローバルコンストラクタから任意のコードを実行する。execを実行した時に以下の事象が起こる。/proc/self/exe(ホスト上ではdocker-runcを指している)にシンボリックリンクを貼ったエントリポイントのでるバイナリがコンテナ内で実行を始める(execveがダンプ可能フラグをセットするためプロセスのダンプが可能となる)。これはコンテナ内で動作している新たなdocker-runcの中でもう一度オリジナルのdocker-runcを実行させる(ホストのバイナリは使用していない)。ここではevilと呼ぶ)を実行する。evilは悪意あるdocker-runcで(追加されたグローバルコンストラクタを用いて)ホストファイルシステム上のdocker-runcを書き換える(書き込みモードでファイルディスクリプタの3番を開く)。コンテナの起動若しくはexecが完了すると、攻撃者が用意したニセのdocker-runcがホストのファイルシステム上で全ての権限を持ったルートとして実行される(このバイナリは権限を破棄し名前空間に入る必要があるため、初期の段階では全てのパーミッションを持っている)。上記の攻撃手法はruncの挙動のみを妨害する、よって当該攻撃手法はDocker若しくはcri-oを用いているかに関わらずkubernetesにおいても成功する(内部でruncを使用していると思われるため)。当該攻撃手法はAWSやGCPにも影響を与える。実行対象の実行形式と/proc/self/exeのinodeをfstatを用いて比較し同一のものであった場合にexit()し、そうでなければ実行対象のファイルディスクリプタを用いてexecveatを呼び出す。実行対象バイナリが/proc/self/exeのシンボリックであることは検出できるが、なぜexecveatなのか。execを実行し比較した際に他のプロセスが"/proc/self/exe."のシンボリックリンクとなっているバイナリをリプレイスすることによって起こるレースコンディションを回避するためである。攻撃者がシンボリックリンクを使用せず、ローダが/proc/self/exeを指している(例えば1行目に#!/proc/self/exeなどの記述があるテキストファイルである)場合にバイパスできてしまう。コンテナで実行するプロセスにライブラリを静的リンクさせる。コンテナ内で悪意ある共有ライブラリを通じて行うコード実行を回避する(静的リンクされたバイナリは共有ライブラリをロードしないため)案だが、共有ライブラリの置き換えはこのエクスプロイトにおいて決して必要ではない。/proc/self/exe(docker-runc)の再実行後、他のプロセスが/proc/<pid-of-docker-runc>/exeを開くことが可能である(execve()の際にダンプ可能フラグがセットされるため)。再度実行が完了しruncのプロセスが存在しているタイミングに合わせる必要があるためエクスプロイトの作成が困難になる。実際はタイミングの幅はかなり大きく先ほどのシナリオで100%成功するエクスプロイトを開発した。これは"汚染されたイメージの実行"というアタックベクタを排除するにとどまった。最終的に以下の修正が当該脆弱性を軽減させるために採用された。名前空間に入る前にそのfdからruncを再度実行するこの修正はもし攻撃者が/proc/self/exeで指されているバイナリを書き換えても、それはメモリ上にコピーされたものであるためにホスト上にはなんの影響も出ないことを保証する。軽減策パッチが適応されていないruncではいくつかの軽減策がある。DockerコンテナをSELinux(--selinux-enabled)を有効にした状態で使用する。これはコンテナ内のプロセスからホスト上のdocker-runcバイナリの書き換えを不可能にする。読み取り専用のファイルシステムを使用する(少なくともdocker-runcを保持しているものに対して)コンテナ内の低い権限を保持しているユーザを使用する、若しくはuid0がそのユーザマップされた新しい名前空間を使用する(それ故そのユーザはホスト上のdocker-runcに対して書き込み権限を保持していない)。当該脆弱性のPoCを検証する。脆弱性自体の概要は以下。»Profile低レイヤ好きのソフトウェアエンジニア。(1)はてなブログをはじめよう!k--onishiさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。