预防委托调用(DELEGATECALL)引起的合约漏洞

  • aisiji
  • 更新于 2022-03-03 12:04
  • 阅读 4218

本文通过编写有漏洞的合约,来了解如何攻击并理解如何预防漏洞的发生。

本文通过编写有漏洞的合约,来了解如何攻击并理解如何预防漏洞的发生。

0_CDZgVHvn9FGeuV7M 图片来源: Arnold Francisca

call不同,用DELEGATECALL进行函数调用时,其代码是在当前调用函数的环境里执行,因此,构建无漏洞自定义库并不像想象的那么简单。有时库代码本身可能是安全无漏洞的;然而当它应用到另一个合约的上下文中却有可能出现漏洞。我们来看一个复杂一点的例子:使用斐波那契数列。

备注:斐波那契数列是指从 0 1 开始,之后的数总是之前的和:0, 1, 1, 2, 3, 5, 8, 13 ...

下面是FibonacciLib.sol中的一个库,它可以生成斐波那契数列或者类似的数列。

// 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));
    }
}

-FbonacciBalance.sol-

利用漏洞攻击

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

你可能已经注意到了,在库和主调合约中都使用了状态变量start。在库合约中,start被用于指定斐波那契数列的起始数字并被设置为0,而在主调合约中它被设置为3。你可能也注意到了,FibonacciBalance合约中的fallback函数会把所有调用委托传递给库合约,这让库合约的setStart函数也可以被调用。回想一下,我们保留了合约的状态,所以实际上允许在本地FibonnacciBalance合约中修改start变量的状态。如果这样的话,start被修改为更大的值就可以提取更多的以太币,因为calculatedFibNumber依赖于start变量(如库合约所示)。事实上,在FibonacciBalance合约中,setStart函数不会(也不能)修改start变量。这个合约的潜在漏洞比仅仅修改start变量要糟糕的多,请继续往下看。

请注意,在第21行,withdraw函数已经执行了fibonacciLibrary.delegatecall(fibSig,withdrawalCounter)。调用了setFibonacci函数,修改了存储slot[1],当前是calculatedFibNumber(即,在执行后,calculatedFibNumber已经被修改了)。但是注意,FibonacciLib合约的start变量存储在slot[0],其实是当前合约的fibonacciLibrary地址。这意味着fibonacci函数将返回一个非预期的结果,因为它引用了start(即slot[0]),当前是fibonacciLibrary地址(它被解释为uint时,会非常大)。因此,withdraw函数会返回,因为合约余额不足uint(fibonacciLibrary) 因为calculatedFibNumber会返回的非常大的值。

更糟糕的是,FibonacciBalance合约让用户通过26行的fallback函数调用所有fibonacciLibrary函数,包括setStart函数,而这个函数允许任何人修改或者设置存储slot[0]。这时,slot[0]fibonacciLibrary地址。因此,攻击者可以创建一个恶意合约,修改这个地址为uint(这可以在Python中用int('<address>',16)轻松完成),然后调用setStart(<attack_contract_address_as_uint>)。将fibonacciLibrary地址改为攻击合约的地址。之后,任何时候,当用户调用withdraw或者fallback函数时,恶意合约就会执行,并盗取合约的全部余额。

下面是一个攻击合约的例子:

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
    }
 }

-Attack.sol-

如何避免

Solidity提供了一个library关键字,用于实现库合约。这就确保了库合约是无状态且不可自毁的。

一个真实的攻击实例:Parity Multisig Wallet (第二次攻击)

库合约:

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);
  }

  ...

}

-WalletLibrary.sol-

钱包合约:

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.sol-

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

点赞 1
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
aisiji
aisiji
江湖只有他的大名,没有他的介绍。