突如として行われた緊急フォーク
2022/10/25、緊急のフォークが行われ、Catapult内の脆弱性が修正されました。具体的な内容はこちら
書いてあることは難しいですが、中々にヤバそうなことがかかれています。せっかくなのでこの修正された脆弱性を古いCatapultで再現してみましょう
問題のコード
脆弱性があったコードは次のものと書かれています
hashes.splice(i / 2, 0, this.hash([hashes[i], hashes[i + 1]]));
このコードではマークルコンポーネントハッシュの計算を行いますが、第2引数が0に指定されたことによって置き換えではなく挿入を行っていることがわかります。その結果最初のコンポーネントハッシュ2つの計算以外がうまく行えていなかったことがわかります。つまり、最初の2つのトランザクション以外は署名の検証がされていなかったということです。悪用ができる致命的なバグですね。
恥ずかしいことに、私は以前、署名の検証をするためにこのコードを見た覚えがあるのですが全く気づきませんでした。確かここの計算で詰まった覚えだけはあったのですが公式のコードを見て納得していた覚えがあります、悔しいです。次こそは見つけ出してやります。
再現する
symbol-bootstrapをフォーク前のバージョンに落とし、プライベートネットワークを構築します。フォーセットを作成して配布するのは面倒なので手数料は0にして起動しましょう
minFeeMultiplier: 0
symbol-bootstrap start -d -p bootstrap -a dual -c custom.yml
2つのインナートランザクションのアグリゲートコンプリートをつくってみます
今回はaがbに、bがaにそれぞれ"ping","pong"と送り合うトランザクションを作ってみました
sdkを利用する場合は古いものを使用しましょう(アグリゲートのバージョンの問題)
const aToB = TransferTransaction.create(
Deadline.create(epochAdjustment),
b_account.address,
[],
PlainMessage.create("ping"),
networkType
).setMaxFee(feemultiplier);
const bToA = TransferTransaction.create(
Deadline.create(epochAdjustment),
a_account.address,
[],
PlainMessage.create("pong"),
networkType
).setMaxFee(feemultiplier);
const bToA_2 = TransferTransaction.create(
Deadline.create(epochAdjustment),
a_account.address,
[],
PlainMessage.create("pongpong"),
networkType
).setMaxFee(feemultiplier);
let init_tx = AggregateTransaction.createComplete(
Deadline.create(epochAdjustment),
[
aToB.toAggregate(a_account.publicAccount),
bToA.toAggregate(b_account.publicAccount),
],
networkType,
[]
).setMaxFeeForAggregate(feemultiplier,1);
const s_init = a_account.sign(init_tx, networkGenerationHash);
const b_cosig = CosignatureTransaction.signTransactionPayload(b_account, s_init.payload, networkGenerationHash);
const final_stx = a_account.signTransactionGivenSignatures(
init_tx,
[
b_cosig
],
networkGenerationHash
);
const res = await transactionHttp.announce(final_stx).toPromise();
作成したfinal_stxのペイロードをbToA_2 のペイロードに差し替えます。
再現すると題名をつけておきながら申し訳ないのですが、差し替え方法は念の為、非開示とします
差し替え前、後のトランザクションをアナウンスします
では3つのアグリゲートにしてみましょう
メッセージを新たに考えるのが面倒だったのでaが2回"ping"とおくるトランザクションにしました
const aToB = TransferTransaction.create(
Deadline.create(epochAdjustment),
b_account.address,
[],
PlainMessage.create("ping"),
networkType
).setMaxFee(feemultiplier);
const bToA = TransferTransaction.create(
Deadline.create(epochAdjustment),
a_account.address,
[],
PlainMessage.create("pong"),
networkType
).setMaxFee(feemultiplier);
const bToA_2 = TransferTransaction.create(
Deadline.create(epochAdjustment),
a_account.address,
[],
PlainMessage.create("pongpong"),
networkType
).setMaxFee(feemultiplier);
let init_tx = AggregateTransaction.createComplete(
Deadline.create(epochAdjustment),
[
aToB.toAggregate(a_account.publicAccount),
aToB.toAggregate(a_account.publicAccount),
bToA.toAggregate(b_account.publicAccount),
],
networkType,
[]
).setMaxFeeForAggregate(feemultiplier,1);
const s_init = a_account.sign(init_tx, networkGenerationHash);
const b_cosig = CosignatureTransaction.signTransactionPayload(b_account, s_init.payload, networkGenerationHash);
const final_stx = a_account.signTransactionGivenSignatures(
init_tx,
[
b_cosig
],
networkGenerationHash
);
const res = await transactionHttp.announce(final_stx).toPromise();
公表されている脆弱性の内容通りであれば3つめ以降のトランザクションのペイロードを差し替えた場合成功するはずです。3つ目のペイロードを差し替え、アナウンスしてみます
通っちゃったよ....
無事(?)脆弱性があることを再現できました
バグは潰しきれない
symbolはローンチ前に負荷テストを行ったりしてバグをある程度潰していますが、一般的にテストはバグの存在を示すことはできてもバグが存在しないことを示すことはできません。よって、これからも何らかのバグが見つかるかもしれません。バグが見つかった際はすぐにアップデートを行い、対応するようにしましょう