本文首次发表在Freebuf上,本文只做归档用。
整形溢出(Arithmetic Issues)
如下代码,如果没有assert判断,那么sellerBalance+value可能会超出uint上限制导致溢出。
1 | pragma solidity ^0.4.15; |
危险的delegatecall(dangerous delegatecall)contractfuzzer
首先需要了解call和delegatecall的区别:call和delegatecall都为合约相互调用时的函数,假设A调用B函数,call方法结果展示到B中,delegatecall方法结果展示到A中。
在如下示例中,Mark如果用delegatecall调用了恶意合约Steal,那么Mark合约会被删除。
复现:
- 用A账户部署Steal,用B账户部署Mark合约,并在部署时为合约附加10个ether。
- 账户B调用Mark.call(address(Steal)),即用B调用Steal的Innocence方法,实际上innocence会在Mark的上下文环境运行,发现账户B收到合约的10 ether(注意不是A账户)
- 用C账户执行Mark.deposit()方法,并附加10ether,再调用destruct方法,发现B无法收到10ether,说明合约确实已经在第二步被销毁。
1 | pragma solidity ^0.4.2; |
无Gas发送(Gasless Send)contractfuzzer
合约C调用合约D1时,由于fallback函数修改了storage变量——这是一个消耗大量gas的操作——导致了超过fallback的gas上限(2300gas)导致fallback失败,调用D2时,由于没有超过上限,调用成功。
复现:
- 用10ether部署C合约,0ether部署D1合约,0ether部署D2合约
- 调用C.pay(1000000000000000000, address(D1)),D1的count值仍为0。
- 调用D1.kill(),以太币不增加。2,3两步说明了D1的fallback调用失败
- 调用C.pay(1000000000000000000,address(D2))
- 调用D2.kill(),发现账户增加1ether,说明D2的fallback调用成功
1 | pragma solidity ^0.4.2; |
依赖于交易顺序/条件竞争(TOD/Front Running)smarter
由于:
- 只有当交易被打包进区块时,他才是不可更改的
- 区块会优先打包gasprice更高的交易
所以攻击者可以恶意操控交易顺序从而使合约对自己有利。如图,出题人和做题人同时发起合约,那么做题人得到的奖励因合约执行顺序不同而不同。
再例如ERC20标准中的approve,整个流程是这样的:
- 用户A授权用户B 100代币的额度
- 用户A觉得100代币的额度太高了,再次调用approve试图把额度改为50
- 用户B在待交易处(打包前)看到了这笔交易
- 用户B构造一笔提取100代币的交易,通过条件竞争将这笔交易打包到了修改额度之前,成功提取了100代币
- 用户B发起了第二次交易,提取50代币,用户B成功拥有了150代币
1 | function approve(address _spender, uint256 _value) public returns (bool success){ |
依赖于时间戳(Timestamp Dependence/Time manipulation)contractfuzzer
攻击者可以修改区块的时间戳±900s以此获益。
依赖于区块号(BlockNumber Dependency)contractfuzzer
和上面依赖时间戳类似
ExceptionDisordercontractfuzzer
1 | function withdraw(uint256 _amount) public { |
上面给出的代码中使用 send() 函数进行转账,因为这里没有验证 send() 返回值,如果msg.sender 为合约账户 fallback() 调用失败,则 send() 返回false,最终导致账户余额减少了,钱却没有拿到。
未处理的异常(Mishandled Exceptions/Unchecked Return Values For Low Level Calls)smarter
例如合约KoET,攻击者可以控制函数调用次数(EVM限制调用深度为1024),从而导致send函数调用失败,但是接下来的代码会继续执行,这样前一个国王就无法得到报酬(compensation)。
Attacker:
复现失败,在Remix中运行递归会崩溃,在实际运行中由于Gas较高,无法交易(预算手续费大于30ether)。
重入漏洞(Reentrancy/DAO)seebug1
Solidity 中 <address>.transfer()
,<address>.send()
和<address>.gas().call.vale()()
都可以用于向某一地址发送 ether,他们的区别在于:
<address>.transfer()
- 当发送失败时会 throw; 回滚状态
- 只会传递 2300 Gas 供调用,防止重入(reentrancy)
<address>.send()
- 当发送失败时会返回 false 布尔值
- 只会传递 2300 Gas 供调用,防止重入(reentrancy)
<address>.gas().call.value()()
- 当发送失败时会返回 false 布尔值
- 传递所有可用 Gas 进行调用(可通过 gas(gas_value) 进行限制),不能有效防止重入(reentrancy)
当外部账户或其他合约向一个合约地址发送ether时,会执行该合约的fallback函数(当调用合约时没有匹配到函数,也会调用没有名字的fallback函数——The DAO)。且call.value()会将所有可用Gas给予外部调用(fallback函数),若在fallback函数中再调用withdraw函数,则会导致递归问题。攻击者可以部署一个恶意递归的合约将公共钱包这个合约账户里的Ether全部提出来。
复现:
- 账户A部署IDMoney合约,账户B部署Attack合约
- 账户A调用IDMoney()方法,并附加10ether
- 账户B部署Attack合约,附加2ether
- 账户B调用Attack.setVictim()方法,设置victim变量为IDMoney合约地址
- 账户B调用Attack.step1()方法,设置amount=1000000000000000000,即合约Attack调用合约IDMoney.deposit()方法
- 账户B调用Attack.step2()方法,设置amount=500000000000000000
- 账户B调用Attack.stopAttack()方法,获得IDMoney的所有余额(包括A的存款,严格说是合约中除了500000000000000000wei的余额)
1 | pragma solidity ^0.4.19; |
注意到合约IDMoney.withdraw()方法已经存在检查账户余额的代码,但是却未能生效,原因是递归调用时没有执行到balances[msg.sender] -= amount;
,因此调用时,账户的余额是不变的,而真正导致递归调用退出的是require(this.balance >= amount);
,这也是为何调用结束后合约还剩下amount数量的以太币的原因。有人会问,如果把这句话删掉呢?我本以为合约会报错,但是很遗憾,合约依然能够正常运行,并且合约中不再剩下任何以太币。
DoS攻击DoS
频繁调用某些Op(EXTCODESIZE和SUICIDE),这些Op花费的Gas小,但是需要大量资源(计算资源,I/O),以此造成DoS,对以太坊合约进行 DoS 攻击,可能导致 Ether 和 Gas 的大量消耗,更严重的是让原本的合约代码逻辑无法正常运行。
复现:
- 账户A部署PresidentOfCountry合约设置_price为1e18(1ether)。
- 账户B调用PresidentOfCountry,并附加1ether,成为President,price=2ether
- 账户C部署Attack,调用start_attack(address(PresidentOfCountry))并附加2ether,账户C成为President。
- 账户B调用PresidentOfCountry,并附加4ether,由于B要成为president,,需要调用PresidentOfCountry合约的becomePresident()函数,该函数会通过 transfer() 把ETH退回给之前的president,即Attack合约地址,但是因为Attacker的回退函数revert()抛异常==>transfer()抛异常==>becomePresident()抛异常,故而账户B永远无法成为president了。
1 | pragma solidity ^0.4.10; |
重放攻击blackhat2018
如果合约存在相同的代码,则攻击者可以使用合约A函数的参数调用合约B。
1 | /* |
变量覆盖varreplace
以如下代码为例,Solidity存储机制的问题,p初始化后的name、mappedAddress地址会与变量testA、testB地址重合,导致调用test函数给结构体p赋值后,变量testA和testB的值也会被覆盖。
复现:
- 调用TestContract.test()方法
- 检查testA和testB的值,已被改变
1 | pragma solidity ^0.4.0; |
相关工作
DASPdasp总结了以太坊合约的Top10安全性问题
luu等人smarter设计一套基于符号执行的智能合约安全审计工具oyente(已做过演示,目前可以检测的漏洞有整形溢出,合约依赖交易顺序,依赖时间戳的漏洞,未处理异常和重入漏洞。
Nikolicmaian等人设计了一套符号执行检测智能合约的工具MAIAN,这些问题包括合约永久锁定资金,资金可被恶意用户转账以及被任意用户杀死,我们选用了34200个合约(去重复后有2365个),我们抽样调查了3759个合约,得到89%的正确率。
jiang等人contractfuzzer设计了一套基于fuzz的智能合约审计工具ContractFuzzer,他们通过在EVM中插桩,以此获取程序在执行中产生的信息,通过预先设置的测试准则发现漏洞,他们设计的工具可以检测无Gas发送、Exception Disorder、重入漏洞、依赖于时间戳漏洞、依赖于区块高度漏洞、危险的Delegatecall、合约永久锁定资金7大安全性问题,经过试验,ContractFuzzer发现漏洞的准确率较高,但是相较于Oyente,此工具找到的漏洞数量较少。
Liu等人ReGuard构建了基于fuzz的智能合约检测工具,旨在检测合约中的重入漏洞,实验表明,相较于Oyente,该工具有更高的准确率,并且能发现更多数量的问题。
chen等人DoS通过动态调整Op执行的gas花费阻止DoS攻击(通过反复执行小gas的opcode,消耗系统资源造成dos)。
参考文献
DoS. Chen, Ting, et al. “An Adaptive Gas Cost Mechanism for Ethereum to Defend Against Under-Priced DoS Attacks.” International Conference on Information Security Practice and Experience. Springer, Cham, 2017. ↩
smarter. Luu, Loi, et al. “Making smart contracts smarter.” Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security. ACM, 2016. ↩
blackhat2018. Bai, Zhenxuan, et al. “Your May Have Paid More than You Imagine:Replay Attacks on Ethereum Smart Contracts.” Blackhat. 2018 ↩
seebug1. 以太坊智能合约安全入门了解一下(上), https://paper.seebug.org/601/ ↩
contractfuzzer. Bo Jiang, Ye Liu, and W.K. Chan. 2018. ContractFuzzer: Fuzzing Smart Contracts for Vulnerability Detection. In Proceedings ofthe 33rd IEEE/ACM International Conference on Automated Software Engineering (ASE’18), September 3–7, Montpellier, France, 10 pages. ↩
varreplace. Solidity中存储方式错误使用所导致的变量覆盖,http://www.freebuf.com/articles/blockchain-articles/175237.html ↩
maian. Ivica Nikolic, Aashish Kolluri, Ilya Sergey, Prateek Saxena, and Aquinas Hobor. 2018. Finding The Greedy, Prodigal, and Suicidal Contracts at Scale. (2018). DOI:https://doi.org/arXiv:1802.06038v1 ↩
ReGuard. Liu, C., Liu, H., Cao, Z., Chen, Z., Chen, B., & Roscoe, B. (2018). ReGuard: Finding reentrancy bugs in smart contracts. Proceedings - International Conference on Software Engineering, 65–68. https://doi.org/10.1145/3183440.3183495 ↩
dasp. http://www.dasp.co/ ↩