Solidity安全贴士(三)

一些攻击手段

1.竞态

  • 调用外部合约的主要危险之一是它们可以接管控制流,并对调用函数意料之外的数据进行更改。 这类bug有多种形式,导致DAO崩溃的两个主要错误都是这种错误。
(1)可重入:
// INSECURE
mapping (address => uint) private userBalances;

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call withdrawBalance again
    userBalances[msg.sender] = 0;
}

当调msg.sender.call.value()()时,并没有将userBalances[msg.sender] 清零,于是在这之前可以成功递归调用很多次withdrawBalance()函数。
  • 其可以在第一次调用这个函数完成之前被多次重复调用。对这个函数不断的调用可能会造成极大的破坏。
  • 使用msg.sender.call.value()())传递给fallback函数可用的气是当前剩余的所有气,在这里,假如从你账户执行提现操作的恶意合约的fallback函数内递归调用你的withdrawBalance()便可以从你的账户转走更多的币。
  • 最好的方法是 使用 send() 而不是call.value()()。这将避免多余的代码被执行。
跨函数竞态:
// INSECURE
mapping (address => uint) private userBalances;

function transfer(address to, uint amount) {
    if (userBalances[msg.sender] >= amount) {
       userBalances[to] += amount;
       userBalances[msg.sender] -= amount;
    }
}

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    if (!(msg.sender.call.value(amountToWithdraw)())) { throw; } // At this point, the caller's code is executed, and can call transfer()
    userBalances[msg.sender] = 0;
}

攻击者在他们外部调用withdrawBalance函数时调用transfer(),如果这个时候withdrawBalance还没有执行到userBalances[msg.sender] = 0;这里,那么他们的余额就没有被清零,那么他们就能够调用transfer()转走代币尽管他们其实已经收到了代币。这个弱点也可以被用到对DAO的攻击。

在执行转账操作之前先清零,可有效解决。

解决竞态中的陷阱

由于竞态既可以发生在跨函数调用,也可以发生在跨合约调用,任何只是避免重入的解决办法都是不够的。

先完成所有内部的工作然后再执行外部调用,这样可以避免竞态发生。避免过早调用外部函数和避免调用那些也调用了外部函数的外部函数。

标记不受信任的函数。

(2)交易顺序依赖(TOD) / 前面的先运行

区块链本身运作原理导致的竞态:(同一个block内的)交易顺序很容易受到操纵。
交易在短暂的时间内会先存放到mempool中,所以在矿工将其打包进block之前可以知道会发生什么动作。这对于一个去中心化的市场来说是麻烦的,因为可以查看到代币的交易信息,并且可以在它被打包进block之前改变交易顺序。在市场上,最好实施批量拍卖(这也可以防止高频交易问题)。

(3)整数上溢和下溢
mapping (address => uint256) public balanceOf;

// INSECURE
function transfer(address _to, uint256 _value) {
    /* Check if sender has balance */
    if (balanceOf[msg.sender] < _value)
        throw;
    /* Add and subtract new balances */
    balanceOf[msg.sender] -= _value;
    balanceOf[_to] += _value;
}

// SECURE
function transfer(address _to, uint256 _value) {
    /* Check if sender has balance and for overflows */
    if (balanceOf[msg.sender] < _value || balanceOf[_to] + _value < balanceOf[_to])
        throw;

    /* Add and subtract new balances */
    balanceOf[msg.sender] -= _value;
    balanceOf[_to] += _value;
}

如果余额达到uint的最大值(2^256),便又会变为0。如果任何用户都有权利更改uint的值,那么它将容易受到攻击。如果只有管理员能够改变它的值,那么它可能是安全的。

对于下溢同样的道理。如果一个uint改变后小于0,那么将会导致它下溢并且被设置成为最大值(2^256)。

对于较小数字的类型比如uint8、uint16、uint24等也要小心:他们更容易达到最大值。

2.Gas的攻击

通过(Unexpected) Throw发动DoS
// INSECURE
contract Auction {
    address currentLeader;
    uint highestBid;

    function bid() payable {
        if (msg.value <= highestBid) { throw; }

        if (!currentLeader.send(highestBid)) { throw; } // Refund the old leader, and throw if it fails

        currentLeader = msg.sender;
        highestBid = msg.value;
    }
}

当有更高竞价时,它将试图退款给曾经最高竞价人,如果退款失败则会抛出异常。这意味着,恶意投标人可以成为当前最高竞价人,同时确保其地址的任何退款始终失败。这样就可以阻止任何人调用“bid()”函数,使自己永远保持领先。

合约可能通过数组迭代来向用户支付(例如,众筹合约中的支持者)。 通常要确保每次付款都成功,如果没成功,应该抛出异常。 问题是,如果其中一个支付失败,您将恢复整个支付系统,这意味着该循环将永远不会完成。 因为一个地址没有转账成功导致其他人都没得到报酬。

解决办法: 优先使用pull 而不是push。

通过区块Gas Limit发动DoS

一次性向所有人转账,很可能会导致达到以太坊区块gas limit的上限。以太坊规定了每一个区块所能花费的gas limit,如果超过交易便会失败。

如果攻击者增加一部分收款名单,并设置每一个收款地址都接收少量的退款。这样一来,更多的gas将会被花费从而导致达到区块gas limit的上限,整个转账的操作也会以失败告终。

比如需要通过遍历一个变长数组来进行转账,最好估计完成它们大概需要多少个区块以及多少笔交易。还必须能够追踪得到当前进行到哪里以便操作失败时从那里开始恢复。

猜你喜欢

转载自blog.csdn.net/qq_26769677/article/details/81517570