NixOSと再現可能ビルドで検出された xz バックドア
(luj.fr)- 2024年3月に発見された
xzバックドアは、影響を受けるsshd環境では リモートコード実行 につながる可能性があり、NixOS の事例は再現可能ビルドがサプライチェーン改ざんを機械的に露見させうることを示した - 悪性要素は
xzの Git リポジトリ全体ではなく、5.6.0・5.6.1の メンテナー提供のリリース tarball に潜んでおり、ビルド中に偽の.xzテストファイルからシェルスクリプトとオブジェクトファイルを生成した - 攻撃は
glibcの ifunc を利用してliblzma読み込み時にコードを実行し、sshdがlibsystemd経由でliblzmaに推移的依存を持つ Debian・Fedora 系の環境を狙っていた - NixOS は通常 GitHub 自動生成のソースアーカイブを優先するが、
xzがnixpkgsの bootstrap 段階にあるため、autoconfの依存循環を避けるにはメンテナー tarball を信頼する必要があった - 提案された防御策は、bootstrap 後に GitHub ソースで
xzを再ビルドし、既存の tarball 由来の成果物と比較する方法であり、条件を有効化するとliblzma.soのサイズ差と_get_cpuidシンボルの追加が明らかになった
xz バックドアが潜り込んだ仕組み
xzは Linux ディストリビューションの中核的な経路で、ソース tarball を展開する際によく使われる圧縮・展開ソフトウェアである- 2024年3月に
xzでバックドアが発見され、悪意あるメンテナー Jia Tan は約3年かけて信頼を得てリポジトリの push 権限を確保し、正当な貢献の合間に難読化されたコードを少しずつ紛れ込ませていた - Andres Freund は複数の Debian unstable マシンで発生していた
sshの 500ms の性能低下を調査するなかでliblzmaまで追跡し、バックドアを特定して文書化した - バックドアの目的は
sshをハイジャックし、特定の RSA 鍵でログインした際に攻撃者が被害マシン上で任意のコマンドを実行できるようにすることだった
悪性 tarball とビルド中に生成されるオブジェクト
- 悪性コンポーネントは
xzGit リポジトリのコード全体に直接含まれていたのではなく、Jia Tan がビルド・署名して配布したxz5.6.0 および 5.6.1 のリリース tarball に含まれていた - Git リポジトリ内の一部のテスト用
.xzファイルにはマシンコードが隠されており、リリース tarball にはそれを取り出すよう偽装した変更が追加されていた - 特に
m4/build-to-host.m4の変更は、見た目には無害でコメントも付いていたが、実際には複数の偽.xzテストファイルを復号・逆難読化するコマンドチェーンを隠していた - この過程の生成物は2つある
xzのビルド中に実行される シェルスクリプト- 悪性の バイナリオブジェクトファイル
- ビルド中に実行されるシェルスクリプトは2つの役割を担っていた
- 特定の Linux ディストリビューション、
glibcの機能、sshのインストール有無など、バックドア実行条件を確認する - 正常なオブジェクト
liblzma_la-crc64_fast.oがバックドアのオブジェクトファイルの_get_cpuidシンボルを使うように改変する
- 特定の Linux ディストリビューション、
- Russ Cox の
xzスクリプト分析 は、ビルド中に悪性リソースが作られる過程を詳しく扱っている
sshd へつながる ifunc ハイジャック
- 動的リンクされたプログラムでは、実行時にダイナミックローダーが共有ライブラリをメモリに読み込み、シンボルのアドレスを Global Offset Table(GOT) に埋める
glibcの ifunc は、動的ロード時に同じ関数の複数実装から1つを選ぶための機能であり、xzバックドアはこれを悪性コード実行経路として利用した- Debian・Fedora など一部のディストリビューションの
sshは、systemd通知サポートのためlibsystemdとリンクされており、libsystemdはさらにliblzmaとリンクしている- この環境では
sshdはliblzmaに推移的依存を持つ
- この環境では
sshd実行時にはダイナミックローダーがlibsystemdとliblzmaを読み込み、バックドアが仕込まれている場合はliblzma読み込み中に悪性コードが実行される- バックドアは ifunc リゾルバが悪性の
_get_cpuidシンボルを呼ぶようにし、まだ読み取り専用になっていない GOT を改変してRSA_public_decryptのアドレスを悪性関数に差し替える - Securelist の詳細分析と、攻撃ベクトルおよび緩和策をまとめた研究論文を参考資料として参照できる
信頼できるソースと NixOS の bootstrap 制約
- 多くのディストリビューションが Git forge の原本ソースではなくメンテナー提供 tarball から
xzをビルドしていたため、この攻撃は効果を持ちえた - tarball ワークフローには歴史的・実務的な理由がある
gitより古い初期 Linux ディストリビューションの方式である- リリース時点のソース状態を自己完結型アーカイブとして保存できる
- manpage や configure スクリプトのような中間生成物を含めてビルド負荷を減らせる
- 圧縮によって保存領域の効率を得られる
- セキュリティの観点では、技術的に可能な場合は最も信頼できる主体が認証したソースからビルドすべきである
- GitHub で開発されるプロジェクトなら、GitHub が各リリースごとに自動生成するアーカイブを使える
- Codeberg、SourceHut、GitLab のような信頼できる第三者プラットフォームにも同じ論理を適用できる
- NixOS は関数型パッケージ管理モデルを採用しており、パッケージは Nix 式でビルドレシピを定義する
- NixOS のメンテナーには、可能な場合
fetchFromGitHubで GitHub 自動生成ソースアーカイブを使う文化があるが、xzパッケージはfetchurlで手動アップロードされたメンテナー tarball を取得していた - その理由は、
xzがnixpkgsの bootstrap 段階に含まれているためである- bootstrap は、小さな seed binary の集合から
nixpkgsの全パッケージを再ビルドできるようにする過程である stdenvは他のパッケージが使う基本コンパイル環境であるstdenvは実行時にxzに依存し、coreutilsのようなパッケージは.tar.xzソースアーカイブを展開するためにxzを必要とする
- bootstrap は、小さな seed binary の集合から
- GitHub ソースから
xzをビルドするには configure スクリプトを作るためのautoconfが必要だが、autoconfもxzに依存する- メンテナー提供 tarball には configure スクリプトがすでに生成済みで含まれているため、この依存循環を断ち切れる
- そのため
nixpkgsグラフの中でxzがビルドされる地点では GitHub ソースアーカイブを使いにくく、メンテナー tarball を信頼する必要があった
ソース比較ではなく成果物の収束比較
- メンテナー tarball と GitHub ソース tarball が同一かを比較する方法は自然に見えるが、実務ではあまりうまくはまらない
- Daniel Stenberg は、リリース tarball がソースと異なること自体が機能だと説明している
- manpage や configure スクリプトのような中間生成物を tarball に含められる
- ディストリビューションが
autoconf依存を避けたい場合には特に有用である
- サプライチェーンセキュリティの観点では、この柔軟性はメンテナーの誠実さを信頼しなければならない負担へと変わる
- 再現可能ビルド とは、同じ条件で2回ビルドしたときにビット単位で同一の成果物が得られる性質である
- reproducible-builds グループは可能な限り多くのパッケージを再現可能にすることを目標としており、Reproducible Builds: Increasing the Integrity of Software Supply Chains では、バイナリ成果物配布への信頼を高める性質としてこれを扱っている
- tarball 生成自体を再現可能にするアプローチは、PostgreSQL プロジェクトが採用している
- ユーザーはメンテナー tarball が原本ソースから誠実に生成されたかを独立に検証できる
- ただし upstream プロジェクトのメンテナー側で実装する必要があるため、FOSS コミュニティでは導入が遅くなる可能性がある
NixOS で提案された検出手順
xzがビット単位で再現可能で、かつメンテナー tarball がビルド過程に影響する変更を含んでいなければ、GitHub tarball ビルドとメンテナー tarball ビルドは同じ成果物を出すはずである- 提案された方法は、bootstrap 後に
xzをもう一度ビルドし、このときfetchFromGitHubで GitHub ソースアーカイブを使って、既存の bootstrap 段階のxz成果物と比較するというものだ - 実装例では
pkgs/tools/compression/xz配下にafter-bootstrap.nixを置き、xz5.6.1 を GitHub ソースからビルドする- メンテナー tarball には configure スクリプトがすでに入っているが、GitHub ソースのビルドでは
autoconf、libtool、automakeなどを使って./autogen.shを実行する必要がある
- メンテナー tarball には configure スクリプトがすでに入っているが、GitHub ソースのビルドでは
- 比較段階として
compareArtifactsを追加し、ビルドの最後にliblzma.soを比較する- 例ではバックドアが改変したファイルである
liblzma.soのみを比較したが、すべてのバイナリおよびライブラリ成果物へ一般化できる
- 例ではバックドアが改変したファイルである
- 正常な master リビジョンでは
xz-after-bootstrapビルドは成功し、2つの成果物は同一だった - バックドアが含まれるリビジョン
c53bbe3では、当初は同一に見えていた- NixOS ではバックドアが有効化されておらず、その理由の1つはインストールスクリプトが
RPM_ARCH変数の設定を確認していたためである env.RPM_ARCH = true;を設定して条件を有効化すると、liblzma.soバイナリの差異が明らかになった
- NixOS ではバックドアが有効化されておらず、その理由の1つはインストールスクリプトが
- 2つの
liblzma.soを保存して比較すると、バックドア入りの成果物は 258KB、bootstrap 後に GitHub ソースからビルドした成果物は 210KB で、約 48KB の差があった nmでシンボルを比較すると_cpuid、_get_cpuid、__tls_get_addr@GLIBC_2.3が追加されており、_get_cpuidは複数のxzバックドア技術報告で文書化されているシンボルである
適用方法と限界
- 提案された保護策は、bootstrap 段階でビルドされ、信頼できるソースアーカイブを使っていないすべてのパッケージに適用するというものだ
*-after-bootstrapパッケージを channel blocker に追加すれば、ビルド失敗時にメンテナーの介入が必要な大きなアラートにできるxz事例の概念実証として、nixpkgsリポジトリに pull request が提出された- この方法には、対象パッケージが ビット単位で再現可能 であるという前提が必要である
- 2017年から2023年までの
nixpkgs-unstableリビジョン17件をサンプルとし、stdenvの non-fixed-output derivation をnix-build --checkで再ビルドした - すべてのリビジョンで
xzはビット単位で再現可能だった - 17件中12件のリビジョンでは、ビルドは可能だが再現不可能なパッケージが1〜2個あり、たとえば
gccは 2017〜2021年、bashは 2019年まで一貫して再現不可能だった
- 2017年から2023年までの
- すべての
stdenvパッケージにこの方法を適用することはできないが、長期的には再現性の高いパッケージに選択的に有効化できる - Ken Thompson の trusting trust 問題は、この方法の理論的限界として残る
- bootstrap 段階の信頼できない
xzがxz-after-bootstrapビルドを間接的に汚染し、成果物が同一に見えるようにできないという前提が必要である - そのような攻撃は非常に複雑になりうるが、この方法自体はその前提に立っている
- bootstrap 段階の信頼できない
- この検出法は、tarball の変更が最終成果物に影響を与える場合にのみ検出できる
- NixOS の実行ファイルにはバックドアが含まれていなかったため、条件を別途有効化していなければ、この事例のバックドアは発見できなかった
1件のコメント
Hacker Newsのコメント
NixOSと再現可能なビルドがxzバックドアを検出できなかった点は指摘しておくべきです。実際にはNixOSも悪意あるxzビルドをユーザーに配布していましたが、マルウェアがNixOSを標的にしていなかったため動作しなかっただけです
NixOSの開発者も「バックドアが公開されたとき、悪意あるバージョンのxzが私たちのユーザーに配布されていた事実に驚いた」と述べていました。いつものことですが、理論と現実は違いますし、xzを可能にしたのは技術的な脆弱性というより、現実世界の人間の脆弱性でした。コミュニティは、こうした問題をより良いソフトウェアだけで常にパッチできるわけではないという事実を、なかなか認められません
すべてのプロセスを制限された権限で実行し、作業に必要なディレクトリを除けば
~/にもアクセスできないようにできれば、たとえば悪意あるpipパッケージがSSHキーを盗むことを防げます。それでもxzバックドアがNixOSで動作しなかった理由は、NixOS特有の非FHSファイルシステム構造にあると見ています最近は、現実が客観的現実に対する個人の感情や価値観と合わないと、しばしば軽視されます。個人的な価値観はありますが、現実がそれに対する自分の感情より重要でないとは思いません。よく言っていたように、理論と現実の違いは、理論上は両者が同じでも、現実ではそうではないという点にあります。NixOSの再現可能なビルドがxz攻撃を検出できた可能性はあったが実際にはできなかった、という気づきが、今後そうしたビルドを分析して別の攻撃をより早く見つける発展につながることを願っています
著者は、今回たまたま起きたやり方に視野が狭まりすぎている感じがします。Jia Tan事件はサンプルが1つしかないので、その方法だけが可能だと見るのは近視眼的です
ここで提案されている防御が通用しないシナリオはいくつも十分に想像できます。Nixユーザーとしても、NixOSがこれを検出できた可能性は低いと思います。実際に検出できなかったわけですから。次回は違う形で起きるかもしれないと今述べたばかりですが、証拠なしにNixを信じるのも愚かです
ここでNixOSはあまり関係ありません。xzバックドアは具体的にRed HatとDebianを標的にしていたからです
同じ論理なら、xzバックドアはWindowsには影響しなかったと言うのも同じくらい関係があります。皮肉なことに、このバックドアは最終的にMicrosoft社員が発見しましたが、この点はしばしば見落とされます
記事では、ディストリビューションは従来のインストール用tarballの代わりに、バージョン管理システム、たとえばGithubから直接ソースコードを取得すべきだとしています
ただ、これで何が解決するのかよく分かりません。悪意あるメンテナーがバイナリの塊をソースコードリポジトリに直接追加すればよいのでは?著者はGithubがコードを検証しているかのように信頼できると見ていますが、当然ながらGithubはそのような検証をしていません
検証済みの再現可能なビルドは、xz utilsの侵害やSolarWinds Orionの改ざんのような事例を防ぐ助けになり得ましたし、十分に取り組む価値があります
Githubのリリースからバイナリをダウンロードして実行するなら、メンテナーを完全に信頼するしかありません。Nixがそうするのはクローズドソースのパッケージだけです
論旨は、XZのメンテナーがGitにチェックインされていないtarballの中に悪意あるコードを隠した、という事実に依拠しています
著者は、NixがGitからtarballを生成してバイナリビルドに入れるよう設定できることを示しています。しかし、これがなぜNixやNixOSに必要な機能なのかは分かりません。RPMやDebに入るものを含め、どんなビルドシステムでも中間段階としてtarballを生成するよう設定できます。実際、Debianはかなり前から再現可能なビルドを重要な目標として取り組んできました。https://wiki.debian.org/ReproducibleBuilds
第一に、NixOSのビルド過程ではxzがブートストラップの初期にあまりにも早く必要になるため、xzの完全なソースビルドを実行できませんでした。第二に、ブートストラップ初期に必要なnixpkgs依存関係が侵害されたかどうかを自動検知するためのnixpkgsの調整を提案しています。他のエコシステムももちろん完全なソースビルドを試して不一致を発見できます。記事の核心は、現在のnixpkgsではそれができないという点にあります
NixOSが防げた事例に注目するなら、CrowdStrike事故を見るべきです。今日の設定が動作しなければ昨日の設定で起動できるという点だけでも、被害の大半を軽減できたはずです
信頼しているフレームワークを使えば、そのフレームワークが攻撃されるまでは安全です。xz のバックドアは発見されたかもしれませんが、Nix エコシステムで動作させる目的で設計されたものではありませんでした。
いつか Nix のコア開発者がスパイだったり、似たような状況になったりすれば、Nix エコシステムを狙った攻撃も出てくるでしょう。Nix が本質的に安全だ、というような答え方はしてほしくありません。1〜2年以内に Nix がうまく攻撃されたら、訪ねていって間違っていたと認めさせたくなるほどです。
より強い主張は「Nix は従来のビルドシステムより攻撃が難しく、コストも高い」というものです。ですから、Nix を安価に攻撃する方法を見つけたら、そのときに来てください。それまでは、技術的なレベルで Nix は代替システムより攻撃しにくいという話は、少なくとももっともらしく、実際にもかなり可能性が高いです。
NixOS は少し先を行っていますが、ほとんどの他のディストリビューションもすべてをソースからコンパイルし、使用するソースが改ざんされていないかを暗号学的に検証し、パッケージ間に バージョン指定付き依存関係 を持っています。Debian にも再現可能ビルドがあります。
問題は、ビルドシステムがソースからビルドする前に、事前コンパイル済みのオブジェクトファイルを削除していなかったことにあります。それを直しても、誰もソースコードを検査しなければ、望むだけバックドアを追加できますし、NixOS であれ他のディストリビューションであれ、それを防ぐことはできません。
優れた技術分析ですが、タイトルは間違っており誤解を招きます。「技術的には正しい」と言えるかもしれませんが、好意的に見ても バックドアが仕込まれていたという意味 に近いものです。
ユニオンファイルシステムのレイヤーを超えるビルド管理ツールが必要であることをよく示しています。たとえば、テストがビルド成果物を汚染できないように追跡し、強制する必要があります。ビルド過程でファイルがファイルに影響を与える 因果追跡グラフ を作成し、そのグラフを明示的に作ったうえで、それを強制するか、以前の追跡グラフとの差分を報告するような仕組みが必要です。
そのとおりです。バックドアを隠すのは確実により難しくなったでしょう。しかし、不可能には程遠いです。
望むなら、いつでもソースコード内にバックドアを隠せます。ただ、もっともらしいバグに見えるようにするにはより多くの労力がかかり、検出される可能性も高くなるだけです。