Article Summary
GPT 4

原理

EVM的整数有int和uint两种。

Solidity 语言中,变量支持的整数类型步长是以8递增的,从 uint8uint256, uint 默认是 uint256,以 uin8 为例

我们知道 uint8 是8位,我们最多可以 2**8-1,也就是 255,若是256则会造成溢出,这是上溢

下溢也是一样的, uint(0)-1 就是255

例子

Capture The Ether 的 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);
    }
}

分析:
首先规定了合约中有1 ether,一个代币需要支付1 ether. 而isComplete()要求余额小于1 ether。

buy(uint256 numTokens)函数则是msg.value == numTokens * PRICE_PER_TOKEN。

再EVM里1ether=10* *8 wei。然而EVM虚拟机最大只有256位即 2**256-1.因此只要我们的numTokens是一个很大的值,就会溢出。即我们花费不足1etner就买到了大量的代币,将一些代币卖出即可完成题目要求。

因此,我们可以购买2**256//10* *18+1 个代币就可以完成题目要求。

整数下溢

contract Bank {
    mapping(address => uint256) public balanceOf;
    ...
    function withdraw(uint256 amount) public {
        require(balanceOf[msg.sender] - amount >= 0);
        balanceOf[msg.sender] -= amount;
        msg.sender.send.value(amount)();
    }
}

分析:

由于uint为无符号类型,因此

balanceOf[msg.sender] - amount >= 0看似没有任何问题,但是由于都是无符号类型,因此结果永远大于等于0的。所以我们可以任意取款。应改为balanceOf[msg.sender] >=amount

还有一种情况,与重入攻击有关:将1的物品卖出两次导致下溢为巨大的正数。

题目

【ciscn2019】 Daysbank
pragma solidity ^0.4.24;

contract DaysBank {
    mapping(address => uint) public balanceOf;
    mapping(address => uint) public gift;
    address owner;
        
    constructor()public{
        owner = msg.sender;
    }
    
    event SendFlag(uint256 flagnum, string b64email);
    function payforflag(string b64email) public {
        require(balanceOf[msg.sender] >= 10000);
        emit SendFlag(1,b64email);
    }

    function getgift() public{
        require(gift[msg.sender]==0);
        balanceOf[msg.sender]+=1;
        gift[msg.sender]=1;
    }
    
    function transfer(address towhere, uint howmuch) public {
        require(howmuch>1);
        require(balanceOf[msg.sender]>1);
        require(balanceOf[msg.sender]>=howmuch);
        balanceOf[msg.sender]-=howmuch;
        balanceOf[towhere]+=howmuch;
    }
    
    function profit() public{
        require(balanceOf[msg.sender]==1);
        require(gift[msg.sender]==1);
        balanceOf[msg.sender]+=1;
        gift[msg.sender]=2;
    }
    
    function transfer2(address towhere, uint howmuch) public {
        require(howmuch>2);
        require(balanceOf[msg.sender]>2);
        require(balanceOf[msg.sender]-howmuch>0);
        balanceOf[msg.sender]-=howmuch;
        balanceOf[towhere]+=howmuch;
    }
}

分析:

找到flag的函数payforflag(),观察得到,要想得到flag需要balanceof大于10w。

而transfer2()函数中balanceOf[msg.sender]-howmuch>0存在整数下溢的漏洞,因而可以利用。但是需要满足balanceOf[msg.sender]>2的要求。

我们可以通过getgift()来获得一个代币,然后可以满足profit()的要求,从而获得两个代币 ,此时balanceOf为2,gift为1。如果要达到balanceOf[msg.sender]>2,那么需要利用transfer()函数才能达到要求,transfer没有下溢的漏洞。

攻击过程:
先利用账号a,通过getgift(),然后利用profit()函数,此时余额为2,gift为1.

然后再用账号b,重复该操作。

再将账号a利用transfer函数转给账号b两个代币。

然后再利用账号b调用transfer2转给账号a一个非常大的金额,达到溢出的效果,此时两个地址都可以执行flag函数。