前言

本次给大家带来一个“智能合约审计”系列的技术分享专题,希望对大家有所帮助,也欢迎各位一起探讨与研究。

基础内容

合约

合约,常被称为“契约”,其为一种“合意”,依此合意,一人或数人对于其他一人或数人负担给付、作为或不作为的债务。从本质上来说,合约是双方当事人的“合意”,是双方当事人以发生、变更、担保或消灭某种法律关系为目的的协议。新婚燕尔,一纸婚约是为合约;你借我贷,劳动雇佣是为合约....合约无处不在,协议遍布丛生。

智能合约

“智能合约(Smart Contract)”的概念由计算机科学家、加密大师尼克·萨博(Nick Szabo)在1993年提出来,1994年他写成《智能合约》论文,那是智能合约的开山之作。
作为一位因为比特币打下基础而受到广泛赞誉的密码学家,尼克·萨博为智能合约下的定义如下:“一个智能合约是一套数字形式定义的承诺(Promises),包括合约参与方可以在上面执行这些承诺的协议。”
(1)数字形式
数字形式意味着合约需要被写入计算机可执行的代码中,只要参与者达成协定,智能合约建立的权利和义务就由一台计算机或计算机网络执行。
a.达成协定。智能合约的参与方在什么时候达成协定,取决于特定的智能合约实施。一般而言,当参与方通过在合约宿主平台上安装合约,致力于合约的执行时,合约就被发现了。
b.合约执行。“执行”的真正意思依赖于实施。一般而言,执行意味着通过技术手段积极实施。
c.计算机可读的代码。合约需要的特定“数字形式”,非常依赖于参与方同意使用的协议。
(2)协议
协议是技术实现(technical implementation),在这个基础上,合约承诺被实现,或者合约承诺实现被记录下来。选择哪个协议取决于许多因素,最重要的因素是————在合约履行期间被交易资产的本质。
以销售合约为例,假设参与方同意货款以比特币支付,选择的协议很明显将会是比特币协议,在此协议上,智能合约被实施。因此,合约必须要用到的“数字形式”就是比特币脚本语言。比特币脚本语言是一种“非图灵完备”的、命令式的、基于栈的编程语言。
萨博认为,智能合约的基本理念是,许多合约条款能够嵌入硬件和软件中。嵌入式合约最初的应用实例是自动售货机、销售点终端、大公司间的电子数据交换和银行间用于转移和清算的支付网络SWIFT、ACH、FedWire。另一个嵌入式合约的例子是数字内容消费,如音乐、电影和电子书等领域的数字版权管理机制。

智能合约的基本架构

智能合约使用solidity编程语言编写,Solidity的语法类似于JavaScript,整体比较好上手,一般一个智能合约基本架构如下:

数据类型&&修饰器&&函数

在对使用了solidity编程语言编写的智能合约审计之前,我们需要先认识一些solidity中的基本知识:

数据类型:

Solidity中主要数据类型有以下几种:
(1)布尔数据类型
布尔数据类型使用bool来表示,可能的取值一共有两种:true、false。
支持的运算符有:!(逻辑非)、&&(逻辑与)、||(逻辑或)、==(等于)、!=(不等于)

(2)整型
整型使用int/uint来表示(有符号和无符号整型)。变量支持的步长以8递增,支持从uint8到uint256,以及int到int256。我们平时看到的uint和int默认代表为uint256和int256。
支持的运算符有:
a.比较:<=、<、==、!=、>=、>,返回值为bool
b.位运算:&、|、^、~
c.数学运算:+、-、*、/、%、**

(3)地址
以太坊地址的长度大小为20个字节,160位,所以可以用一个uint160编码。地址是所有合约的基础,所有的合约都会继承地址对象,也可以随时将一个地址串,得到对应的代码进行调用。当然地址代表一个普通账户时,就没有这么多丰富的功能啦。
支持的运算符: <=、<、==、!=、>=、>
eg: 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF

(4)数组
数组可以分为两大类:定长字节数组、动态大小的字节数组
定长字节数组:
byte1、byte2、byte3、byte4....,允许以步长1递增。byte默认表示byte1。
支持的运算符:
a.比较:<=、<、==、!=、>=、>,返回值为bool类型。
b.位运算符:&、|、^、~
支持序号的访问,与大多数语言一样,取值范围为【0,n),其中n表示长度。
成员变量.length表示这个字节数组的长度(只读)。

