最近公開された、2018年のBitcoin Coreに存在していた脆弱性に関するペーパー↓
https://invdos.net/paper/CVE-2018-17145.pdf
この脆弱性はもともとJavaScriptで実装されたBitcoinのノード実装の1つであるBcoinで発見されたもので、攻撃方法は同じでも、BcoinとBitcoin Coreで脆弱性が現れるコードには違いがある。またホワイトペーパーの説明にも一部間違いがあるようなので、その辺りも含めてみてみよう。
尚、Bitcoin Coreの場合、脆弱性の影響を受けるのはv0.16.0とv0.16.1とされている。
CVE-2018-17145の内容
CVE-2018-17145はBitcoinのP2Pメッセージの1つであるinv
メッセージを使った攻撃になる。
invメッセージ
inv
メッセージは新しいブロックやトランザクションの存在を接続中のピアに通知する際に使用される。inv
メッセージにはブロックやトランザクションのハッシュのリストが含まれており、inv
を受信したノードは自身が対象のハッシュ値を持つブロックやトランザクションを持っていない場合、それをgetdata
メッセージを使って要求する。
inv
メッセージは最大50,000個のアイテム(トランザクションハッシュ or ブロックハッシュ)を一度に送信できる。
攻撃方法
攻撃方法はシンプルで、インバウンド接続を許可しているノードに接続し、49,999個(ペーパーに49,999と書いてあるけど、上限が50,000なので別に50,000でも良いはず)のランダムなハッシュを生成し、それをinv
メッセージにセットして送信し、getdata
要求が来ても対応するトランザクションデータを返さない(元々ランダムに生成したハッシュなので、その元となる有効なトランザクションなど存在しない)。これを繰り返す。
ただ、この攻撃が成功するには、攻撃対象のノードがinv
メッセージで受信したハッシュリストの内、未確認のハッシュをトラッキングしているノードである必要がある。未確認のトランザクションのハッシュをメモリ上に保持しているノード実装であれば、この攻撃の影響を受ける。1 Gbpsのネットワーク帯域を持つ環境であれば、125MB/秒のデータ送信が可能なため、49,999個の32バイトのハッシュ値を持つinv
メッセージを秒間約83回送信できる。するとすぐにノードのメモリは膨れ上がり、Out-of-Memoryを引き起こさせノードを応答不能にすることができる。
ノードは対応するTxが確認されていない状態で受信したinv
のTxのリストを他のピアにリレーすることはないので、この攻撃はターゲットノードを絞った上で個別に実行する必要がある。
Bcoinの脆弱性の原因
Bcoinでは、↑のように受信したinv
のアイテムをトラッキングしていたため、この攻撃の影響を受ける。
このinv
のアイテムをトラッキングしている理由は、未確認のトランザクションハッシュのトランザクションについてgetdata
メッセージでデータを要求する際、既にgetdata
を送信済みのデータについて再度getdata
を送信しないために送信済みかどうかのチェックをするためで、BcoinではハッシュリストをMapで管理していた。
修正のコミットは↓で、
1つのピアからの未確認トランザクションハッシュの上限を10000とし、それを超えたらそのピアとの接続を切断するという対策を採っている。
Bitcoin Coreの脆弱性の原因
Bitcoin Coreの場合、Bcoinのように単純にトラッキングしているという訳ではなく、正確にはinv
で受信したアイテムをウォレットに通知するためのコードが対象になる。
↑のペーパーで、Bitcoin Coreでもinv
メッセージを処理する際の機能していない制限のチェックとして以下のsrc/net_processing.cpp
のコードをリストアップしているが、これは正しくない。このコードはinv
メッセージ内のハッシュの数が50,000より大きくないかチェックしてるだけであって、inv
のトラッキングをするためのコードではない。
if (vInv.size() > MAX_INV_SZ) { LOCK(cs_main); Misbehaving(pfrom->GetId(), 20, strprintf("message inv size() = %u", vInv.size())); return false; }
実際にBitcoin Coreでチェックが動作しているのは、src/net.cpp
の以下のコードだ
if (mapAskFor.size() > MAPASKFOR_MAX_SZ || setAskFor.size() > SETASKFOR_MAX_SZ) return;
mapAskFor
は、未確認のトランザクションハッシュについて、getdata
を要求するために確保されるMapで、MAPASKFOR_MAX_SZ
はinv
の上限となじ50,000なので、Bcoinと同じトラッキングの意味としては、対応がされている。
Bitcoin Coreで実際に問題となるのは、トラッキングではなくinv
で受信したアイテムをウォレットに通知するためのコードになる。そのコードが、src/net_processing.cpp
のinv
メッセージを処理の最後の以下のコード↓
// Track requests for our stuff
GetMainSignals().Inventory(inv.hash);
ペーパーでは、その中身(src/validationinterface.cpp
)がリストアップされている↓
void CMainSignals::Inventory(const uint256 &hash) { m_internals->m_schedulerClient.AddToProcessQueue([hash, this] { m_internals->Inventory(hash); }); }
このコードが導入されたのが↓のコミットで、
ウォレットへのinv
のアイテムの通知処理を、バックグラウンドスレッドで動作するようキュー化したもの。この場合、攻撃はバックグラウンドで動作する通知処理のスピードを、inv
で送られてくるアイテムによるキューの成長が上回る場合に成功する。実際にどういうマシンスペック、ネットワーク帯域の下で成功するかについては、シミュレーション結果などはホワイトペーパーでは提供されていない。
問題のコードは以下のコミットで修正されている。
修正方法としては、Inventoryの通知自体を削除している。これはバックグラウンドのキュー化以前に戻してもmainスレッドでの処理は発生するからか?まぁ未確認のハッシュをそもそもウォレットに通知する必要もないのだろうけど。
Bitcoin Core以外にもLitecoinやbtcdなどにも影響があったようだが、↑でも分かるよう実装によって脆弱性のポイントは違う可能性はある。