0x00 前言

本次给大家带来的是“智能合约审计系列”的第二篇——“权限隐患&条件竞争”。
在这篇文章中将会给大家介绍一些智能合约开发者在合约开发过程中经常会忽略的权限问题,包括智能合约开发者在开发中为自己留的特权(后门)所带来的安全隐患,同时还将对“条件竞争”进行一个探讨。

0x01 基础知识

权限的概念

权限是指为了保证职责的有效履行,任职者必须具备的对某事项进行决策的范围和程度。它常常用“具有xxxxxxx的权利”来进行表达,比如:公司的CEO具有否定某项提议的权利。
站在合约的管理角度来讲,智能合约中的用户可以分为合约的owner、合约的普通用户两类。合约的owner是合约的拥有者,他可以执行合约中所有的函数;合约的普通用户可以执行合约中约定范围内的函数,对于一些对权限有校验或对执行者身份有要求的函数(比如:用onlyowner修饰器修饰的函数)是无法执行的。

solidity中函数的可见性

solidity使用了public、private、internal、external来对函数的可见性进行限定,下面进行简单的介绍:
public:使用public限定的函数可以被合约内部函数、继承合约、外部合约调用。

private:使用private限定的函数只能被合约内部函数调用。

当继承合约中的函数调用父合约中用private限定的函数时会编译报错:

internal:使用internal限定的函数可以被合约内部函数以及继承合约调用。

external:使用external限定的函数只能外部合约调用。

注:函数的默认可见性为public

0x02 权限隐患

构造函数权限问题

构造函数及其作用
Solidity编写合约和面向对象编程语言非常相似,我们可以用构造函数(constructor)来初始化合约对象。Solidity中构造函数就是方法名和合约名字相同的函数,创建合约时会调用构造函数对状态变量进行数据初始化操作。

构造函数可用的函数类型为public或internal,如果有payable修饰,就只能是public类型。而大部分人的写法都是 public或者不写。不写类型则由函数可见性默认为public类型。同时,如果构造函数带参数,则一定要放在合约下的第一个函数。

版本升级后构造函数的变化
从0.4.22版本开始,solidity编译器引入了constructor关键字,以替代低版本的将合约名作为构造函数名的语法,避免程序员容易出现的编码错误。使用旧写法会出现 warning 信息。
新版本写法为:

构造函数带来的安全问题
构造函数之所以区别于普通函数,是因为构造函数它主要用户初始化整个合约对象,而且不能被任意用户所调用,所以一旦构造函数可以被任意用户调用时,调用者就可以获得初始化合约的权限,带来安全隐患。下面举几个之前引发的案例作为简要分析:
1.构造函数名与合约名不相同
在编译器0.4.22之前构造函数的函数名默认是和合约名一致的,如果智能合约的开发者在开发过程中出现"构造函数名与合约名不一致"的现象(大小写、多加了一个s等情况),那么构造函数将不再是“构造函数”,而变为一个任意用户可以调用的普通函数,任意用户可以通过调用该函数实现对合约的初始化操作,例如ReaperCoin11合约:
合约地址:https://etherscan.io//address/0x1b7cd071187ec0b2995b96ee82296cfa639572f1#code

如上图所示,根据注释可以知晓合约中的reaper11函数是“构造函数”,但是细细看该函数名与合约名——ReaperCoin11不一致,所以此处的构造函数变成了一个public修饰的普通函数!我们可以通过Remix来看看区别:
a.构造函数名与合约名不一致时:

可以看到构造函数可以被任意用户调用
b.修改构造函数名为ReaperCoin11之后,重新编译:

此时,你会发现构造函数不可被用户调用,即不可被任意用户用于初始化合约!这就是所谓的区别!
2.constructor函数不规范
在编译器0.4.22之后使用了constructor来替代原先的“构造函数名与合约名必须一致”的代码编写规范,但是一些合约开发者在开发工程中往往还是会出现各种错误,例如:在constructor前面加function,或者加了function然后开头的C写成了大写,即“function Constructor(){}”,这样便使得构造函数变成了公有函数,可被人任意调用,下面举例来说明:
a.加入function变成普通函数形式:
MDOT合约:https://etherscan.io//address/0xef7d906fd1c0eb5234df32f40c6a1cb0328d7279#code