动态大小的字节数组:
string:是一个动态尺寸的UTF-8编码字符串,它其实是一个特殊的可变字节数组,string是引用类型,而非值类型。
bytes:动态字节数组,引用类型。
原则:

  • bytes用来存储任意长度的字节数据,string用来存储任意长度的UTF-8编码的字符串数据。
  • 如果长度可以确定,尽量使用定长的如byte1到byte32中的一个,因为这样更省空间。

(5)字符串
字符串使用string来进行描述,字符串字面量是指由单引号,或双引号引起来的字符串。字符串并不像C语言,包含结束符,例如foo这个字符串大小仅为三个字节。

(6)映射
映射是一种键值对的映射关系存储结构。定义方式为mapping(_keyType=>_KeyValue)。键的类型允许除映射外的所有类型,如数组,合约,枚举,结构体。值得类型无限制。
映射可以被视作一个哈希表,其中所有可能的键已被虚拟化的创建,被映射到一个默认值(二进制表示的0)。但在映射表中,我们并不存储键的数据,仅仅存储它的keccak256哈希值,用来查找值时使用。

对于数据类型我们这里就仅仅列举这些我们经常看到的,经常用到的数据类型。

修饰器

修改器(Modifiers)可以用来轻易的改变一个函数的行为。比如用于在函数执行前检查某种前置条件。修改器是一种合约属性,可被继承,同时还可被派生的合约重写(override)。下面我们来看一段示例代码:

在上图中的第14~18行定义了一个修饰器,该修饰器限定要求msg.sender必须为合约的owner,否则就抛出异常,该修饰器用到智能合约mortal的close函数中则在函数调用之前修饰器会首先对调用者的身份进行一个检查,判断是否为owner,如果不是,则抛出异常,如果是,则正常执行。
在智能合约当中,修饰器经常可见,而且修饰器的使用一方面简化的代码,另外一方面对于合约中权限的控制起到了清楚的说明。

函数

在智能合约当中,函数为某种操作的执行主体,一个函数的基本结构如下:

实例如下:

审计工具

“工欲上其事必先利器”!

etherscan的使用

我们要进行智能合约的审计,那么我们就必须要有一个审计的目标,然而我们一般获取到的审计目标都是一个地址,那么我们如何通过地址来获取到该地址对应的智能合约的代码呢?这就要使用到以太坊浏览器——Etherscan了,下面我进行简单的介绍:
1、Eterscan界面介绍

2、根据地址查询合约

3、合约页面介绍

注:如果你获得类似于https://etherscan.io/address/0xB8c77482e45F1F44dE1745F52C74426C631bDD52#code
这样的信息,你可以直接在浏览器当中打开即可看到对应的智能合约的源码信息。

Remix的基本使用

Remix是个用于调试开发智能合约的IDE工具,它有在线版与离线版两种,离线版需要你自我搭建环境,自我安装,这里我们直接使用在线版的Remix为大家介绍Remix的使用方法。
1.Remix的访问
在浏览器中输入以下地址可以直接开启Remix
https://remix.ethereum.org/#optimize=false&version=soljson-v0.4.24+commit.e67f0147.js
2.界面介绍

3.简易操作
(1)新建一个项目

(2)编写智能合约内容

(3)编译

注:这里你也可以选择“Auto compile”自动编译,这里你会看到有黄色部分的提示,这里表示有警告,这些警告一般都是与编程规范出现了冲突或者编程者省略了一些无关紧要的内容,不是错误,当变为红色的时候就是错误了,就一定要改!
(4)部署

(5)执行函数

之后你会在执行结果栏中看到该参数传入之后执行的最终结果是什么,这里就不再多做解释了!

Sublime编辑器

Sublime是一框强大的编辑器,笔者这里推荐它的原因是它可以支持solidity代码的高亮显示,这对我们做代码审计来说十分非常非常有帮助的,至于如何配置高亮代码显示,大家可以参考下面的文章,这里就不再多说了:
https://www.jianshu.com/p/6578897771f5

整型溢出

整型溢出原理

