智能合约常见攻击方式

人走茶凉 提交于 2020-02-06 12:59:40

准备

普通攻击

fallback回退函数

合约可以有一个未命名函数,该函数不能有参数,也不能有返回值。fallback函数在以下情况会被调用:

  • 一个调用中,没有其他函数与给定的函数标识符匹配(或没有提供调用数据)。由于Solidity中,Solidity提供了编译期检查,所以我们不能直接通过Solidity调用一个不存在的函数。但我们可以使用Solidity的提供的底层函数address.call来模拟这一行为,进行函数调用。
  • 合约收到以太币,没有任何数据(为接受以太币,fallback函数必须标记为payablefunction() payable public{},否则合约无法接收,如果通过转账函数transfer发送到没有定义payable的合约,会抛出错误,导致后面的代码无法执行!但如果有合约通过自毁selfdestruct(address)的方式发送,即使没有定义为payable都得收下!)。通过MetaMask的Send向合约地址转以太币触发(ethernaut环境下可通过集成函数contract.sendTransaction({value: 1})转以太币;通过address.call.gas(1000000).value(msg.value)();在合约中转以太币)。

构造函数

构造函数无法显式调用,但如果构造函数和contract名字不一致,就能被直接调用,solidity 0.4.22引入了关键词constructor来指定构造函数。

随机数预测

  • 使用区块变量block.coinbase/difficulty/gaslimit/number/timestamp,或基于过往块的哈希值block.blockhash(block.number – 1),由于区块变量在同一区块是共用的,通过攻击合约调用目标合约,即可实现预测
12345678910111213
contract CoinFlip {
  function flip(bool _guess) public returns (bool);
}
contract Attack {
  address _addr;
  constructor(address param) { _addr = param;}
  function guess() public{
    CoinFlip cf = CoinFlip(_addr); //new instance with address
    uint256 blockValue = uint256(block.blockhash(block.number-1)); //generate the same random value
    bool side = uint256(uint256(blockValue) / FACTOR) == 1;
    return cf.flip(side); //invoke method with instance
  }
}
  • 使用未来区块的区块哈希(使用上一次调用区块高度计算哈希值),由于EVM 能存储的区块哈希为最近的256条,超过的话置为 0。因此,如果第二次调用时,与第一次下注时的区块高度差超过了 256,那么此时的产生的区块哈希为0,此时伪随机数就变成可猜测的了
  • 使用私有的种子变量,将变量标记为私有只会阻止其他合约访问它,但是可以以链下的方式去获取链上的存储信息。如使用客户端的web3 API方法web3.eth.getStorageAt(addr, argument_index,callback),可以检索合约的存储
123
web3Provider = new Web3.providers.HttpProvider('https://ropsten.infura.io/'); //默认的web3必须带上callback回调函数
web3 = new Web3(web3Provider);
web3.eth.getStorageAt("0xad836dc9bc4fa0af947a27128edfa14459d77f19", 1, function(x, y) {alert(web3.toAscii(y))}); //以alert的方式将该地址的第2个值转化ascii显示

解决该问题可选的方案有RANDAOOraclize等,以去中心化的方式或是与外界互联网交互的方式得到安全的随机数。

区分tx.originmsg.sender

msg.sender是函数的直接调用方,在用户手动调用该函数时是发起交易的账户地址,但也可以是调用该函数的一个智能合约的地址。而tx.origin则必然是这个交易的原始发起方,无论中间有多少次合约内/跨合约函数调用,而且一定是账户地址而不是合约地址。所以如果存在用户通过合约A调用合约B,那么对应合约B而言,msg.sender 是合约 A 地址,但tx.origin 是用户的账户地址。

如果目标合约使用tx.origin作为校验的依据,攻击者以钓鱼等方式,欺骗目标合约拥有者向攻击协议发送以太币,调用fallback函数,然后在fallback函数调用目标合约,由于tx.origin会是交易原始发起方,也就是目标合约拥有者,满足校验条件,从而实现攻击。

解决方案:通过require(tx.origin == msg.sender)限制外部合约对内部合约的调用。

整数溢出

uint默认为256位无符整型,可表示范围[0, 2**256-1]。如果对0减1,则由256位的0,变成256位1,整数下溢,变成一个最大的整数。同理,如果对256位1加1,则变成256位0,变成最小的整数。同理:uint8,只能存储在范围[0,255]的数字。

解决方案:

  • 在每一次数学运算时进行判断,如a=a+b;,就可以写成if(a+b>a) a=a+b;
  • 使用 OpenZeppelin 团队开发的SafeMath库,如果整数溢出漏洞发生时,函数将进行回退操作,如加法操作写为:a=a.add(b);

call/delegatecall调用

使用call函数来进行合约交互,对目标合约发送数据。delegatecall跟call主要的不同:通过delegatecall调用,仅使用目标地址的代码,其他信息则使用当前合约(如:msg.sender等)。delegatecall是危险函数,他可以在被调用合约完全操作原始合约的状态,谨慎使用!

通过call/delegatecall调用函数,传入的第一个参数是四个字节时,会把这四个字节当作函数的id来寻找被调用函数,而一个函数id的生成规则是其函数签名的sha3的前4个bytes。因此通过:web3.sha3("pwn()").slice(0,10)=0xdd365b8b,加上0x,总共取前10个字符。

重入攻击(Re-entrancy)

被攻击函数withdraw()在发送以太币msg.sender.call.value(_amount)() 之后才更新余额balances[msg.sender] -= _amount; ,因此通过攻击合约调用withdraw时,攻击合约在fallback函数中接收以太币时再次调用withdraw,则可以在更新余额之前无限递归调用withdraw

1234567891011121314151617181920
contract Reentrance {
  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      if(msg.sender.call.value(_amount)()) {
        _amount;
      }
      balances[msg.sender] -= _amount; // update balance after send
    }
  }
}
contract Attack {
  function() public payable {
    uint weHave = c.balanceOf(this);
    if (weHave > c.balance) {
      if (c.balance != 0) c.withdraw(c.balance);
      return;
    }
    c.withdraw(weHave);
  }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!