[CTF Writeup] Devcon 5 - "Chain Relay Under Attack CTF"
の3日目に開催されたworkshop「Nuts and Bolts of Cross-Chain Communication」でのCTFが新鮮で面白かったです。(DEFCONではない)
問題はシンプルで、「Ethereumのスマートコントラクトが与えられる。このコントラクトはBitcoinのブロックチェーンを構築するが、不正なブロックが送られるとブロックチェーンが壊れる。直してね」というもの。なんでこういう問題かっていうのは、彼らのクロスチェーンコミュニケーションの論文を参照。
Flagを取る形式ではなく、実際に攻撃を捌ければ点数が貰えるフルフィードバック形式。
問題§
問題リポジトリ: crossclaim/devcon5-relay-attack: Game repo for DEVCON 5 Cross-Chain Communication Workshop
attackTestCases.test.min.js
にサンプルケースが転がってます。
testで結果が変わることがあって大変でした。
TESTCASE 1: Do we allow users to reset the relay with a new genesis block at will??§
サンプルケース
it("TESTCASE 1: set duplicate initial parent - should fail",
async () => {
l(),
await c.reverts(relay.setInitialParent(genesis.header,
genesis.height))
}),
Genesis block が改ざんされないようにする問題。
require(_headers[blockHeaderHash].merkleRoot == 0, ERR_GENESIS_ALREADY_SET);
や
require(_heaviestHeight == 0, ERR_GENESIS_ALREADY_SET);
を追加すればいい。
TESTCASE 2: Maybe we should not allow duplicate submissions?§
it("TESTCASE 2: duplicate block submission - should fail",
async () => {
l(),
block1 = r[1];
let e = await relay.submitBlockHeader(block1.header);
c.eventEmitted(e,
"StoreHeader",
e => e.blockHeight == block1.height),
await c.reverts(relay.submitBlockHeader(block1.header),
t.ERROR_CODES.ERR_DUPLICATE_BLOCK)
}),
すでに block が store されてきたらダメ。
require(_headers[hashCurrentBlock].merkleRoot == 0, ERR_DUPLICATE_BLOCK_SUBMISSION);
TESTCASE 3a, 3b: block header is provided by the user. What could go wrong??§
it("TESTCASE 3a: too large block header - should fail",
async () => {
l(),
block1 = r[1],
await c.reverts(relay.submitBlockHeader(block1.header + "123"),
t.ERROR_CODES.ERR_INVALID_HEADER)
}),
it("TESTCASE 3b: too small block header - should fail",
async () => {
l(),
block1 = r[1],
await c.reverts(relay.submitBlockHeader(block1.header.substring(0,
28)),
t.ERROR_CODES.ERR_INVALID_HEADER)
}),
ブロックヘッダーの長さは80byteでなくてはいけない。
require(blockHeaderBytes.length == 80, ERR_INVALID_HEADER_FORMAT);
TESTCASE 4: Shall we make sure we are building a chain and not storing random blocks?§
it("TESTCASE 4: submit block where prev block is not in main chain - should fail",
async () => {
l(),
block2 = r[2],
await c.reverts(relay.submitBlockHeader(block2.header),
t.ERROR_CODES.ERR_PREV_BLOCK)
}),
Previous block が store されてなければ、不正。
require(_headers[hashPrevBlock].merkleRoot != 0, ERR_PREV_BLOCK_NOT_FOUND);
TESTCASE 5: Did the miner do the work?§
it("TESTCASE 5: weak block submission - should fail",
async () => {
fakeGenesis = {
hash: "0x00000000000000000012af6694accf510ca4a979824f30f362d387821564ca93",
height: 597613,
merkleroot: "0x1c7b7ac77c221e1c0410eca20c002fa7b6467ba966d700868928dae4693b3b78",
header: "0x00000020614db6ddb63ec3a51555336aed1fa4b86e8cc52e01900e000000000000000000783b3b69e4da28898600d766a97b46b6a72f000ca2ec10041c1e227cc77a7b1c6a43955d240f1617cb069aed"
},
fakeBlock = {
hash: "0x000000000000000000050db24a549b7b9dbbc9de1f44cd94e82cc6863b4f4fc0",
height: 597614,
merkleroot: "0xc090099a4b0b7245724be6c7d58a64e0bd7718866a5afa81aa3e63ffa8acd69d",
header: "0x0000002093ca64158287d362f3304f8279a9a40c51cfac9466af120000000000000000009dd6aca8ff633eaa81fa5a6a861877bde0648ad5c7e64b7245720b4b9a0990c07745955d240f16171c168c88"
},
await relay.setInitialParent(fakeGenesis.header,
fakeGenesis.height),
await c.reverts(relay.submitBlockHeader(fakeBlock.header),
t.ERROR_CODES.ERR_LOW_DIFF)
}),
uint256 target = getTargetFromHeader(blockHeaderBytes)
で、difficulty target を取得している。 ブロックのハッシュ値がこれを上回らなければOK。
require(hashCurrentBlock <= bytes32(target), ERR_DIFF_TARGET_HEADER);
TESTCASE 6: txid is provided by the user. What could go wrong?§
it("TESTCASE 6: empty txid - should fail",
async () => {
l(),
block1 = r[1];
let e = await relay.submitBlockHeader(block1.header);
c.eventEmitted(e,
"StoreHeader",
e => e.blockHeight == block1.height),
tx = block1.tx[0],
await c.reverts(relay.verifyTx("0x0000000000000000000000000000000000000000000000000000000000000000",
block1.height,
tx.tx_index,
tx.merklePath,
0),
t.ERR_INVALID_TXID)
}),
トランザクションIDが0だったら不正。
require(txid != bytes32(0), ERR_INVALID_TXID);
TESTCASE 8: Are we sure this transaction is "securely" included?§
it("TESTCASE 8: missing tx confirmation check - should fail",
async () => {
l(),
block1 = r[1];
let e = await relay.submitBlockHeader(block1.header);
c.eventEmitted(e,
"StoreHeader",
e => e.blockHeight == block1.height),
confirmations = 10,
r.slice(2,
4).forEach(e => { relay.submitBlockHeader(e.header) }),
tx = block1.tx[0],
await c.reverts(relay.verifyTx(tx.tx_id,
block1.height,
tx.tx_index,
tx.merklePath,
confirmations),
t.ERR_CONFIRMS)
})
10 confirmations してるか確認すればいい
require(_headers[_heaviestBlock].blockHeight - txBlockHeight >= confirmations, ERR_CONFIRMS)