先日公開された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
- スタックから公開鍵と署名が取り出される
- 実行中のインプットで実行されるスクリプトを
scriptCode
と呼び、そこからsubScript
を作成する。scriptCode
のスクリプト内のOP_CODESEPARATOR
を考慮したものがsubScript
。具体的な仕組みは、以前の記事のsubScriptとscriptCodeセクション参照参照。 subScript
内に署名データがある場合は、それをsubScript
から削除する(これがFindAndDeleteと呼ばれる処理)- 残っている
OP_CODESEPARATOR
をsubScript
から削除する - 署名の最終byteのSigHashタイプを削除
- 現在処理中のトランザクションのコピー(
txCopy
)を作成 txCopy
の全トランザクションインプットのスクリプトを空(1 byteの0x00)にするtxCopy
の実行中のインプットのscriptSigに↑のsubScript
をセットする- SigHashタイプを考慮してトランザクションを調整し、TxをシリアライズしたデータにSigHashタイプを加えた結果のdouble-SHA256を計算
- ハッシュ値をメッセージダイジェストとして、スタック上の公開鍵を使用して署名検証を実行する。
問題となったのは、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
のデータプッシュとsig
とfoo
を連結した値のデータプッシュがscriptCode
内にある場合、
- FindAndDeleteは、完全一致する
sig
のみを削除する - removeOpcodeByDataは、
sig
とsig||foo
の両方を削除する
この結果、Bitcoin Coreでは有効と判定される署名を、btcdでは無効と判定するようになる(その逆もしかり)。原因となったコードが導入されたコミットが76339ba。
Bitcoin Coreでは、現在FindAndDelete
により署名の削除が発生するようなスクリプトの場合、非標準トランザクション扱いになりリレーされないけど、sig||foo
のように署名に別のデータが付与されている場合、削除対象にならないので、そういうredeemScript
を持つP2SHを使用するトランザクションは標準トランザクションとしてリレーされる。そのため、上記のようなトランザクションを構成すればbtcdのチェーン分岐を促す攻撃自体は簡単にできる。もし、まだv0.24.2にアップグレードしていない場合は、早急にアップグレードした方がいい。
*1:ANYPREVOUTが導入されれば回避できる