1 ポイント 投稿者 GN⁺ 2025-03-24 | 1件のコメント | WhatsAppで共有
  • 2024年3月に発見された xz バックドアは、影響を受ける sshd 環境では リモートコード実行 につながる可能性があり、NixOS の事例は再現可能ビルドがサプライチェーン改ざんを機械的に露見させうることを示した
  • 悪性要素は xz の Git リポジトリ全体ではなく、5.6.05.6.1メンテナー提供のリリース tarball に潜んでおり、ビルド中に偽の .xz テストファイルからシェルスクリプトとオブジェクトファイルを生成した
  • 攻撃は glibcifunc を利用して liblzma 読み込み時にコードを実行し、sshdlibsystemd 経由で liblzma に推移的依存を持つ Debian・Fedora 系の環境を狙っていた
  • NixOS は通常 GitHub 自動生成のソースアーカイブを優先するが、xznixpkgsbootstrap 段階にあるため、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 とビルド中に生成されるオブジェクト

  • 悪性コンポーネントは xz Git リポジトリのコード全体に直接含まれていたのではなく、Jia Tan がビルド・署名して配布した xz 5.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 シンボルを使うように改変する
  • Russ Cox の xz スクリプト分析 は、ビルド中に悪性リソースが作られる過程を詳しく扱っている

sshd へつながる ifunc ハイジャック

  • 動的リンクされたプログラムでは、実行時にダイナミックローダーが共有ライブラリをメモリに読み込み、シンボルのアドレスを Global Offset Table(GOT) に埋める
  • glibcifunc は、動的ロード時に同じ関数の複数実装から1つを選ぶための機能であり、xz バックドアはこれを悪性コード実行経路として利用した
  • Debian・Fedora など一部のディストリビューションの ssh は、systemd 通知サポートのため libsystemd とリンクされており、libsystemd はさらに liblzma とリンクしている
    • この環境では sshdliblzma に推移的依存を持つ
  • sshd 実行時にはダイナミックローダーが libsystemdliblzma を読み込み、バックドアが仕込まれている場合は 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 を取得していた
  • その理由は、xznixpkgsbootstrap 段階に含まれているためである
    • bootstrap は、小さな seed binary の集合から nixpkgs の全パッケージを再ビルドできるようにする過程である
    • stdenv は他のパッケージが使う基本コンパイル環境である
    • stdenv は実行時に xz に依存し、coreutils のようなパッケージは .tar.xz ソースアーカイブを展開するために xz を必要とする
  • GitHub ソースから xz をビルドするには configure スクリプトを作るための autoconf が必要だが、autoconfxz に依存する
    • メンテナー提供 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 を置き、xz 5.6.1 を GitHub ソースからビルドする
    • メンテナー tarball には configure スクリプトがすでに入っているが、GitHub ソースのビルドでは autoconflibtoolautomake などを使って ./autogen.sh を実行する必要がある
  • 比較段階として compareArtifacts を追加し、ビルドの最後に liblzma.so を比較する
    • 例ではバックドアが改変したファイルである liblzma.so のみを比較したが、すべてのバイナリおよびライブラリ成果物へ一般化できる
  • 正常な master リビジョンでは xz-after-bootstrap ビルドは成功し、2つの成果物は同一だった
  • バックドアが含まれるリビジョン c53bbe3 では、当初は同一に見えていた
    • NixOS ではバックドアが有効化されておらず、その理由の1つはインストールスクリプトが RPM_ARCH 変数の設定を確認していたためである
    • env.RPM_ARCH = true; を設定して条件を有効化すると、liblzma.so バイナリの差異が明らかになった
  • 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年まで一貫して再現不可能だった
  • すべての stdenv パッケージにこの方法を適用することはできないが、長期的には再現性の高いパッケージに選択的に有効化できる
  • Ken Thompson の trusting trust 問題は、この方法の理論的限界として残る
    • bootstrap 段階の信頼できない xzxz-after-bootstrap ビルドを間接的に汚染し、成果物が同一に見えるようにできないという前提が必要である
    • そのような攻撃は非常に複雑になりうるが、この方法自体はその前提に立っている
  • この検出法は、tarball の変更が最終成果物に影響を与える場合にのみ検出できる
    • NixOS の実行ファイルにはバックドアが含まれていなかったため、条件を別途有効化していなければ、この事例のバックドアは発見できなかった