计算机中整数变量有上下界,如果在算术运算中出现越界,即超出整数类型的最大表示范围,数字便会如表盘上的时针从12到1一般,由一个极大值变为一个极小值或直接归零。此类越界的情形在传统的软件程序中很常见,但是否存在安全隐患取决于程序上下文,部分溢出是良性的(如tcp序号等),甚至是故意引入的(例如用作hash运算等)。
以太坊虚拟机(EVM)为整数指定固定大小的数据类型。这意味着一个整型变量只能有一定范围的数字表示。例如,一个 uint8 ,只能存储在范围 [0,255] 的数字。试图存储 256 到一个 uint8 将变成 0。不加注意的话,只要没有检查用户输入又执行计算,导致数字超出存储它们的数据类型允许的范围,Solidity 中的变量就可以被用来组织攻击。

整型溢出实例

乘法溢出:

案例:CVE-2018-10299https://nvd.nist.gov/vuln/detail/CVE-2018-10299)
这里只拿出里面存在漏洞的一部分代码作为讲解

如上图所致,在该合约当中的第24行,在计算需要转账的总额度amount时未使用SafeMath函数进行溢出检查,直接将转账的地址个数与每个地址接收的代币数量进行乘法操作,如果这里输入极大的value,那么amount的值将有可能发生溢出,导致代币增发。
下面在Remix中进行测试:
1.编译

2.部署

3.溢出操作
调用函数batchTransfer函数,并向batchTransfer函数中传入地址数组["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"],以及value的值“0x8000000000000000000000000000000000000000000000000000000000000000”即2的255次方,这样做的目的是为了得到下面的结果:

amout=2**255*2

这样一来amount就超出了uint256数据类型的最大范围[0,2**256-1],发送账户的余额不减少,而接受者账户代币增加,实现了“无中生有”。
批量转账操作:

查看接受者账户余额:

案例二: CryptonitexCoin:
合约地址:https://etherscan.io/address/0x2c0c06dae6bc7d7d7d8b0d67a1f1a47d6165e373#code

如上图所示,在该智能合约当中的第240行存在整型溢出,由于amount参数可控,我们可以将其构造为一个较大的数值,而且sellPrices的值是由合约的owner来指定的(第226~229行),当合约的owner指定一个非常高的sellPrices的时候,如果我们在使用一较大的amount值与之相乘,那么最后的结果将有可能发生溢出,从而实现“高价低卖”,卖家看似是以很高的价格卖出去了代币,但是却收到的资本却非常的少。这个实例仅仅做说明,不再进行演示。

减法溢出

案例代码如下:

如上图所示,在智能合约中的distribute函数的功能是从owner账户向指定的地址列表传入代币,但是在对balance[owner]的账户做减法运算的时候,未使用SafeMath函数进行数值运算操作,而且也没有判断合约的owner是否有足够的代币,直接一个循环对owner进行减法处理,这里如果转出的代币总量大于owner账户余额,那么balance[owner]将会发生下溢,变成一个极大的值。
在remix中演示如下:
(1)编译

(2)部署

(3)下溢操作

调用distribute函数传入地址数组:
["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"],使用owner分别向这两个地址发送代币。
执行之前owner的余额:

执行distribute函数:

执行之后owner的余额:

可以从上面的结果当中看到合约的owner在执行完distribute函数之后,按理来说转账操作应该会使得合约的owner的代币减少,但是这里去不减反增了,故这里的“下溢”确实存在。

加法溢出

案例: GEMCHAIN
合约地址:https://etherscan.io/address/0xfb340423dfac531b801d7586c98fe31e12a32f31#code

如上上图所示,该智能合约中的mintToken函数用于增发代币,但是在增发代币的过程中对于加法操作没有使用SafeMath函数进行数值运算操作,而且也没有使用require对是否发生溢出进行检查,故这里存在溢出风险,如果合约的owner给target增发较多数量的mintedAmount那么将会导致溢出问题的发生。
使用remix演示如下:
(1)编译

(2)部署

(3)溢出操作

第一次铸币:
首先,我们先调用mintToken函数向地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”铸币,铸币的数量为:
“0x8000000000000000000000000000000000000000000000000000000000000000”即2的255次方

铸币之后地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”的余额为:

为了让其发生溢出,我们还需要向地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”铸币,铸币的数量仍然为:“0x8000000000000000000000000000000000000000000000000000000000000000”即2的255次方,目的就是为了让2的255次方+2的255次方发生溢出,超出uint256的最大范围。下面具体看操作

第二次铸币:

查看余额:

从上面的结果我们可以发现确实发生了溢出!可想而知,如果合约的owner在不校验溢出问题的情况下向某一地址铸币,那么该地址如果发生溢出,那么代币数量将会发生变化,时而出现减少的情况(因为发生溢出)。

特殊情况
有时候你会发现虽然我们看到一个合约当中有整型溢出的风险,例如在transfer函数中未使用require进行溢出校验,同时也没有使用SafeMath函数进行数值运算防护的情形,但是该合约当中已经规定了token的总量(totalSupply),而且没有铸币函数(mintToken)另外增加代币,那么合约总体来说是安全的,不存在整型溢出,为什么这样说呢?因为你永远都不会发生两个数值相加超过uint256的情况,但是在这中情况下你就应该将目光放到“乘法溢出”或“减法下溢”的问题上来进行查找,审计是否真的不存在“整型溢出”问题。

注:这里可以给大家给几个GitHub上的罗列出的存在整型溢出漏洞的合约的地址,大家可以使用笔者在本篇文章中描述的方法自己在本地进行一个复现查看,验证是否存在!

  1. https://github.com/sec-bit/awesome-buggy-erc20-tokens/blob/59e167f74a8d7cf48eadf25a75c65e461450aea0/raw/totalsupply-overflow.txt
  2. https://github.com/peckshield/vuln_disclosure/blob/7a1e99695945220f4bbc10100f72fa7ecb9e0a79/tradeTrap.csv
  3. https://github.com/BlockChainsSecurity/EtherTokens/blob/6e1e0952bc2a4b213cdc6db6ba7a855d9c776242/GEMCHAIN/mint%20integer%20overflow.md
  4. https://github.com/dwfault/AirTokens/blob/aff7102887096a6c8d384820835818f445f3401f/Link_Platform__LNK_/mint%20integer%20overflow.md

防范整型溢出问题

那么如何防范这种整型溢出问题呢?官方给出的建议是使用OpenZepplin提供的SafeMath函数库进行数值运算操作,使用SafeMath库函数可以有效的对溢出问题进行检查与防范,SafeMath函数库源代码如下:
Openzeppline:
https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/math/SafeMath.sol

pragma solidity ^0.5.2;

/**
 * @title SafeMath
 * @dev Unsigned math operations with safety checks that revert on error
 */
library SafeMath {
    /**
     * @dev Multiplies two unsigned integers, reverts on overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b);

        return c;
    }

    /**
     * @dev Integer division of two unsigned integers truncating the quotient, reverts on division by zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // Solidity only automatically asserts when dividing by 0
        require(b > 0);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    /**
     * @dev Subtracts two unsigned integers, reverts on overflow (i.e. if subtrahend is greater than minuend).
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b <= a);
        uint256 c = a - b;

        return c;
    }

    /**
     * @dev Adds two unsigned integers, reverts on overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a);

        return c;
    }

    /**
     * @dev Divides two unsigned integers and returns the remainder (unsigned integer modulo),
     * reverts when dividing by zero.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        require(b != 0);
        return a % b;
    }
}

应用了SafeMath函数的智能合约实例:
https://etherscan.io/address/0xB8c77482e45F1F44dE1745F52C74426C631bDD52#code

可以看到在上面的智能合约当中对于数值运算都使用了SafeMath函数进行操作,而且也使用了require对溢出校验进行防护,总体较为安全。

总结

整型溢出问题发生的根源还是在于合约的开发者在开发合约时未考虑到“整型溢出”问题。作为审计人员的我们在看到合约时也要保持清醒,对于存在疑惑的地方应该采用“调试、验证”的方法去排除疑虑,而且在审计的过程中也要十分的认真、细心才可以,不要放过任何一个有可能存在问题的地方,例如修饰器/修饰词对应的权限问题、逻辑处理问题等等。
智能合约审计系列的后续文章,后期会陆续更新,谢谢各位的阅读,希望对大家有所帮助!Thank you!

点击收藏 | 2 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