这是一篇比较完整的评估智能合约安全的文章。
当下区块链技术的增长对分布式共识展示出了无与伦比的机会,智能合约应用在之前时间里面出现了百万美元的丢失,(如:非常有名的DAO Attack事件),这令我们对于智能合约应用的安全性产生了非常大的担忧。在这篇文章中我们将透彻的展示多种针对能合约应用的攻击和为确保智能合约安全性所必须要进行的审计过程,保持最新的开发方式以及讨论从各种可靠的源中得到的灵感。
智能合约审计如同传统的代码审计一样是保证代码安全性非常重要的基础步骤(审计的过程主要经过细致的研究对象代码,在代码被最终发布在生产环境之前从中找出可能存在的安全性缺陷以及漏洞)。这就像是在一座桥梁正式运行之前进行的测试一样。这两种情况下,开发者(建造者)都有责任去保证产品的安全性。区块链技术本质是一本不断自我复制和单项增长的Merkle Trees的列表(因此这是不允许修改的),由于智能合约是自己执行的且一经发布不可修改,所以必须要在发布之前找出它所有的漏洞。
这个章节讨论一些值得注意的已知攻击,通过下面的步骤你也可以在审计过程中找到相应的攻击方式。
条件竞争是一种在常规系统里面由于事件发生的顺序没有按照预期而导致的问题。在智能合约中条件竞争可能发生在调用外部智能合约而导致调用流程被恶意的控制。
Reentrancy(重入)攻击是由于一些方法在没有完成之前就被重新单独的调用而导致的一种条件竞争。例如:DAO被攻击的一种原因就是由于不同的方法内部没有按照预期的方式来进行交互调用导致的。此类问题的主要解决方案是阻止一些方法中的并行调用,尤其要仔细检查智能合约中的外部调用。
这是一个有Reentrancy bug的代码片段:
contract ReentrancyVulnerability {
function withdraw () {
uint transferAmount = 10 ether;
if (!msg.sender.call.value(transferAmount)()) throw;
}
function deposit() payable {} // make contract payable and send ether
}
上面的代码中 !标示的地方是一个外部调用,此处的外部调用可以被绕过。在withdraw方法中函数中我们传了10个以太到调用我们合约的调用者中,到目前来说并没有啥问题。然鹅,接受者可以通过多次递归的调用该函数进行恶意利用,具体看下面的流程。此处将上述的智能合约代码存放在ReentrancyVulnerability.sol文件中并且作为攻击者创建一个Hacker.sol的文件作为一个攻击的智能合约来利用上述的外部调用的地方。可以用这样的合约来对一些可能存在潜在漏洞的合约进行安全测试。
contract Hacker {
ReentrancyVulnerability r;
uint public count;
event LogFallback(uint c, uint balance);
function Attacker(address vulnerable) {
r = ReentrancyVulnerability(vulnerable);
}
function attack() {
r.withdraw();
}
function () payable {
count++;
LogFallback(count, this.balance);
if (count < 10) {
r.withdraw();
}
}
在Hacker.sol文件中,定义了两个比较重要的函数。第一个是用于提现的函数调用了存在重入漏洞的智能合约的withdraw()函数发送10个以太到hacker智能合约中,这样就会触发在hacker合约中定义的 function() payable {}这个回调函数(该函数就是拿到超额以太的真凶)。
这里的Event LogFallback(uint v, uint balance)在回调函数被调用的时候会被触发,这个事件的触发同时会被通过当作循环计数变量的count在if控制语句中进行控制,当本函数被调用10次以后将会停止继续递归的调用受害合约的withdraw函数,防止合约最终的调用失败导致以太被退回。
上述过程的最终表现就是当Hacker合约中反复的调用withdarw()函数直到if条件控制语句中的条件达成即count变量最终到达10。
重入只是问题的表象而不是问题的原因,所以请确保对于那些正在被使用的外部调用都要进行详细的分析,而不是直接试图在函数中阻止这些这些重入的发生,意思就是要在进行外部调用之前完成所有应该完成的内部操作。
提示:对于外部调用应该采用最小功能的模式 或者 必须保证在外部调用之前就把所有的内部操作都完成。
跨方法的条件竞争:类似于攻击两个方法共享一个状态的方法,解决方法当然也是一样的。这里有个例子就是攻击外部调用函数transfer()在用户设置它的余额为0之前,虽然攻击者一经取回了它的以太。
function transfer(address to, uint amount) {
transfer occurs here
}
function withdrawBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
require(msg.sender.call.value(amountToWithdraw)());
userBalances[msg.sender] = 0;
}
在上面的代码中,黑客可能在withdrawBalance()方法正在执行的过程中 或者 是外部调用已经在调用的过程中 调用transfer函数,如你所见这个userBalance被设置为0需要在外部调用之后才会被赋值,所以提现功能可能在withdrawal()函数已经被调用的情况下继续操作。
Transaction-Ordering Dependence (TOD) / Front Running 也是另一种条件竞争,不过这次的操作的对象是区块链中的区块内的交易的顺序。这些相关的交易只会在内存池里面存在很短的时间。实施Front Running攻击可以让一个用户可以通过操控交易的顺序来使得自己得到收益而让别人蒙受损失的效果。
有一种可能造成Front Running攻击的情况就是 Timestamp Dependence(时间戳依赖),所以你需要仔细的检查对于时间戳的使用,尤其在一些交易时间具有非常重要的金融属性的地方,比如一些菠菜合约中。在以太坊中时间戳不是和全局时间直接同步的,这里的差异性可以让矿工得到很大的优势。
另外一种条件是Integer Overflow and Underflow,这种情况的发生是由于无符号的整数当达到最大的循环以后继续增加可能会变成0 或者 一个负数如果被当成无符号整数可能会被当成一个非常大的值。这些整数溢出的问题都可以通过Mythril来进行检测。
下面是一个整数溢出的非常直白的例子:
uint public c = a + b;
攻击者可以通过加法 或者 减法来进行整数上溢,和下溢的操作:
function underflow() public {
c -= 2**256-1;
}
function overflow() public {
c += 2**256-1;
}
另外一种攻击方法要被启用需要可以强行的对合约进行发送以太的操作,要确认在逻辑中没有资金进入合约的限制,攻击可以将这样的逻辑给干掉。
任何像下面的代码都逻辑都存在上述的漏洞:
require(this.balance > 0); // note that 0 could be any number
为了防止这样的代码从你正在分析的代码中成功启动,审计过程应该采用工程化方法(具有理论和实践背景的严格验证,以及工具的应用)。
上述已知攻击的漏洞和一些其他的安全性问题,可以通过下面的审计过程找到,这些步骤来自于ConsenSys Best Practices、HashEx audit framework还有一些其他的公开的审计方法的汇总和整理。
确保在智能合约发布前进行一次完整的代码审计过程,尽可能保证完整审计的代码是接近用户最终接触到的最终版本的智能合约版本。
提供一份免责声明:注意审计的目的是提供一些对于安全的讨论,而不是提供任何安全的保证。例如:“本次审计中出现的信息仅供一般讨论,不打算向任何个人或实体提供法律安全保障。”
需要解释一下你的身份:表现出来你在这个领域的权威,或者为什么你可以被信任进行严格的代码审计分析,并且用强有力的审计来证明它。
从安全的角度来概述你的审计的方法和过程。
分析上述的文档中的相关攻击手段是否会在当前的合约中被触发。
在这一步,重点讨论漏洞的严重级别,并且依照漏洞的验证级别提供不同的修复建议。这里的处理对象不仅仅是一些直接的漏洞,还要处理一些可能存在潜在漏洞的情况。
合约复杂度的增加会增加错误发生的概率,所以要注意复杂的合约逻辑、非模块代码、专用工具和代码以及执行清晰的流程。这些地方可能并不一定会引起漏洞的发生,但是确必须不能忽略。
合约在事件触发失败的的时候如何响应,如一些bug或者发生漏洞?检查合约是否会暂停或者资金是否有管理的风险。
所有的使用到的库或者工具是否已经安装到了最新的版本?最新版本的工具打上了最新的补丁,所以不要使用老版本的工具或者库避免造成不必要的风险。
来自之前的发布版本的重复代码,可能没有进行很完整严格的代码审计。所以无论如何对于之前没有经过审计的代码的使用必须非常的消息谨慎,如果一些经过了非常严格审计的代码是可以正常使用的。
状态是否在外部调用以后发生改变?外部调用可能可以操控执行流程,所以必须要先保证在外部调用之前进行完成了完整的内部调用过程。
非信任的合约是否已经被标记出来?外部合约应该被清晰的标记出来表达代码交互存在不安全性。这包括命名约定,比如UntrustedSender,而不是Sender。
外部调用的错误是否被正确的处理?如果遇到异常,合约调用的异常将会自动传播,如果不处理这种可能性(通过检查返回值),合约调用将失败。
外部调用是否喜欢Push Over Pull? 确保外部调用被隔离到它们自己的交易中,以最小化外部调用失败的后果。
代码是否假设合约将以零余额开始?一个合同地址可能会在合同创建之前收到wei,所以不应该有一个初始余额假设。
确保合约的功能里面不要把上链的时间作为重要的时间戳,因为这些数据是公开的并且一些错误的顺序还可能导致一些游戏的另一方会受到损失(如 石头剪刀布的游戏)。
需要考虑参与者在出现放弃或者不返回任何值的情况下的处理情况。
不变量是否被强制判断了?失败的断言将触发断言保护机制。在处理不变量(如assert(this))时应该使用assert()。平衡> = totalSupply);
是否进行整数除法?简单地说,所有整数的除法都是四舍五入的整数。如果这样会导致问题,那就用乘法来代替。
如果以太被强行发送会发生什么?由于ETH可以被强制发送到一个地址,请注意任何检查合约余额的不变量代码,还有就是当强行进行ETH发送的时候对这部分代码的影响。
是否使用了tx.origin?不应该使用tx.origin进行授权,因为它包含您的地址,所以另一个合同可以调用您的合同并被授权(如果使用tx.origin,建议使用msg.sender())。
在上面的讲诉攻击的那一节中,Ethereum的时间戳并不是同步真实的全局时钟的,矿工是可以利用这个差异的自行修改时间戳的,所以最好不要在关键的地方使用Ethereum的时间戳。
对发现的漏洞提出修复建议并采取进一步措施。如果合约已经被修复,要思考合约是否已经安全到可以在主网中使用?
在这里,我们将从一些历史的审计例子和代码片段中找到一些灵感,您可以将它们应用到您自己的智能合约审计中。智能合约审计领域的实体越来越多,其框架的范围主要从关注注释到关注测试为主,既有优点也有缺点。
对“unchecked-send”Bug 的一个例子,看一下的代码片段:
if (gameHasEnded && !(prizePaidOut)) {
winner.send(1000); // send a prize to the winner
prizePaidOut = True;
}
正如审计步骤一节中所讨论的,应该始终仔细审查send()的使用。在这种情况下,send()方法可能会失败,从而导致赢家无法获得报酬。类似的漏洞可能存在于拍卖这样的用例中,其中潜在的大量的资金处于风险之中。
根据Ethereum文档,“如果调用堆栈深度为1024(调用方可以强制这样做),如果接收方耗尽了GAS,也会发生一样的故障”。文档提供的解决方案是“始终检查send的返回值,或者使用更好的方式:使用收件人取款的模式”。
基于官方文档的建议,给出了下面的方案…
if (gameHasEnded && !(prizePaidOut)) {
accounts[winner] += 1000
accounts[loser] += 10
prizePaidOut = True;
}
...
function withdraw(amount) {
if (accounts[msg.sender] >= amount) {
msg.sender.send(amount);
accounts[msg.sender] -= amount;
}
}
在这种情况下,对代码进行重构,使失败的发送一次只影响一方。
ConsenSys最佳实践框架提供了许多“good and bad code”示例,它们涵盖了已知的攻击。
pragma solidity ^0.4.4; // bad
pragma solidity 0.4.4; // good
例如,上面已经指出,注释应该被锁定到一个特定的编译器版本,以避免使用不同的版本来部署合约,这可能会有更大的未发现bug的风险。
uint256 constant private salt = block.timestamp; // warning
还有此处,代码中对于使用链的时间戳的使用的地方会被标记出来,对于时间戳的使用必须非常的谨慎细致。
我们鼓励您分析由consenysus和bountyone推荐的许多其他示例。
完整的审计可能包括文档和用例的测试,用例由用户行为来解释。在这种情况下,应该使用行为驱动开发(BDD)实践,它类似于开发的智能合约的功能测试,但是更关注安全性而不是功能性。
为了使用truffle来审计Ethereum智能合约,使用标准的npm install -g truffle来安装框架,然后使用truffle init来创建项目结构(假设您之前已经安装了node.js)。
这个过程的重点是在导入合约和库用来检查测试条件之后,在测试网络中编写测试并执行它们。您可以使用通常的assert()或测试框架,如Chai。最后,只需围绕我们构建的步骤进行测试,例如检查溢位和下溢位、测试函数的极限、确保返回值的格式正确,等等。
许多以智能合约为中心的分布式应用程序都实现了各种软件工具来辅助审计实践。这些工具,例如针对漏洞的自动代码检查,可以作为一种补充,但不应该取代正式的审计过程。如前所述,Mythril是一个选项,它可以用来检测uint溢出和下溢。另一个工具是Etherscrape,在这里用于在使用send()时对重入性bug进行实时Ethereum合约的侦听。还有像Bountyone这样的分散审计平台,当工具不足时,它们会将公司和自由审计师聚合在一起。
根据发现的漏洞的严重程度,建议将重点放在合同的某些方面进行改进。您还可以建议使用bug bounty和持续性渗透测试,这是在合约发布之前发现其他bug或问题的有效方法,并提供Ethereum赏金的模式。
请注意,新添加的合约将使它们处于未经审计的状态,因为代码重构可能会引入新的漏洞。最终会造成合约新的问题——无论是在公共审计还是私人审计中。
本指南提供的审计大纲一般适用于各类智能合约,但针对的是Ethereum合约,这是目前最流行的合约,因此交易的资金最多,将它们置于最高的攻击风险和最大的审计需求。
现在您已经拥有了执行智能合约审计的工具、资源和专有技术—继续提高区块链空间的安全性和可信度。如果你有兴趣获得支付审计智能合同,或如果你需要你的智能合同审计可以看看 Bountyone.io
由于区块链技术仍然是一个新兴的、快速发展的领域,因此没有用于全面解决方案的首选资源,因此我们建议使用各种资源来增强您对本指南的理解。
“Smart Contract Security Best Practices” by ConsenSys
“Audit the Deployed Smart Contract, Not GitHub!” by ConsenSys
“Developing smart contracts: smart contract audit best practices” by SMARTYM
“The Importance Of Audits And Secure Coding For Smart Contracts” on ETHNews
“Onward with Ethereum Smart Contract Security” by Zeppelin Solutions
“EtherCamp’s Hacker Gold (HKG) public code audit” by Zeppelin Solutions
Solidified: A Full-audit Service for Smart Contracts
SmartDec: Tool-driven Smart Contract Security Platform
Harvard Innovation Lab Audits at Experfy
Security Audit performed on Ethereum Classic Multisig Wallet by Dexaran
“Scanning Live Ethereum Contracts for the ‘Unchecked-Send’ Bug” by Hacking, Distributed
*参考来源:blockgeeks,TimYeung编译整理,转载请注明来自 FreeBuf.COM