1件のコメント

 
GN⁺ 2025-03-24
Hacker Newsのコメント
  • NixOS再現可能なビルドがxzバックドアを検出できなかった点は指摘しておくべきです。実際にはNixOSも悪意あるxzビルドをユーザーに配布していましたが、マルウェアがNixOSを標的にしていなかったため動作しなかっただけです
    NixOSの開発者も「バックドアが公開されたとき、悪意あるバージョンのxzが私たちのユーザーに配布されていた事実に驚いた」と述べていました。いつものことですが、理論と現実は違いますし、xzを可能にしたのは技術的な脆弱性というより、現実世界の人間の脆弱性でした。コミュニティは、こうした問題をより良いソフトウェアだけで常にパッチできるわけではないという事実を、なかなか認められません

    • Nixの宣言的な構成は、いくつもの方法で攻撃耐性を高めるのにかなり有用ですが、まだ活用されていない可能性が多くあります。個人的には細分化された使い捨てコンテナを最優先で実装したいですし、Guixにはすでにこうした機能があります
      すべてのプロセスを制限された権限で実行し、作業に必要なディレクトリを除けば~/にもアクセスできないようにできれば、たとえば悪意あるpipパッケージがSSHキーを盗むことを防げます。それでもxzバックドアがNixOSで動作しなかった理由は、NixOS特有の非FHSファイルシステム構造にあると見ています
    • それでもこの記事は良かったです。NixOSはソースコードと切り離されたビルド成果物、つまり再現不能な成果物を防ぐ技術的解決策を掲げており、xzバックドアはまさにビルド成果物の中に潜んでいたからです
    • その通りです。タイトルはNix方式がバックドアを検出できたかもしれない、という意味に見えましたが、実際にはNixをどう変えればそのようなバックドアを検出できるかを提案する記事に近いです
    • 再現可能なビルドが、他の発見経路より先にxzバックドアを実際に捕まえたわけではない、という事実を強調してくれて良かったです
      最近は、現実が客観的現実に対する個人の感情や価値観と合わないと、しばしば軽視されます。個人的な価値観はありますが、現実がそれに対する自分の感情より重要でないとは思いません。よく言っていたように、理論と現実の違いは、理論上は両者が同じでも、現実ではそうではないという点にあります。NixOSの再現可能なビルドがxz攻撃を検出できた可能性はあったが実際にはできなかった、という気づきが、今後そうしたビルドを分析して別の攻撃をより早く見つける発展につながることを願っています
    • 理由がちょっと面白いです。NixOSのブートストラップはソースコードをダウンロードしますが、そのソースはxzで圧縮されたtarballなのです
  • 著者は、今回たまたま起きたやり方に視野が狭まりすぎている感じがします。Jia Tan事件はサンプルが1つしかないので、その方法だけが可能だと見るのは近視眼的です
    ここで提案されている防御が通用しないシナリオはいくつも十分に想像できます。Nixユーザーとしても、NixOSがこれを検出できた可能性は低いと思います。実際に検出できなかったわけですから。次回は違う形で起きるかもしれないと今述べたばかりですが、証拠なしにNixを信じるのも愚かです

  • ここでNixOSはあまり関係ありません。xzバックドアは具体的にRed HatとDebianを標的にしていたからです
    同じ論理なら、xzバックドアはWindowsには影響しなかったと言うのも同じくらい関係があります。皮肉なことに、このバックドアは最終的にMicrosoft社員が発見しましたが、この点はしばしば見落とされます

    • 少し改善されたNixOSやGuixなら、このバックドアがリポジトリに入った瞬間に自動で検出できたはずです。だから関係があります
  • 記事では、ディストリビューションは従来のインストール用tarballの代わりに、バージョン管理システム、たとえばGithubから直接ソースコードを取得すべきだとしています
    ただ、これで何が解決するのかよく分かりません。悪意あるメンテナーがバイナリの塊をソースコードリポジトリに直接追加すればよいのでは?著者はGithubがコードを検証しているかのように信頼できると見ていますが、当然ながらGithubはそのような検証をしていません

    • この方式が解決するのは、「レビューされたもの」と「ソフトウェアのビルドに使われたソースコード」が異なることが多い、という問題です
      検証済みの再現可能なビルドは、xz utilsの侵害やSolarWinds Orionの改ざんのような事例を防ぐ助けになり得ましたし、十分に取り組む価値があります
    • その批判は少し的外れです。ソースからビルドすれば自分も皆もソースを検査できますが、提供されたtarballからビルドすると、autoconfの過程でファイルが変更されるため、元のソースと比べて本質的に異なります
      Githubのリリースからバイナリをダウンロードして実行するなら、メンテナーを完全に信頼するしかありません。Nixがそうするのはクローズドソースのパッケージだけです
  • 論旨は、XZのメンテナーがGitにチェックインされていないtarballの中に悪意あるコードを隠した、という事実に依拠しています
    著者は、NixがGitからtarballを生成してバイナリビルドに入れるよう設定できることを示しています。しかし、これがなぜNixやNixOSに必要な機能なのかは分かりません。RPMやDebに入るものを含め、どんなビルドシステムでも中間段階としてtarballを生成するよう設定できます。実際、Debianはかなり前から再現可能なビルドを重要な目標として取り組んできました。https://wiki.debian.org/ReproducibleBuilds

    • 記事は実際に「Leveraging bitwise reproducibility」節でreproducible-buildsプロジェクトを引用しています。この記事の要点は2つです
      第一に、NixOSのビルド過程ではxzがブートストラップの初期にあまりにも早く必要になるため、xzの完全なソースビルドを実行できませんでした。第二に、ブートストラップ初期に必要なnixpkgs依存関係が侵害されたかどうかを自動検知するためのnixpkgsの調整を提案しています。他のエコシステムももちろん完全なソースビルドを試して不一致を発見できます。記事の核心は、現在のnixpkgsではそれができないという点にあります
  • NixOSが防げた事例に注目するなら、CrowdStrike事故を見るべきです。今日の設定が動作しなければ昨日の設定で起動できるという点だけでも、被害の大半を軽減できたはずです

    • ただし、それは起動の柔軟性がないWindows側の問題です。UbuntuもZFS上なら同じことができます
  • 信頼しているフレームワークを使えば、そのフレームワークが攻撃されるまでは安全です。xz のバックドアは発見されたかもしれませんが、Nix エコシステムで動作させる目的で設計されたものではありませんでした。
    いつか Nix のコア開発者がスパイだったり、似たような状況になったりすれば、Nix エコシステムを狙った攻撃も出てくるでしょう。Nix が本質的に安全だ、というような答え方はしてほしくありません。1〜2年以内に Nix がうまく攻撃されたら、訪ねていって間違っていたと認めさせたくなるほどです。

    • 基準は過去も今後も 絶対的なセキュリティ ではありません。それは何ものも満たせない不可能な基準です。それでも、今日のソフトウェアが30年前より概して安全になっているのは客観的な事実です。
      より強い主張は「Nix は従来のビルドシステムより攻撃が難しく、コストも高い」というものです。ですから、Nix を安価に攻撃する方法を見つけたら、そのときに来てください。それまでは、技術的なレベルで Nix は代替システムより攻撃しにくいという話は、少なくとももっともらしく、実際にもかなり可能性が高いです。
    • バックドアが Nix を狙ったものではなかったとしても、露見しないためには Nix でビルドされている間にも疑いを持たれない必要がありました。
  • NixOS は少し先を行っていますが、ほとんどの他のディストリビューションもすべてをソースからコンパイルし、使用するソースが改ざんされていないかを暗号学的に検証し、パッケージ間に バージョン指定付き依存関係 を持っています。Debian にも再現可能ビルドがあります。
    問題は、ビルドシステムがソースからビルドする前に、事前コンパイル済みのオブジェクトファイルを削除していなかったことにあります。それを直しても、誰もソースコードを検査しなければ、望むだけバックドアを追加できますし、NixOS であれ他のディストリビューションであれ、それを防ぐことはできません。

  • 優れた技術分析ですが、タイトルは間違っており誤解を招きます。「技術的には正しい」と言えるかもしれませんが、好意的に見ても バックドアが仕込まれていたという意味 に近いものです。
    ユニオンファイルシステムのレイヤーを超えるビルド管理ツールが必要であることをよく示しています。たとえば、テストがビルド成果物を汚染できないように追跡し、強制する必要があります。ビルド過程でファイルがファイルに影響を与える 因果追跡グラフ を作成し、そのグラフを明示的に作ったうえで、それを強制するか、以前の追跡グラフとの差分を報告するような仕組みが必要です。

  • そのとおりです。バックドアを隠すのは確実により難しくなったでしょう。しかし、不可能には程遠いです。
    望むなら、いつでもソースコード内にバックドアを隠せます。ただ、もっともらしいバグに見えるようにするにはより多くの労力がかかり、検出される可能性も高くなるだけです。