我们使用Remix编译一下,看看是否真的是这样(口说无凭嘛!)
在编译时,发现会给出“警告”哦!但是因为“警告”在合约开发中很常见,一般不是什么致命错误所以很多合约开发者在开发合约以及调试过程中会忽略这一点!

我们部署之后,发现合约中的constructor函数缺失成为了一个“普通函数”,不再是“构造函数”:

b.constructor拼写错误
TOGToken合约:https://etherscan.io//address/0xb9d5c2548266428795fd8b1f12aedbdeb417fe54#code

我们使用Remix编译部署看看:

安全建议:使用规范的构造函数定义方法。

普通函数权限问题

对于一些普通函数,我们一般会使用一些修饰器来进行修饰,同时有时候也会使用public、private、internal、external来进行修饰,在笔者审计合约的时候发现有一些合约的开发者为自己留下的传说中的“后门”,下面简单的举几个例子:

a.burn函数权限问题
Token合约:https://etherscan.io/address/0xc42209aCcC14029c1012fB5680D95fBd6036E2a0#code

如上图所示,在该合约中的burn函数被onlyAuthorized修饰器修饰限定,通过查看可以发现onlyAuthorized修饰器限定了msg.sender必须要为合约的owner,所以只有合约的owner可以调用burn函数,那么此处不是很正常吗?又有什么风险呢?答案是此处的burn函数的功能是用于销毁代币的,而burn函数中一共有两个参数,第一个参数_member为要销毁代币的用户地址,第二个参数为要销毁的代币的数量,虽然后面使用了SafeMath函数库,但是合约的owner依旧可以通过调用burn函数然后将要销毁代币的用户地址传给第一个参数,然后将要销毁代币的数量传给第二个参数即可,合约的owner可销毁任意用户的代币,甚至归0。

b.melt函数
sacToken合约:https://etherscan.io/address/0xabc1280a0187a2020cc675437aed400185f86db6#code


如上图所示,在该合约中的melt函数用于销毁用户的代币,但是该合约只能被合约的CFO调用,那么我们看看onlyCFO修饰器的具体细节:

可以从上图中看到,CFO其实就是合约的owner,那么我们现在可以确定melt函数真正的调用者应该是合约的owner,下面我们继续分析melt函数:在melt函数中,一共有两个参数,第一个参数是要销毁代币的目标地址,第二个参数是要销毁的代币金额数量,故合约的owner可以通过传入任意用户的地址,之后传入要销毁的代币数量,通过sub减法操作,走正常的逻辑流程之后达到销毁目标用户的代币的目的。

授人以鱼不如授人以渔,类似的函数还有很多,有兴趣的读者可以在理解以上逻辑的基础上自我去挖掘一波!
安全建议:对函数的逻辑流程、参数的设置与定义、修饰器的使用、修饰词的使用严格把控。

0x03条件竞争

条件竞争的概念

条件竞争漏洞是一种服务器端的漏洞,由于服务器端在处理不同用户的请求时是并发进行的,因此,如果并发处理不当或相关操作逻辑顺序设计的不合理时,将会导致此类问题的发生。
由于以太坊区块链是公开的,每个人都可以看到其他人未决交易的内容。这意味着,如果某个用户提交了一个有价值的解决方案,恶意用户可以窃取该解决方案并以较高的费用复制其交易,以抢占原始解决方案。

ERC20中的条件竞争问题

在ERC20代币合约中有这样个函数——approve:

该函数的主要功能是授权给第三方让其代替当前账户转账给其他账户,但是在这个函数当中却存在“事务顺序依赖性问题”,简要描述如下:
假设有两个用户:用户A,用户B
1.用户A通过调用approve函数允许用户B代其转账的数量为N(N>0);
2.经过一段时间后,用户A决定将N改为M(M>0),所以再次调用approve函数;
3.用户B在第二次调用被矿工处理之前迅速调用transferFrom函数转账N数量的token;
4.用户A对approve的第二次调用成功后,用户B便可再次获得M的转账额度,即用户B通过交易顺序攻击获得了N+M的转账额度。
当然这个漏洞的利用难度还是有点高的!

