Develop with pleasure!

福岡でCloudとかBlockchainとか。

btcd v0.24.2未満に存在したコンセンサスバグの内容

先日公開されたbtcd v0.24.2未満に存在したコンセンサスバグについて↓

CVE-2024-38365 public disclosure (btcd `FindAndDelete` bug) - Implementation - Delving Bitcoin

署名検証時の動作

バグの原因となっていたのは、Segwit導入前のレガシーなトランザクションの署名検証時に、署名対象のメッセージを構築する際の処理。実際に署名検証時に行われる操作は↓

https://en.bitcoin.it/wiki/OP_CHECKSIG#How_it_works

  1. スタックから公開鍵と署名が取り出される
  2. 実行中のインプットで実行されるスクリプトscriptCodeと呼び、そこからsubScriptを作成する。scriptCodeスクリプト内のOP_CODESEPARATORを考慮したものがsubScript。具体的な仕組みは、以前の記事subScriptとscriptCodeセクション参照参照。
  3. subScript内に署名データがある場合は、それをsubScriptから削除する(これがFindAndDeleteと呼ばれる処理)
  4. 残っているOP_CODESEPARATORsubScriptから削除する
  5. 署名の最終byteのSigHashタイプを削除
  6. 現在処理中のトランザクションのコピー(txCopy)を作成
  7. txCopyの全トランザクションインプットのスクリプトを空(1 byteの0x00)にする
  8. txCopyの実行中のインプットのscriptSigに↑のsubScriptをセットする
  9. SigHashタイプを考慮してトランザクションを調整し、TxをシリアライズしたデータにSigHashタイプを加えた結果のdouble-SHA256を計算
  10. ハッシュ値をメッセージダイジェストとして、スタック上の公開鍵を使用して署名検証を実行する。

問題となったのは、3のFindAndDeleteの処理。Bitcoin CoreのFindAndDeleteでは、署名と完全一致するデータのみをsubScriptから削除する。

ただ、scriptPubkeyやP2SHのredeemScript内に署名が含まれるようなケースはほぼないので、実際にFindAndDeleteで署名が削除されるのは特殊なケース。Bitcoin Coreのテストケースで登場するのは、

OP_CHECKSIGVERIFY 30450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01

というOP_CHECKSIGVERIFYと署名データで構成されるredeemScriptのP2SHを使用するトランザクション

010000000169c12106097dc2e0526493ef67f21269fe888ef05c7a3a5dacab38e1ac8387f1581b0000b64830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e0121037a3fb04bcdb09eba90f69961ba1692a3528e45e67c85b200df820212d7594d334aad4830450220487fb382c4974de3f7d834c1b617fe15860828c7f96454490edd6d891556dcc9022100baf95feb48f845d5bfc9882eb6aeefa1bc3790e39f59eaa46ff7f15ae626c53e01ffffffff0101000000000000000000000000

この場合、scriptCodeとして上記のredeemScriptがセットされ、FindAndDeleteで署名値が削除される。

※上記のトランザクションは特殊なトランザクションで、現状UTXOのredeemScript内にそれを使用する際の署名を入れることはできない。これは、そのような署名を作るためには、そのUTXOを含むトランザクションのtxidが必要になるが、txidを計算するためには署名が必要という循環依存問題が発生するため*1。↑では、そのような制約があるなかで署名検証をパスするように、署名データから公開鍵を復元し、その公開鍵をscriptSigにセットするというやり方で作られている。

※ なお、FindAndDelete処理が行われるのはレガシーなUTXOに対してのみで、segwitではこのような処理は行われない。

btcdの実装

btcdの実装において、↑のFindAndDelete相当の処理をしているのがremoveOpcodeByData関数。

署名データをsigとすると、

<sig> <sig||foo>

のように1つのsigのデータプッシュとsigfooを連結した値のデータプッシュがscriptCode内にある場合、

  • FindAndDeleteは、完全一致するsigのみを削除する
  • removeOpcodeByDataは、sigsig||fooの両方を削除する

この結果、Bitcoin Coreでは有効と判定される署名を、btcdでは無効と判定するようになる(その逆もしかり)。原因となったコードが導入されたコミットが76339ba

Bitcoin Coreでは、現在FindAndDeleteにより署名の削除が発生するようなスクリプトの場合、非標準トランザクション扱いになりリレーされないけど、sig||fooのように署名に別のデータが付与されている場合、削除対象にならないので、そういうredeemScriptを持つP2SHを使用するトランザクションは標準トランザクションとしてリレーされる。そのため、上記のようなトランザクションを構成すればbtcdのチェーン分岐を促す攻撃自体は簡単にできる。もし、まだv0.24.2にアップグレードしていない場合は、早急にアップグレードした方がいい。

*1:ANYPREVOUTが導入されれば回避できる