Solidity 智能合约漏洞检测

智能合约的漏洞

对无名函数的调用

在 Solidity 中,智能合约中可以定义无名函数,该函数被称为 fallback 函数。当某个合约(通过 send、call、delegatecall 或直接调用)接收到不存在的方法请求或者接收到转账时,fallback 函数会被触发。在某些情况下,fallback 函数可能被攻击者利用,最典型的案例是 TheDao 智能合约遭到的攻击。

send 请求的 Gas 限制

在 Solidity 中,通过 <address>.send(amount) 可以向某个地址发送以太币,但是 send 请求的 Gas 限制为 2300,只能用于执行有限的指令。由于 send 请求会触发合约的 fallback 函数,所以 fallback 函数中不能包含太复杂的指令(比如修改状态的指令),否则会出现 Gas 不足的异常。

异常的处理

在 Solidity 中,异常出现的情况包括:Gas 耗尽、调用栈过深,以及 throw 指令的调用。然而,如果方法调用的方式不同,异常处理的方式也可能不同:当出现异常时,如果调用链中的调用方式皆为直接调用,则程序停止继续执行并进行回滚;而如果调用链中的某些调用方式为 send、call 或 delegatecall 请求,则进行回滚直至遇到 send、call 或 delegatecall 请求,然后 send、call 或 delegatecall 请求返回 false,程序在该点继续执行。如果合约中没有检查 send、call 或 delegatecall 请求的返回值,则可能会出现危险。

类型转换

在 Solidity 中,通过 <interface>.someMethod(a, b, c) 可以调用某个合约的方法。编译器会将接口转换为被调用者的地址,但是编译器只会检查接口是否声明了 someMethod 方法,而不会检查被调用的合约是否真的是目标合约,以及 someMethod 的调用形式是否与声明的形式匹配。当 <interface>.someMethod(a, b, c) 调用指令出现时,可能会出现三种情况:

  • 如果 <interface> 为不是合约的地址,那么调用失败,没有副作用。

  • 如果 <interface> 是一个合约地址,并且合约中声明了与调用形式一致的方法,则该方法被调用。

  • 如果 <interface> 是一个合约地址,但是没有方法与调用形式一致,则 <interface> 的 fallback 函数将会被触发。

以上三种情况都不会抛出异常,所以调用者不会意识到错误的发生。

重入

在 Solidity 中,当一个合约调用了另一个合约的方法,当前合约的执行会被暂停,直到调用结束后再继续执行。也就是说,即使一个合约方法是非递归的,它也可能被再次进入。比方说,合约 A 在 someMethod 方法中通过 send 方法向合约 B 发送了一定数量的以太币,从而触发了合约 B 的 fallback 函数,而合约 B 的 fallback 函数同样可以调用合约 A 的 someMethod 方法,进而再次进入 someMethod 方法。如果合约 B 是攻击者,则它可通过重入 someMethod 方法来利用合约 A 在暂停时的即时状态。

私密信息的保存

在 Solidity 中,合约中的字段信息是公开的,即使为字段添加了 private 关键字也不能保证信息的安全,因为与字段修改有关的交易将会被公开记录到区块链上。如果没有采取加密措施,合约中的信息可能会被攻击者利用。

不可修改的漏洞

智能合约一旦被部署就不能再修改,所以合约代码中的漏洞可能会被攻击者利用。

以太币的流失

在转移以太币时,发送方需要指定接收方的地址,如果指定的地址为孤儿地址(也就是不属于任何用户或合约),则转移的以太币将会永久流失。

调用栈的大小限制

Solidity 对方法的调用栈有大小限制,如果调用栈过深,则会触发异常。

状态的不可预测性

智能合约的状态由其字段和账户决定,用户的交易能够改变智能合约的状态。当用户需要调用某个合约的方法时,用户并不能保证请求在执行时的状态与请求发起时的状态一致,这是因为交易请求的处理次序不是确定的,在用户发起的请求被处理之前可能有别的交易请求修改了合约的状态。

随机变量的生成

如果合约中涉及到随机变量,合约的设计者可能会用到伪随机数,一种常用的方法是选择将来会出现的某些区块的哈希值或时间戳作为伪随机数的种子。然而,由于矿工能够控制区块中的交易,所以矿工可能会恶意地影响随机变量的生成,从而干扰最终结果。

交易次序依赖

如果合约需要通过交易次序来做决策,最终结果可能会被矿工恶意干扰。

时间戳依赖

与上一点类似,如果合约需要通过时间戳来决定先后次序,最终结果可能会被矿工恶意干扰。

整数溢出问题

使用 Solidity 的整数类型时,可能会出现溢出问题(包括上溢出和下溢出)。比如合约中的某个整数字段表示某个账户的存款,如果该字段为 0,但是该账户的拥有者通过某种攻击方式使得该字段的数值减一,那么该存款数值将会变为 2^256 - 1。

漏洞检测工具 Oyente

正如前文所说,智能合约在部署之后就不能修改了,所以最好能在部署之前发现合约的漏洞。为此,可借助漏洞检测工具来检查智能合约,Oyente 就是这样一款工具。

Oyente 的架构图如下:

该系统的输入为合约的字节码(可在区块链上获得)以及当前以太坊区块链的全局状态(让分析更加精确)。

  1. CFG Builder 模块将会生成 CFG(控制流图,control flow graph)。CFG 的节点是没有跳转的指令执行序列,而 CFG 的边是从源节点到目标节点的跳转。

  2. Explorer 模块会在 CFG 中进行模拟的执行,记录相关的信息,并借助 Z3 找出永远不会被执行的路径。

  3. Core Analysis 模块将会分析合约中可能会出现的漏洞并进行标记。包括:

    • 未处理异常检测:如果被调用者抛出异常,那么被调用者的操作符栈中将会被压入 0。根据这一特性,可检查合约是否在每次调用之后都执行了 ISZERO 指令(该指令检查栈顶元素是否为 0)。如果不是,则说明有的异常可能会被忽略。

    • 重入漏洞检测:每次遇到 CALL 指令,都在 CFG 上查找在该 CALL 指令之前执行的条件判断指令。接着,修改合约中的变量,检查该条件判断是否仍然满足。如果仍然满足,则说明该 CALL 指令所在的方法在重入的情况下仍然能再次执行,具有被攻击的危险。

    • 交易次序依赖漏洞检查:Explorer 返回的信息包括以太币的流动。如果交易次序的修改导致以太币流动的路径不同,则表示该合约对交易次序有依赖。

    • 时间戳依赖漏洞检查:用特殊的变量表示时间戳。每次进行条件判断时,检查表示时间戳的变量是否有被引用,如果有则表示该合约对时间戳有依赖。

  4. Validation 模块将会借助 Z3 来检查 Core Analysis 模块的判断是否正确。

  5. Visualizer 模块将会展示检测的结果。

对 Oyente 的改进

Oyente 只能检测智能合约的一部分漏洞,论文 Making Smart Contracts Even Smarter 中提出了额外五种漏洞检测方法,我个人感觉比较靠谱的两个方法是:

  • 地址检查:根据以太坊标准来检查合约中的所有地址是否合法,如果存在不合法的地址则该合约有丢失以太币的可能。

  • 检测随机变量的漏洞:收集一些比较常见但是较为危险的随机数生成方法,通过检查指令序列的方法来探测合约中是否使用了这些方法来生成随机数,如果是,则表明该合约存在漏洞。

参考

A Survey of Attacks on Ethereum smart contracts

Nine Pitfalls of Ethereum Smart Contracts to Be Avoided

Making Smart Contracts Smarter

Updated: