文章前言
与大多数区块链一样,以太坊节点汇集交易并将其形成块,一旦矿工解决了共识机制(目前Ethereum的ETHASH PoW),这些交易就被认为是有效的,解决该区块的矿工也会选择来自该矿池的哪些交易将包含在该区块中,这通常是由gasPrice交易决定的,在这里有一个潜在的攻击媒介,攻击者可以观察事务池中是否存在可能包含问题解决方案的事务,修改或撤销攻击者的权限或更改合约中的对攻击者不利的状态,然后攻击者可以从这个事务中获取数据,并创建一个更高级别的事务gasPrice并在原始之前将其交易包含在一个区块中。
条件竞争
下面给出一个示例合约:
contract FindThisHash {
bytes32 constant public hash = 0xb5b5b97fafd9855eec9b41f74dfb6c38f5951141f9a3ecd7f44d5479b630ee0a;
constructor() public payable {} // load with ether
function solve(string solution) public {
// If you can find the pre image of the hash, receive 1000 ether
require(hash == sha3(solution));
msg.sender.transfer(1000 ether);
}
}
这个合约包含1000个ether,找到并提交正确答案的用户将得到这笔奖励,当一个用户找出答案并调用solve函数,并把答案作为参数,然而不幸的是,攻击者可以观察交易池中任何人提交的答案,他们看到这个解决方案,检查它的有效性,然后提交一个远高于原始交易的gasPrice的新交易,解决该问题的矿工可能会因攻击者的gasPrice更高而先打包攻击者的交易,攻击者将获得1000ether,最初解决问题的用户将不会得到任何奖励(合约中没有剩余ether),条件竞争问题由此产生!
防御措施
有两类用户可以进行这种的提前交易攻击:
-
用户(修改他们的交易的gasPrice)
-
矿工自己(他们可以按照他们认为合适的方式重新排序交易)
总体来说,一个易受第一类(用户)攻击的合约比一个易受第二类(矿工)攻击的合约明显更糟糕,因为矿工只能在解决一个区块时执行攻击,这对于任何针对特定区块的单个矿工来说都是不可能的,下面给出一些缓解措施:
gaslimt
可以采用的一种方法是在合约中创建限制条件,即gasPrice上限,这可以防止用户增加gasPrice并获得超出上限的优先事务排序,这种预防措施只能缓解第一类攻击者(任意用户)的攻击,在这种情况下,矿工仍然可以攻击合约,因为无论gasPrice如何,他们都可以根据需要排序交易。
披露方案
更可靠的方法是尽可能使用提交—披露方案(commit-reveal),这种方案规定用户使用隐藏信息(通常是散列)发送交易,在交易已包含在块中之后,用户发送一个交易来解密已经发送的数据(披露阶段),此方法可防止矿工和用户进行前瞻性交易,因为他们无法确定交易内容,然而这种方法无法隐藏交易价值(在某些情况下,这是需要隐藏的有价值信息),ENS智能合约允许用户发送交易,其承诺数据包括他们愿意花费的以太数量,然后用户可以发送任意值的交易,在披露阶段用户退还了交易中发送的金额与他们愿意花费的金额之间的差额。
相关讨论
对于Approve函数的\\”条件竞争\\”问题,曾引发的广泛的讨论:
首先是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”判断是为了安全,不加是为了标准,你会如何抉择?(虽然该类型的漏洞利用难度比较高)
文末总结
合约的开发者应当建立一套开发标准规范,同时尽可能的搜集网络上公开的现有的合约的漏洞的类型以及利用方法和安全补救方法,之后不断的完善自己的开发体系,而且在合约上线之前建议还是找专门的公司进行合约的安全审计,对合约做一次评估为好。
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34157.html