条件竞争问题引起的讨论

对于“条件竞争”这个问题,曾引发的广泛的讨论:
首先是Ethereum官方给出了一个建议:
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md

上面的主要意思是限制approve函数在将配额从N修改为M时,只能先从N修改为0,再从0修改为M,可以通过以下语句进行限制:

随后就有人提出上面的这种安全建议可以解决“事务顺序依赖性”,但是如果加了require进行限制,那么合约就不符合ERC20标准规范了,具体详情可以参考下面的链接:
https://github.com/RewardsNetwork/Alloy-ICO-Contracts/issues/9

之后openzeppelin也给出了另外一个安全建议:
使用increaseApproval函数和decreaseApproval函数来代替approve函数,同时由于ERC20标准性问题合约当中也必须要有approve(没有添加require),具体代码如下:

这里的increaseApprove的含义是在原有的“配额”基础上再增加“配额”
这里的decreaseApprove的含义是在原有的“配额”基础上再减少“配额”
笔者认为如果将approve以及increaseApprove、decreaseApprove三个函数放到一个合约当中,而且这三个函数都是public,approve函数当中也没有安全判断,那么如果用户仍然可以调用Approve进行“配额”划分,此时的increaseApprove和decreaseApprove和没存在基本上是一模一样的,在这种情况下合约仍然存在“事务顺序依赖性问题”。
笔者在做智能合约审计的过程中发现有不少合约分别采用了以下三种对于“approve事务顺序依赖性”问题的处理方法:
1.在approve当中增加Require进行安全判断
例如:
https://etherscan.io/address/0x0317ada015cf35244b9f9c7d1f8f05c3651833ff#code
https://etherscan.io/address/0x3597bfd533a99c9aa083587b074434e61eb0a258#code
https://etherscan.io/address/0x38c6a68304cdefb9bec48bbfaaba5c5b47818bb2#code
……..
2.使用increasApprove和decreaseApprove函数替代approve函数,Approve函数保持ERC20标准,不增加require进行安全防范
例如:
https://etherscan.io/address/0x58a4884182d9e835597f405e5f258290e46ae7c2#code
https://etherscan.io/address/0x05d412ce18f24040bb3fa45cf2c69e506586d8e8#code
https://etherscan.io/address/0x153ed9cc1b792979d2bde0bbf45cc2a7e436a5f9#code
…….
3.使用increaseApprove和decreaseApprove函数替代Approve函数,Approve函数当中使用require进行安全防范。
例如:
https://etherscan.io/address/0xc98e0639c6d2ec037a615341c369666b110e80e5#code
https://etherscan.io/address/0xbb49a51ee5a66ca3a8cbe529379ba44ba67e6771#code
https://etherscan.io/address/0x1b22c32cd936cb97c28c5690a0695a82abf688e6#code
……..
以上的解决方案在众多的智能合约当中都可以见到,其中第一种、第二种居多,第三中偏少。
对于“事务顺序依赖性”问题的解决方案可以从以下两个角度来看:
从安全角度看:
第一种、第三种都可以成功的解决“事务顺序依赖性”问题,而第二种仍然无法有效的解决“事务顺序依赖性”问题。
从ERC20标准来看:
第一种、第三种都违反了ERC20标准规范,第二种符合ERC20标准规范。
小思考:
加了“require”判断是为了安全,不加是为了标准,你会如何抉择?(虽然该类型的漏洞利用难度比较高)

0x04 总结

合约的开发者应当建立一套开发标准规范,同时尽可能的搜集网络上公开的现有的合约的漏洞的类型以及利用方法和安全补救方法,之后不断的完善自己的开发体系,而且在合约上线之前建议还是找专门的公司进行合约的安全审计,对合约做一次评估为好。

道路千万条,安全第一条。
编码不规范,亲人两行泪~

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