quentangle

Posted on Mar 02, 2022Read on Mirror.xyz

防范智能合约攻击 — “DELEGATECALL”

通过一段有漏洞的智能合约代码,看看攻击是如何发生的,以及修复它的预防技术

翻译:团长(https://twitter.com/quentangle_

photo credit: Arnold Francisca

由于DELEGATECALL的上下文保护性质,建立无漏洞的自定义库并不像人们想象的那样容易。库中的代码本身可以是安全和无漏洞的;但是,当在另一个应用程序的上下文中运行时,就会出现新的漏洞。让我们看一下斐波那契数这个相当复杂的例子。

让我们考虑一下FibonacciLib.sol中的库,它可以生成Fibonacci序列和类似形式的序列。

// library contract - calculates Fibonacci-like numbers
contract FibonacciLib {
    // initializing the standard Fibonacci sequence
    uint public start;
    uint public calculatedFibNumber;

    // modify the zeroth number in the sequence
    function setStart(uint _start) public {
        start = _start;
    }

    function setFibonacci(uint n) public {
        calculatedFibNumber = fibonacci(n);
    }

    function fibonacci(uint n) internal returns (uint) {
        if (n == 0) return start;
        else if (n == 1) return start + 1;
        else return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

FibonacciLib.sol

这个库提供了一个函数,可以生成序列中的第n个斐波那契数。用户可以改变序列的起始数(start),并计算出这个新序列中的第n个斐波那契数。现在由一个使用该库的合约,如FibonacciBalance.sol所示。

contract FibonacciBalance {

    address public fibonacciLibrary;
    // the current Fibonacci number to withdraw
    uint public calculatedFibNumber;
    // the starting Fibonacci sequence number
    uint public start = 3;
    uint public withdrawalCounter;
    // the Fibonancci function selector
    bytes4 constant fibSig = bytes4(sha3("setFibonacci(uint256)"));

    // constructor - loads the contract with ether
    constructor(address _fibonacciLibrary) external payable {
        fibonacciLibrary = _fibonacciLibrary;
    }

    function withdraw() {
        withdrawalCounter += 1;
        // calculate the Fibonacci number for the current withdrawal user-
        // this sets calculatedFibNumber
        require(fibonacciLibrary.delegatecall(fibSig, withdrawalCounter));
        msg.sender.transfer(calculatedFibNumber * 1 ether);
    }

    // allow users to call Fibonacci library functions
    function() public {
        require(fibonacciLibrary.delegatecall(msg.data));
    }
}

FibonacciBalance.sol

漏洞利用

这个合约允许参与者从合约中提取以太,以太的数量等于参与者提取顺序所对应的斐波那契数;也就是说,第一个参与者得到1个以太,第二个也得到1个,第三个得到2个,第四个得到3个,第五个得到5个,以此类推,直到合约的余额小于被提取的斐波那契数。

你可能已经注意到,状态变量start在库和主调合约中都被使用。在库合约中,start被用来指定斐波那契数列的开始,并被设置为0,而在主调合约中被设置为3。你还会注意到,FibonacciBalance合约中的回退函数fallback允许将所有调用传递给库合约,这使得库合约的setStart函数可以被调用。回顾我们保留了合约的状态,似乎这个函数将允许你改变本地FibonnacciBalance合约中的start变量的状态。如果是这样,这可以让人提取更多的以太,因为calculatedFibNumber取决于起始变量(如在库合约中看到的)。事实上,setStart函数并没有(也不能)修改FibonacciBalance合约中的起始变量。这个合约内部的潜在漏洞比仅仅修改起始变量要严重得多。

现在注意到,在 withdraw 的第 21 行,我们执行了 fibonacciLibrary.delegatecall(fibSig,withdrawingCounter)。这调用了setFibonacci函数,它修改了slot[1],在我们当前的上下文中是calculatedFibNumber。这当然是可以预期的(即执行后,calculatedFibNumber被修改)。然而,记得FibonacciLib合约中的起始变量位于slot[0]中,即当前合约中的fibonacciLibrary地址。这意味着函数 fibonacci 会给出一个意外的结果,因为它引用了 start(slot[0]),在当前的调用环境中,它是 fibonacciLibrary 的地址(当解释为一个 uint 时,它通常会相当大)。因此,撤回的函数很可能会被退回,因为它不会包含uint(fibonacciLibrary)数量的以太,这就是calculatedFibNumber将返回的东西。

译注:delegateCall相关内容可参考

https://docs.soliditylang.org/en/v0.4.21/introduction-to-smart-contracts.html#delegatecall-callcode-and-libraries

https://solidity-by-example.org/delegatecall/

更糟糕的是,FibonacciBalance合约让用户通过第26行的回退函数调用所有的fibonacciLibrary函数。正如我们所讨论的,这包括setStart函数。正如我们所讨论的,这个函数让任何人改变或设置slot[0]。在这里,slot[0]fibonacciLibrary 的地址。因此,攻击者可以创建一个恶意合约,将地址转换成一个uint(这可以在Python中使用int('<address>',16)轻松完成),然后调用setStart(<attack_contract_address_as_uint>)。它将把 fibonacciLibrary 改为攻击合约的地址。然后,每当用户调用 withdrawfallback 函数时,恶意合约就会运行,它将窃取合约的全部余额。

这类攻击合约的一个例子是:

contract Attack {
    uint storageSlot0; // corresponds to fibonacciLibrary
    uint storageSlot1; // corresponds to calculatedFibNumber

    // fallback - this will run if a specified function is not found
    function() public {
        storageSlot1 = 0; // we set calculatedFibNumber to 0, so if withdraw
        // is called we don't send out any ether
        <attacker_address>.transfer(this.balance); // we take all the ether
    }
 }

预防措施

Solidity 为实现库合约提供了library关键字。这确保了库合同是无状态的和非自毁的。

真实世界的黑客实例: Parity Multisig Wallet (2nd Hack)

库合约:

contract WalletLibrary is WalletEvents {

  ...

  // throw unless the contract is not yet initialized.
  modifier only_uninitialized { if (m_numOwners > 0) throw; _; }

  // constructor - just pass on the owner array to multiowned and
  // the limit to daylimit
  function initWallet(address[] _owners, uint _required, uint _daylimit)
      only_uninitialized {
    initDaylimit(_daylimit);
    initMultiowned(_owners, _required);
  }

  // kills the contract sending everything to `_to`.
  function kill(address _to) onlymanyowners(sha3(msg.data)) external {
    suicide(_to);
  }

  ...

}

钱包合约:

contract Wallet is WalletEvents {

  ...

  // METHODS

  // gets called when no other function matches
  function() payable {
    // just being sent some cash?
    if (msg.value > 0)
      Deposit(msg.sender, msg.value);
    else if (msg.data.length > 0)
      _walletLibrary.delegatecall(msg.data);
  }

  ...

  // FIELDS
  address constant _walletLibrary =
    0xcafecafecafecafecafecafecafecafecafecafe;
}

请注意,Wallet合约通过一个委托调用将所有调用传递给WalletLibrary合约。这段代码中的常数_walletLibrary地址作为实际部署的WalletLibrary合约的占位符(它在0x863DF6BFa4469f3ead0bE8f9F2AAE51c91A907b4)。


原文链接:Preventing Smart Contract Attacks on Ethereum — “DELEGATECALL”