序文
出典: コボ・グローバル
序文
ETHがPoSコンセンサスシステムをアップグレードするにつれて、元のPoWメカニズムのETHチェーンは一部のコミュニティ(以下ETHWと呼びます)の支援を受けてハードフォークすることに成功しました。ただし、一部のオンチェーン プロトコルは設計の開始時にハード フォークの可能性に備えて準備されていなかったため、対応するプロトコルには ETHW フォーク チェーンに特定のセキュリティ リスクがあり、最も深刻なセキュリティ リスクはリプレイ攻撃です。
ハード フォークの完了後、ETHW メイン ネットワーク上でリプレイ メカニズムを使用した少なくとも 2 つの攻撃がありました。OmniBridgeリプレイ攻撃とPolygon Bridge最初のレベルのタイトル
リプレイの種類
まず、分析を開始する前に、リプレイ攻撃の種類について予備的に理解する必要があります。一般に、リプレイ攻撃は 2 つのカテゴリに分類されます。そしてそして署名付きメッセージの再生副題
トランザクションのリプレイ
副題
署名付きメッセージの再生
最初のレベルのタイトル
オムニブリッジとポリゴンブリッジの攻撃原理
副題
OmniBridge
OmniBridge は、xDAI と ETH メインネット間の資産転送に使用されるブリッジであり、主にブリッジの指定されたバリデーターに依存してクロスチェーン メッセージを送信し、クロスリンク資産の転送を完了します。 OmniBridge では、バリデーターによって送信される検証メッセージのロジックは次のとおりです。
function executeSignatures(bytes _data, bytes _signatures) public {
_allowMessageExecution(_data, _signatures);
bytes32 msgId;
address sender;
address executor;
uint32 gasLimit;
uint8 dataType;
uint256[2] memory chainIds;
bytes memory data;
(msgId, sender, executor, gasLimit, dataType, chainIds, data) = ArbitraryMessage.unpackData(_data);
_executeMessage(msgId, sender, executor, gasLimit, dataType, chainIds, data);
}
この関数では、まず、#L2 行の署名チェックに従って、送信された署名が指定されたバリデータによって署名されているかどうかが判断され、その後、#L11 行でデータ メッセージがデコードされます。デコードされたコンテンツから、返されたフィールドにchainIdフィールドが含まれていることを見つけるのは難しくありません。つまり、署名されたメッセージは再生できないということですか?分析を続けてみましょう。
function _executeMessage(
bytes32 msgId,
address sender,
address executor,
uint32 gasLimit,
uint8 dataType,
uint256[2] memory chainIds,
bytes memory data
) internal {
require(_isMessageVersionValid(msgId));
require(_isDestinationChainIdValid(chainIds[1]));
require(!relayedMessages(msgId));
setRelayedMessages(msgId, true);
processMessage(sender, executor, msgId, gasLimit, dataType, chainIds[0], data);
}
_executeMessage 関数をトレースすると、#L11 行でchaindId の有効性をチェックしたことがわかります。 function _isDestinationChainIdValid(uint256 _chainId) Internal returns (bool res) {
return _chainId == sourceChainId();
}
function sourceChainId() public view returns (uint256) {
return uintStorage[SOURCE_CHAIN_ID];
}
後続の関数ロジックの分析を続けると、chainId のチェックが実際にはチェーン自体のchainId を取得するために evm の元のchainId オペコードを使用せず、uintStorage 変数に格納されている値を直接使用していることを見つけるのは難しくありません。この値は管理者によって設定されるため、メッセージ自体にはチェーン識別子がないと考えられ、理論的には署名されたメッセージを再生することが可能です。
副題
Polygon Bridge
Omni Bridge と同様に、Polygon Bridge は Polygon と ETH メインネット間の資産転送のためのブリッジです。オムニ ブリッジとは異なり、ポリゴン ブリッジは引き出しのブロック証明に依存しており、ロジックは次のとおりです。
function exit(bytes calldata inputData) external override {
//...重要でないロジックを省略します
// verify receipt inclusion
require(
MerklePatriciaProof.verify(
receipt.toBytes(),
branchMaskBytes,
payload.getReceiptProof(),
payload.getReceiptRoot()
),
"RootChainManager: INVALID_PROOF"
);
// verify checkpoint inclusion
_checkBlockMembershipInCheckpoint(
payload.getBlockNumber(),
payload.getBlockTime(),
payload.getTxRoot(),
payload.getReceiptRoot(),
payload.getHeaderNumber(),
payload.getBlockProof()
);
ITokenPredicate(predicateAddress).exitTokens(
_msgSender(),
rootToken,
log.toRlpBytes()
);
}
関数ロジックを通じて、トランザクションが実際にサブチェーン (プロイゴン チェーン) で発生することを確認するために、transactionRoot と BlockNumber をそれぞれチェックすることにより、コントラクトが 2 つのチェックを通じてメッセージの正当性を判断していることを見つけるのは難しくありません。誰でもトランザクション データを通じて独自のtransactionRootを構築できるため、チェックは実際にはバイパスできますが、_checkBlockMembershipInCheckpointのロジックを見ることでわかるため、2番目のチェックはバイパスできません。
function _checkBlockMembershipInCheckpoint(
uint256 blockNumber,
uint256 blockTime,
bytes32 txRoot,
bytes32 receiptRoot,
uint256 headerNumber,
bytes memory blockProof
) private view returns (uint256) {
(
bytes32 headerRoot,
uint256 startBlock,
,
uint256 createdAt,
) = _checkpointManager.headerBlocks(headerNumber);
require(
keccak256(
abi.encodePacked(blockNumber, blockTime, txRoot, receiptRoot)
)
.checkMembership(
blockNumber.sub(startBlock),
headerRoot,
blockProof
),
"RootChainManager: INVALID_HEADER"
);
return createdAt;
}
対応する headerRoot は _checkpointManager コントラクトから抽出されます。このロジックに従って、_checkpointManager が headerRoot を設定する場所を確認します。
function submitCheckpoint(bytes calldata data, uint[3][] calldata sigs) external {
(address proposer, uint256 start, uint256 end, bytes32 rootHash, bytes32 accountHash, uint256 _borChainID) = abi
.decode(data, (address, uint256, uint256, bytes32, bytes32, uint256));
require(CHAINID == _borChainID, "Invalid bor chain id");
require(_buildHeaderBlock(proposer, start, end, rootHash), "INCORRECT_HEADER_DATA");
// check if it is better to keep it in local storage instead
IStakeManager stakeManager = IStakeManager(registry.getStakeManagerAddress());
uint256 _reward = stakeManager.checkSignatures(
end.sub(start).add(1),
/**
prefix 01 to data
01 represents positive vote on data and 00 is negative vote
malicious validator can try to send 2/3 on negative vote so 01 is appended
*/
keccak256(abi.encodePacked(bytes(hex"01"), data)),
accountHash,
proposer,
sigs
);
//....残りのロジックは省略
コードの #L2 行で、署名データが borChianId のみをチェックし、チェーン自体のchainId をチェックしていないことを見つけるのは難しくありません。メッセージはコントラクトで指定された提案者によって署名されているため、理論的には攻撃者も次のことを行うことができます。フォークされたチェーン上で提案者のメッセージ署名を再生し、正当な headerRoot を送信してから、Polygon Bridge を使用して ETHW チェーンの exit 関数を呼び出し、対応するトランザクション マークル証明を送信すると、出金が成功して headerRoot チェックに合格することができます。
アドレス 0x7dbf18f679fa07d943613193e347ca72ef4642b9 を例にとると、このアドレスは次の手順で ETHW チェーン上のアービトラージを正常に完了しています。
まず第一に、メインネット取引所からコインを引き出すには紙幣の機能を頼りにします。
Polygon Bridge の DepositFor 関数を通じて、Polygon チェーンにコインをデポジットします。
ETH メイン ネットワークは、コインを引き出すために Polygon Bridge の exit 関数を呼び出します。
ETHメインネット提案者によって送信されたheaderRootをコピーして抽出します。
前のステップで抽出した提案者の署名メッセージを ETHW で再生します。
最初のレベルのタイトル
なぜこうなった?
上記で分析した 2 つの例から、これら 2 つのプロトコルが ETHW に対するリプレイ攻撃に遭遇したことを見つけるのは難しくありません。これは、プロトコル自体に適切なリプレイ防止保護がなかったためで、その結果、フォークされたチェーン上でプロトコルに対応する資産が空洞化してしまいました。 。ただし、2 つのブリッジ自体は ETHW フォーク チェーンをサポートしていないため、ユーザーが損失を被ることはありません。しかし、私たちが考慮しなければならないのは、なぜこれら 2 つの橋が設計の初めにリプレイ保護対策を追加しなかったのかということです。実際、その理由は非常に単純で、OmniBridge であれ Polygon Bridge であれ、彼らが設計したアプリケーション シナリオは非常に単一であり、それらは自分で指定した対応するチェーンにアセットを転送するためにのみ使用されるためです。チェーン展開のため、リプレイ保護はありません。プロトコル自体にはセキュリティへの影響はありません。
対照的に、ETHW 上のユーザーは、これらのブリッジがマルチチェーン シナリオをサポートしていないため、ETHW フォークされたチェーン上で操作すると、代わりに ETH メイン ネットワーク上でメッセージ リプレイ攻撃を受けることになります。
UniswapV2 を例に挙げると、現在 UnswapV2 のプール コントラクトには許可関数があり、この関数には変数 PERMIT_TYPEHASH があり、変数 DOMAIN_SEPARATOR が含まれています。
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, UniswapV2: EXPIRED);
bytes32 digest = keccak256(
abi.encodePacked(
\x19\x01,
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, UniswapV2: INVALID_SIGNATURE);
_approve(owner, spender, value);
}
この変数は EIP712 で最初に定義され、chainId が含まれており、設計の開始時にマルチチェーン シナリオでのリプレイ防止が含まれていましたが、uniswapV2 プール コントラクトのロジックによれば、次のようになります。
constructor() public {
uint chainId;
assembly {
chainId := chainid
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256(EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)),
keccak256(bytes(name)),
keccak256(bytes(1)),
chainId,
address(this)
)
);
}
最初のレベルのタイトル
プロトコル設計開始時の注意点
最初のレベルのタイトル
副題
ユーザーへの影響
副題
取引所とカストディアンへの影響
要約する
要約する
マルチチェーンシナリオの発展に伴い、リプレイ攻撃は理論レベルから徐々に主流の攻撃手法となってきており、開発者はプロトコル設計を慎重に検討する必要があります。メッセージ署名メカニズムを設計する際には、署名内容としてchainIdなどの要素を可能な限り追加してください。また、ユーザー資産の損失を防ぐために、関連するベスト プラクティスに従ってください。