• 智能合约的常见漏洞


    目录:

    参考文献:
    [1]: [Principles of Security and Trust'17]A Survey of Attacks on Ethereum Smart Contracts (SoK)
    [2]: [CCS'16]Making Smart Contracts Smarter
    [3]: [ASE'18]ContractFuzzer: Fuzzing Smart Contracts for Vulnerability Detection

    1. 重入(Reentrancy) [1, 2, 3]

    • 基本概念

      • 智能合约中的 fallback (回退)函数
        一个智能合约中,可以有一个没有函数名,没有参数也没有返回值的函数,也就是 fallback 函数。一个没有定义 fallback 函数的合约,如果接收ether,会触发异常,并返还ether(solidity v0.4.0开始)。所以合约要接收ether,必须实现回退函数。在三种情况下,这个函数会被触发:
        • 如果调用这个合约时,没有匹配上任何一个函数。那么,就会调用默认的 fallback 函数。
        • 当合约收到ether时(没有任何其它数据),这个函数也会被执行。
          注意,执行 fallback 函数会消耗gas。
    • 场景/例子
      例子引自: (https://medium.com/@MyPaoG/explaining-the-dao-exploit-for-beginners-in-solidity-80ee84f0d470)

    /* 此合约用于1)记录用户余额,2)可以取款,3)可以存款。有reentrancy漏洞。*/
    
    contract Bank{
    
    /* 地址(唯一)和余额的映射 */
       mapping(address=>uint) userBalances;
    
    /* 返回用户余额 */
       function getUserBalance(address user) constant returns(uint) {
         return userBalances[user];
       }
    
    /* 给指定的用户增加余额 */
       function addToBalance() {
         userBalances[msg.sender] = userBalances[msg.sender] + msg.value;
       }
    
    /* 用户取款(这里假设取余额中全部的钱) */
       function withdrawBalance() {
         uint amountToWithdraw = userBalances[msg.sender];
         /* 把钱转给用户。如果交易失败,则throw。 */
         if (msg.sender.call.value(amountToWithdraw)() == false) {
             throw;
         }
         /* 如果交易成功,把用户的余额设置为0。 */
         userBalances[msg.sender] = 0;
       }
    }
    
    /* 这是一个攻击具有reentrancy漏洞的智能合约(Bank)的智能合约(BankAttacker)。在这个例子里,它实现了两次攻击。 */
    contract BankAttacker{
    
       bool is_attack;
       address bankAddress;
    
    /* 输入:1)_bankAddress:要攻击的智能合约(Bank)的地址,2)_is_attack:开启或关闭攻击。*/
       function  BankAttacker(address _bankAddress, bool _is_attack){
           bankAddress=_bankAddress;
           is_attack=_is_attack;
       }
    
    /* 这是一个fallback函数,用于调用withdrawnBalance函数(当开始攻击时,即is_attack为true) 。这个函数会被触发是因为有reentrancy漏洞的智能合约(Bank)中的withdrawBalance函数被执行。为了避免无限递归调用fallbacks,有必要设置有限的次数,例如这里设置2次。因为每次调用是需要gas的,如果gas用完了,攻击就失败了。 */
       function() {
           if(is_attack==true)
           {
               is_attack=false;
               if(bankAddress.call(bytes4(sha3("withdrawBalance()")))) {
                   throw;
               }
           }
       }
    
    /* 存款函数。主要功能是给智能合约Bank发送75wei,并且调用addToBalance。 */
       function  deposit(){
            if(bankAddress.call.value(2).gas(20764)(bytes4(sha3("addToBalance()")))
            ==false) {
                   throw;
               }
       }
    
    /* 这个函数会触发Bank中的withdrawBalance函数。*/
       function  withdraw(){
    		if(bankAddress.call(bytes4(sha3("withdrawBalance()")))==false ) {
                   throw;
               }
    
       }
    }
    

    攻击者利用BankAttack(vulnerable contract)与Bank进行交互,主要过程:

    1. 攻击者首先通过调用BankAttack中的 deposit 函数发送75wei到Bank,从而调用Bank中的 addToBalance 函数。
    2. 【第一次取款】攻击者通过调用BankAttack中 withdraw 进行取款(取75wei)。同时,触发了Bank中的 withdrawBalance
    3. Bank中的 withdrawBalance 发送75wei给BankAttack,从而触发了BankAttack的 fallback 函数,最后更新 userBalances 变量。
    4. 【第二次取款】BankAttack的fallback函数再次调用Bank中的 withdrawBalance 函数,相当于再次取款。注意,这个时候,相当于递归调用,因此第一次取款还未结束,因此,Bank中的变量 userBalances 的值还没有更新。所以,调用第二次取款时,Bank误以为BankAttack还存有75wei。因此,成功地再次执行了取款的操作。
      以下是流程图:
      Reentrancy Attack Process
    • 检测方法

      • 工具一: Oyente [2]
        主要思想: 利用条件路径。在每次执行CALL函数之前,先利用符号执行获取整个函数的条件路径。然后检查路径[unclear]
      • 工具二: ContractFuzzer [3]
        主要思想: 如下图所示,创建一个AttackerAgent去与目标contract交互。
        reentrancy-contractfuzzer
    • 修复方法

    • QA

      • 循环调用什么时候停止?
        当1) 执行最终out-of-gas, 2)达到了stack limit, 3)当攻击者所有的ether都被用完了。
      • 停止后整个程序产生了什么影响?
        最终,最后一个调用会失败(不影响区块链状态),因此有且仅有一个异常被抛出。之前的所有调用都被认为是合法的,因此,都成功执行完毕。
    • 其他

      • 相关漏洞:TheDao hack

    2. Call to the unknown [1]

    • 基本概念
      每个智能合约的函数通过函数名和参数类型来保证唯一性(Signature)。所以,本来一个合约时想执行某函数,由于代码写错了,没有匹配到其他的函数,所以就默认调用 fallback 函数。

    • 检测方法
      检测参数类型和函数名与调用函数是否一致。

    3. Gasless send [1, 3]

    • 基本概念
    1. 发送ether: send() 函数
      当使用send(相当于一个特殊的call())发送以太币到一个合约时,有可能会发生out-of-gas异常。当签名不匹配任何的函数时,将会触发回退函数。由于send()函数指定了一个空函数签名,所以当fallback函数存在时,它总是会调用它。但和一般的函数不同的是,执行send()所消耗的gas默认上线被限定在2,300(如果特别指定上限的话,可以大于2,300)。
    • 场景/例子

      • 例1
        gasless example
        合约C给合约D1和D2发送ether。会有以下三种可能的情况:
        1. n≠0, d=D1。 C发送失败,并抛出out-of-gas异常。因为2,300不足以执行D1的 fallback() 函数,即count++;
        2. n≠0, d=D2。C发送成功。
        3. n=0, d=D1/D2。对于编译器版本<0.4.0,两个都会失败,D1是因为2,300不足以执行 fallback,D2是因为 fallback 为空。对于编译器版本≥0.4.0,D1失败,D2成功,原因同1和2。
          总之,send 成功的两种可能:1)发送ether给一个合约,而这个合约的 fallback 花费小于可花费的gas。2)发送ether给一个用户。
      • 例2:King of the Ether Throne game
        还有一个例子就是叫“King of the Ether Throne”的游戏。这个游戏的玩法大致就是发送ether到一个叫KotET的智能合约中(如下图所示)。想成为king的玩家必须要支付一些ether给当前的king,加上少量的fee给KotET这个智能合约。
        king1
        假设有一个玩家想要成为king,那么他就会想KotET发送一定量(msg.value)的ether。然后就会调用KotETfallback 函数。fallback 函数会首先checkmsg.value是否大于之前的king设定的报价(LINE14)。如果小于,则说明竞价失败,则throw。反之,就会取得王座,成为新的king。
        这个contract看似没问题,实际上会有gasless send bug。当LINE17执行失败的时候(gas不够执行 fallback()),那么王座会被这个contract所持有。
        King2
        那么,现在假设,重写合约(如上图LINE6所示),用call替换send,然后去check它的返回值,如falsethrow。虽然这个版本看似比之前的版本要好,但是,这个合约还是有bug:假设现在有一个叫Mallory的attacker,它的 fallback 函数里面就是一个throw。它发送足够的ether给KotET,然后成为的新的king。这个时候,就再也没有人可以取代它的王位,因为每次给Mallory发送ether的时候,都必须要调用Malloryfallback 函数。因此,KotET的LINE6的条件会一直为true。因此,程序不会再执行下去。
    • 参考

    4. Exception disorder/Mishandled Exceptions [1, 2, 3]

    • 基本概念
    1. 智能合约的相互调用(call,delegatecall,callcode)
      在函数调用的过程中, Solidity 中的内置变量 msg 会随着调用的发起而改变,msg 保存了调用方的信息包括:调用发起的地址,交易金额,被调用函数字符序列等。
      三种调用方式的异同点

      • call: 最常用的调用方式,调用后内置变量 msg 的值会修改为调用者,执行环境为被调用者的运行环境(合约的 storage)。
      • delegatecall: 调用后内置变量 msg 的值不会修改为调用者,但执行环境为调用者的运行环境。
      • callcode: 调用后内置变量 msg 的值会修改为调用者,但执行环境为调用者的运行环境。
    2. solidity的异常处理
      三种抛异常的场景:

      • 执行到out-of-gas
      • call 栈溢出
      • 执行到throw语句
        如果在执行被调用的合约时有异常抛出,那么,被调用的合约会终止执行并且revert状态,并返回false。但是,当一个合约以不同的方式调用另外一个合约时,solidity没有一个一致的方法去处理异常。调用的合约可能无法获取被调用的合约中的异常信息。如下图所示,
        ExceptionDisorder
      • 情况一:Bob直接调用Aliceping
        ==>throws an exception==>执行结束==>transaction revert。所以,Bobx还是为0。
      • 情况二:Bob通过call调用Aliceping
        ==>call返回false==>执行继续。所以,Bobx为0。
        更一般的情况,假设有一串函数调用链(如,a()调用b(),b()调用c(),...),直到异常抛出。那么,异常处理如下:
      • 情况一: 所有函数调用都是直接调用,直到程序停止,所有的side effect都revert。所有由最初调用函数的用户提供的的gas都被消耗完。
      • 情况二: 调用链中至少有一个函数调用是通过call来实现的。那么,异常会进行传递(类似于溯源),被调用的合约的side effect都会revert。所有由最初调用函数的用户提供的的gas也都被消耗完。
        可见,处理异常的方式的不一致性会影响到合约的安全性。比如,如果仅仅根据没有异常抛出就认为转账是成功的,这是不安全的。有研究表明,~28%的合约没有去检查call/send调用。

    5. Type casts [1]

    • 基本概念
      solidity是强类型语言,所以会有类型检查,如变量赋值时,如把字符串赋值给整型变量。但是,有些情况即使类型不匹配,也不会进行类型检查,因此会导致此bug。

    • 场景/例子
      如下图所示,solidity编译器不会检查以下类型是否匹配:

      1. c是否是一个有效地址;
      2. Alice里是否真的有ping
        typecase-example
        所以,有时候,开发者以为编译器做了类型检查,但其实并没有。所以,在执行时,会出现以下情况:
      3. c不是一个地址,所以直接return。
      4. 正确调用,代码正确执行。
      5. c是一个正确的地址,但是,没有匹配任何Alice中的函数,所以调用alicefallback 函数。
        以上三种情况中任意一种发生,都不会抛出异常。所以,开发者不会察觉。

    6. Keeping secrets [1]

    • 基本概念
      许多应用都需要暂时合约的字段保密,即暂时不可见。比如,两个玩家对战,那么,下一步可能需要暂时对对手不可见。但是,尽管solidity可以申明某些变量为private,但是,这并无法保证它是真的不可见的。这个时候,可能就需要一些加密技术去解决这个问题。

    7. Ether lost in transfer [1]

    • 基本概念
      给一个地址发送ether,这个地址符合地址规范,但是是一个完全独立的空地址。所以,会导致ether丢失。

    8. Unpredictable state [1, 2]

    • 定义
      在[2]中,它也被称作"Transaction-Ordering Dependence(TOD)"。一个block包含一个transaction的集合,同属于一个block的transaction的执行顺序是不确定的(只有矿工可以确定)。因此,也就导致了block的状态是不确定的。假设block处于状态(σ),其中包含了两个transaction (T_1)(T_2)(T_1)(T_2)又同时调用了同一个合约。那么,在这个时候,用户是无法知道这个合约的状态的,因为这取决于(T_1)(T_2)的实际执行顺序。

    • 场景
      unpredictable-state-example

      • 场景一: Benign Scenario
        假设(T_o)(T_u)差不多时间发送信息到Puzzle。其中,(T_o)是来自合约的所有者,他想更新提出方案的奖励值。(T_o)是来自提出解决方案的用户,他想通过方案得到奖励。那么,在这个时候,(T_o)(T_u)的执行顺序会影响到提出方案的用户最终能获得多少奖励。
      • 场景二: Malicious Scenario
        注意,从(T_u)被广播到(T_u)被记录在block之间,有12s的时间间隔。也就是说,Puzzle合约的所有者可以一直保持监听网络,看是否有人提到解决方案到Puzzle。一旦有,他就发送一个transaction去更新奖励(比如设为一个很小的数)。在这种情况下,合约的所有者就很有可能(注意,并非一定)通过很小的花费就得到了解决方案。

    9. Generating randomness [1]

    • 基本概念
      有的开发者可能会利用下一个block的hash值或时间戳作为生成随机数的种子,但是在就像下面10. Timestasmp dependency中提到的,timestamp在一定程度上是可以"受控"于矿工。所以,这会导致这个bug。

    10. Timestasmp dependency [1, 2, 3]

    • 基本概念
      很多合约的执行逻辑是和当前block的时间戳有关的。而一个block的时间戳是由矿工(挖矿时的系统)决定的,并且允许有。但是,这里时间可以允许有900秒的偏移(The miner could cheat in the timestamp by a tolerance of 900 seconds

    • 场景/例子
      timestamp-example
      第五行到第七行依赖于当前block的时间戳。因此,矿工可以事先计算出对自己有利的时间戳,并且在挖矿时将时间设置成对自己有利的时间。

    • 检测方法

      • 工具一: Oyente [2]
        获取执行路径,判断路径中是否依赖时间戳。
      • 工具二: ContractFuzzer [3]
        是否同时满足两个条件: 1)依赖于时间戳,2)是否有转账。

    11. Dangerous `DelegateCall` [3]

    • 基本概念
      Exception disorder中提到了3种智能合约相互调用的方法。
      QA

    • 场景/例子
      delegate-example
      Wallet合约中,LINE6调用delegatecall并且传参msg.data。这使得attacker可以调用walletLibrary中的任意一个public function。因此,attacker可以调用LINE10的initWallet,以此成为Wallet这个合约的拥有者。然后他就可以从wallet发送ether到他自己的地址。

    • 参考

    12. Freezing ether [3]

    • 基本概念
      有些合约用于接受ether,并转账给其他地址。但是,这些合约本身并没有自己实现一个转账函数,而是通过delegatecall去调用一些其他合约中的转账函数去实现转账的功能。万一这些提供转账功能的合约执行suicideself-destruct操作的话,那么,通过delegatecall调用转账功能的合约就有可能发生ether被冻结的情况。

    • 检测方法

      • 工具一: ContractFuzzer [3]
        如果balance大于0且没有转账功能。
  • 相关阅读:
    NuGet 包版本引用 | Microsoft Docs
    victoriaMetrics库之布隆过滤器
    记一次victoriaMetrics代理性能优化问题
    victoriaMetrics之byteBuffer
    面试官:什么是MySQL 事务与 MVCC 原理?
    趣说 | 数据库和缓存如何保证一致性?
    VirtualBox复制虚拟机后丢失eth0网卡的解决办法
    VirtualBox安装centos6遇到的问题
    Lambda表达式获得泛型
    springbootxjar加密打包
  • 原文地址:https://www.cnblogs.com/XBWer/p/9697361.html
Copyright © 2020-2023  润新知