1. 浮点和精度
在撰写本文时(Solidity v0.4.24),Solidity 不支持定点或浮点数。这意味着浮点表示必须用 Solidity 中的整数类型进行表示。如果没有正确实施,这可能会导致错误/漏洞。
如需进一步阅读,请参阅以太坊合约安全技术和提示:整数除法的舍入。
1.1 漏洞
由于 Solidity 中没有固定小数点类型,因此开发人员需要使用标准整数数据类型来实现它们自己的类型。在这个过程中,开发人员可能遇到一些陷阱。我将尝试在本节中重点介绍其中的一些内容。
让我们从一个代码示例开始(为简单起见,忽略任何数值上溢/下溢问题)。
这个简单的合约在代币的买卖中存在一些明显的问题。虽然买卖代币的数学计算是正确的,但浮点数的缺乏会给出错误的结果。例如,当在 [7]行 上购买令牌时,如果该值小于1 ether,最初的除法将产生0,并使得最后的乘法结果也是0(即200 wei除以1e18weiPerEth等于0)。同样,当销售代币时,如果代币数量小于10,就只能得到0 ether。事实上,这里四舍五入总是舍去,所以销售29 tokens只能得到2 ether。
这个合约的问题是精度只能到最近的 ether(即1e18 wei)。如果您在处理ERC20代币的decimals时需要更高的精度,有时会有点棘手。
1.2 预防技术
保持智能合约的正确精确度非常重要,尤其是在处理反映经济决策的比率时。
您应该确保您使用的任何比率都可以在分数中使用大数。例如,我们在示例中使用了费率tokensPerEth。但是使用weiPerTokens这样的很大的数字会更好。为求出代币的数量我们可以使用msg.sender/weiPerTokens。这样做会给出更精确的结果。
要记住的另一个策略是注意操作的顺序。在上面的例子中,代币购买量的计算是msg.value/weiPerEth * tokenPerEth。请注意,除法发生在乘法之前。如果计算首先进行乘法,然后再进行除法,即msg.value * tokenPerEth/weiPerEth,那么这个例子会达到更高的精度,。
最后,为数字定义精度时,这样做可能是一个好主意:将变量转换为更高精度,执行所有数学运算,最后在需要时将其转换回所需的输出精度。一般来说,uint256是最常见的数据类型(因为这种类型使用的 Gas 最少),它们的范围约为 60 个数量级,其中一些是可用于数学运算的精确度。有意义的是:最好让 Solidity 中的所有变量都保持高精度,而在外部应用程序中转换回较低的精度(这实际上是 ERC20 代币合约中变量decimals的工作原理)。要查看如何完成此操作的示例以及执行此操作的库,我建议查看Maker DAO DSMath。他们的命名可能不尽合理,但这个概念是非常有用的。
1.3 真实世界的例子:Ethstick
我无法找到一个舍入问题导致合约漏洞的好例子,但我相信这里有很多。如果你有一个好的想法,请随时更新。
由于缺乏一个很好的例子,我希望读者能关注Ethstick,主要是因为我喜欢合约中的酷命名。但是,这个合约并没有使用任何扩展的精确度,它是用wei来处理的。所以这个合约会有四舍五入的问题,但只会在wei的层级上出现。它有一些更严重的缺陷,但这些都与区块链上的熵源难题有关。关于 Ethstick 合约的进一步讨论,我推荐你阅读 Peter Venesses 的另一篇文章:许多以太坊合约就是黑客的糖果。
2. Tx.Origin 用作身份验证
Solidity 中有一个全局变量,tx.origin,它遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。在智能合约中使用此变量进行身份验证会使合约容易受到类似网络钓鱼的攻击。
有关进一步阅读,请参阅Stack Exchange Question,Peter Venesses 的博客和Solidity – Tx.Origin 攻击。
2.1 漏洞
授权用户使用tx.origin变量的合约通常容易受到网络钓鱼攻击的攻击,这可能会诱骗用户在有漏洞的合约上执行身份验证操作。
考虑下面这个简单的合约,
请注意,在 [11]行 中,此合约授权withdrawAll()函数使用tx.origin。攻击者可以创建下面形式的合约,
要利用这个合约,攻击者会先部署它,然后说服Phishable合约的所有者发送一定数量的 ETH 到这个恶意合约。攻击者可能把这个合约伪装成他们自己的私人地址,或者对受害人进行社会工程学攻击让后者发送某种形式的交易。受害者除非很小心,否则可能不会注意到目标地址上有代码,或者攻击者可能将其伪装为多重签名钱包或某些高级存储钱包。
只要受害者向AttackContract地址发送了一个交易(有足够的 Gas),它将调用fallback函数,后者又以attacker为参数,调用Phishable合约中的withdrawAll()函数。这将导致所有资金从Phishable合约中撤回到attacker的地址。这是因为,首先初始化调用的地址是受害者(即Phishable合约中的owner)。因此,tx.origin将等于owner、Phishable合约中 [11]行 中的require要求会通过,(合约中的钱可以全部被取出)。
2.2 预防技术
tx.origin不应该用于智能合约授权。这并不是说该tx.origin变量不应该被使用。它确实在智能合约中有一些合法用例。例如,如果有人想要拒绝外部合约调用当前合约,他们可以实现一个从require(tx.origin == msg.sender)中实现这一要求。这可以防止中间合约调用当前合约,只将合约开放给常规无代码地址。
2.3 真实世界的例子:未知
我不知道真实世界中任何使用这一手段造成攻击的例子。
3. 以太坊机关
我打算用社区发现的各种有趣机关填充本节。这些都保存在这个博客中,因为如果在实践中使用这些机关,它们可能有助于智能合约开发。
3.1 无密钥的 ether
合约地址是确定性的,这意味着它们可以在实际创建合约之前算出。创建合约的地址和产生自其他合约的地址都是这种情况。实际上,创建的合约地址取决于:
从本质上讲,合约的地址就是立约账户及其交易 Nonce 的keccak256哈希值[2]。用合约来创建合约时也是如此,只不过合约的 nonce 从 1开始,外部账户的 nonce 从 0 开始。
这意味着给定一个以太坊地址,我们可以计算出该地址可以产生的所有可能的合约地址。例如,如果地址0x123000...000是在其第 100 次交易中创建合约的,则所创合约的地址为keccak256(rlp.encode[0x123...000, 100]),也就是0xed4cafc88a13f5d58a163e61591b9385b6fe6d1a(校对注:此处疑为作者笔误。EOA 的第 100 笔交易的 Nonce 应为 99,但意思是毫无问题的)。
这是什么意思呢?这意味着您可以将 ether 发送到预先确定的地址(你不知道那个地址的私钥,但可以确定您可以用自己的某个账户在该地址上创建合约)。您可以将ether发送到该地址,日后再在该地址上创建合约取回 Ether。构造函数可用于返回所有预先发送的 ether。因此,如果有人获得了你的以太坊私钥,攻击者很难发现你的以太坊地址还可以取得这些隐藏的 Ether。事实上,如果攻击者花去了太多交易次数,以致越过了取出隐藏 Ether 所需的 Nonce,这些隐藏的 Ether 都不可能再恢复了。
让我用合约说得更清楚一点。
这个合约允许你存储无密钥的以太(相对安全,从某种意义上说你不能错误地忽略 Nonce)[3]。futureAddresses()功能可用于计算此合约可产生的前 127 个合约地址,方法是指定nonce。如果您将ether发送到其中一个地址,则可以在日后通过多次调用retrieveHiddenEther()来恢复。例如,如果您选择nonce=4(并将 ether 发送到关联的地址),则需要调用retrieveHiddenEther()四次,然后 Ether 会回到beneficiary地址。
这可以在没有合约的情况下完成。您可以将 ether 发送到可以从您的一个标准以太坊帐户创建的地址,并在以后以正确的 Nonce 恢复。但是要小心,如果你不小心超过了恢复你的以太币所需的交易 Nonce,你的资金将永远丢失。
有关一些更高级的技巧,你可以用这个小窍门做更多的信息,我推荐阅读Martin Swende 的文章。
3.2 一次性地址
以太坊交易签名使用椭圆曲线数字签名算法(ECDSA)。通常,为了在以太坊上发送经过验证的交易,您需要使用您的以太坊私钥签署一条消息,该私钥授权从您的账户中支出。更详细一点,您签名的信息就是以太坊交易的一部分,具体而言包括to, value, gas, gasPrice, nonce, data领域。以太坊签名的结果是三个数字v,r和s。我不会详细说明这些代表的内容,感兴趣的读者可以自行阅读ECDSA wiki页面(描述r和s)以及以太坊黄皮书(附录F–描述v),以及为当前使用的v而作的EIP155。
所以我们知道以太坊交易签名包含一条消息和数字v,r以及s。我们可以通过检查消息(即交易细节)、r和s是否能派生出以太坊地址,来检查签名是否有效。如果派生的以太坊地址匹配交易的from字段,那么我们知道r以及s是由拥有(或有权访问)from字段的私钥的人创建,因此签名是有效的。
现在考虑一下,我们并不拥有一个私钥,而是为任意事务构建r值和s值。考虑我们有一个交易,参数为:
我忽略了其他参数。该交易将发送 10 ether 到0xa9e地址。现在让我们假设,我们生成了一些数字r和s(这些有特定的范围)以及一个v。如果我们推导出与这些编号相关的以太坊地址,我们将得到一个随机的以太坊地址,且让我们假设为0x54321。知道这个地址,我们可以发送 10 ether 到地址0x54321(不需要拥有该地址的私钥)。在将来的任何时候,我们都可以发送交易:
以及签名,即v,以及我们生成的r和s。这将是一个有效的交易,因为派生地址将与我们的from字段一致。这使我们可以将我们的钱从这个随机地址(0x54321)中分配到我们选择的地址0xa9e。因此,我们设法将 Ether 存储在我们没有私钥的地址中,并使用一次性交易来取回 Ether。
这个机关还可以无需信任的方式向许多人发送 Ether,正如 Nick Johnson 在如何将ether 发送给 11,440 个人中所描述的那样。
有趣的 hacks/bugs 列表
CoinDash
SmartBillions
Exchange Didn’t add “0x” to payload
注 2:交易 Nonce 就像一个交易计数器。每当你发送一次交易,它就会增加。
注 3:不要部署此合约来存储任何真实的 Ether。它仅用于演示目的。它没有固有的权限,如果你部署并使用这个合约,任何人都可以取出你的以太币。
原文链接:https://blog.sigmaprime.io/solidity-security.html
作者:Dr Adrian Manning
本文由慢雾安全团队翻译。这里是最新译文的 GitHub 地址:https://github.com/slowmist/Knowledge-Base/blob/master/solidity-security-comprehensive-list-of-known-attack-vectors-and-common-anti-patterns-chinese.md。