攻撃者はGitのコミット署名フォーマットにおける署名の可変性(signature malleability)を悪用することで、「検証済み(Verified)」とされたGitHubコミットを密かに複製できます。内容は同一でありながらバイト単位では異なるコミットを作成し、有効な署名を保持したまま、新しいハッシュ値のもとで新たな「検証済み」バッジを取得できてしまうのです。
これは、検証済みコミットハッシュが特定の署名済みコンテンツを示す一意かつ不変の識別子であるという、長年の前提を崩すものです。その結果、ハッシュに基づくブロック処理、依存関係のピン留め、再現可能なビルド(reproducible build)のワークフローが、巧妙なサプライチェーン攻撃にさらされることになります。
複製される検証済みGitHubコミット
Gitのセキュリティモデルでは、コミットハッシュはツリー、親コミットのリスト、作者およびコミッターのメタデータ、コミットメッセージ、そしてgpgsigヘッダーに埋め込まれた署名のバイト列までを包含するコンテンツアドレス型の識別子として扱われます。
Networksecurity solutions
署名自体がハッシュ計算の対象範囲内に含まれているため、署名が証明する論理的な内容を変えることなく、そのバイトエンコーディングだけを変更すれば、ツリーやメタデータには一切手を加えずにコミットハッシュだけを変化させることができてしまいます。
検証プロセスが求めているのは、あくまで対象データに対する「有効な署名」であって、その署名の一意な正規エンコーディングではありません。この点が、攻撃者にとって検証を通過させたままコミットのシリアライズ形式を書き換える余地を生んでいます。
Jacob Ginesin氏による「Git Hash Chain Malleability(Gitハッシュチェーンの可変性)」に関する新たな研究では、現在GitHubが対応しているすべてのGPGベースの署名方式――ECDSA、RSA、EdDSA、S/MIME/CMS――を網羅する、3つの実用的な改変手法が示されています。
ECDSAの場合、古典的な代数的対称性を利用し、公開の曲線パラメータのみを使って有効な署名ペア(r,s)(r,s)(r,s)を(r,n−s)(r, n – s)(r,n−s)へと変換できます。これにより、同一のコミットデータに対して有効性を保ったまま、バイト列が異なる第2の署名が得られ、結果として新たなハッシュ値が生成されます。
RSAおよびEdDSA(OpenPGP形式)を狙う場合、攻撃はRFC 4880 §5.2.3で定義される非ハッシュ化サブパケット領域を標的とし、無視されるはずの実験的サブパケットを追加します。
重要なのは、GitHubのサーバー側検証がこれら3つの改変手法すべてを受理してしまい、改変された各コミットを、新しいコミットハッシュのみをキーとした、独立かつ永続的な「検証済み」エントリとして記録してしまう点です。
GitHubは検証前にOpenPGPやCMSのコンテナを正規化していません。非正規形式のECDSAスカラー値を受け入れ、助言的な非ハッシュ化サブパケットを無視し、基礎となる署名が検証できさえすれば非DER形式のCMSエンベロープも許容してしまいます。そのため、2つの「検証済み」コミットが実は同一の内容をエンコードしているという事実を、インターフェース上では一切示すことができません。
比較表示画面では、オリジナルとその改変された双子が別々のブランチにプッシュされた場合、ツリーはバイト単位で同一であるにもかかわらず「1コミット進んでいる、1コミット遅れている」と表示されることがあり、実際には存在しない乖離があるかのような誤った印象を与えてしまいます。
この「ハッシュチェーンの可変性」は個々のコミットにとどまらず連鎖的に波及します。Gitの各コミットは親コミットをハッシュ値で指定するため、署名済みコミットを書き換えると、内容自体は変わっていなくても、すべての子孫コミットが親フィールドを更新し、新たなハッシュ値を生成せざるを得なくなるのです。
Ginesin氏は、gpgsigヘッダー内の署名タイプを自動検出し、適切な改変手法を適用したうえで、グラフの整合性を保つよう子孫コミットを書き換え、さらにブランチポインタを新しいHEADへと進めるという、概念実証(PoC)ツールを公開しています。
公開されているデモリポジトリでは、S/MIME署名およびEdDSA署名それぞれについて、この手法で改変されたコミットが示されており、いずれも有効なGitHubの「検証済み」バッジを保持したままです。これはArxivが報じたところによるものです。
ソフトウェアサプライチェーンに対する下流への影響は甚大です。既知の悪意あるコミットをSHA値でブロックするハッシュベースのインシデント対応制御やプッシュ保護ルールは、ブロックリストに載っていない、新たに生成された同じく「検証済み」のハッシュ値のもとで、いわば「幽霊の双子」を再プッシュするだけで簡単に回避できてしまいます。
Nix flakes、Goモジュールの擬似バージョン、ピン留めされたGitHub ActionsのSHA値、あるいは特定のコミットを参照するDockerfileなど、コミットハッシュに依存する依存関係ピン留めの仕組みは、正規のフォージ(forge)とは異なる、別途有効に署名されたハッシュチェーンを提供するミラーによって、一見信頼できるように見せかけながら誤誘導されてしまう可能性があります。
コミットハッシュをリビジョンの正となる識別子として扱う、再現可能ビルドシステムやSLSAのような来歴(provenance)フレームワークも、この脆弱性をまるごと引き継いでしまいます。つまり「検証済み」のハッシュ値はもはやコンテンツを一意に識別するものではなくなり、Sigstoreの Rekor のようなログで用いられる、不変のソースや永続的な信頼の基点という前提そのものを揺るがすことになります。
こうした対策が広く展開されるまでの間、防御側にとっての結論は明白です。署名済みコミットもGitHubの「検証済み」バッジも、特にCI/CDやサプライチェーンにおいて高い保証が求められる文脈では、コンテンツを一意に識別するものとして扱うことはできません。
ハッシュに基づく重複排除、ブロック処理、ピン留めは、攻撃者が巧妙に操作しうる生の署名済みオブジェクトのバイト列に対して直接行うのではなく、コンテンツの正規化と署名検証を終えた後に行うべきです。
Windows、Linux、macOSの仮想マシンでサイバー脅威を実際に操作し、攻撃チェーン全体を再現 - ANY RUNでマルウェアとフィッシングを解析
Antivirus& Malware
翻訳元: https://gbhackers.com/duplicate-verified-github-commits-using-signature-malleability/