(图片来自:consensys)
可重入(Reentrancy)或整数溢出漏洞,是大多数开发人员知道或者至少听说过的,关于智能合约当中容易出现的安全问题。另一方面,在考虑智能合约的安全性时,你可能不会立即想到针对密码签名实现的攻击方式。它们通常是与网络协议相关联的。例如,签名重放攻击(signature replay attacks),一个恶意用户可窃听包含有效签名的协议序列,并针对目标进行重放攻击,以期获得益处。
本文将解释智能合约处理DAPP生成签名时可能存在的两种类型的漏洞。我们将通过Diligence团队在今年早些时候完成的现实例子审计结果进行分析。此外,我们将讨论如何设计智能合约,以避免这类漏洞的出现。
签名是以太坊网络中的基础,发送至网络的每笔交易都必须具有有效的签名。下图显示了这种交易的一个例子。除了交易标准属性,例如 from
、to
、gas
、value
或input
在全局命名空间中可用,并且经常出现在智能合约代码中,字段v
,r
以及s
共同组成了交易签名。
以太坊网络确保只有具有有效签名的交易可被纳入新的区块当中。这为交易提供了以下安全属性:
msg.sender
是真实的;from
字段中公共地址对应的私钥签名的,这是不可否认的,并且拥有私钥的签名方已经进行了任何状态更改。
协议层并不是签名发挥作用的唯一场地。签名也越来越多地被用于智能合约本身。随着gas价格的上涨,而扩容解决方案仍在进程当中,则避免链上(on-chain)交易便凸显出了越来越多的重要性。当谈到链外的交易时,签名也是非常有用的,EIP-191以及EIP-712,都是有关于如何处理智能合约中签名数据的通证标准。而后者旨在改善链外消息签名的可用性。那么,为什么它是有用的,以及它是如何节省链上交易的?
让我们来查看一个简单的例子。爱丽丝为鲍伯创建了一个命题,她将其编码成了一条消息。她还用自己的私钥创建了消息的签名,并通过协商好的通道发送给鲍伯。鲍伯可以验证爱丽丝是否签署了该消息,如果鲍伯认为该命题是合适的,那么他可以创建新的交易,将他自己的消息、爱丽丝的消息及签名共同纳入到一个智能合约当中。通过数据,这个智能合约可以证实:
第一个例子,是由Consensys的Diligence部门在审计去中心化新闻应用Civil时发现的一个漏洞例子,与此案例相关的系统的第一部分,被Civil称之为Newsroom(新闻编辑室),而内容编辑可以把自己的文章发布到这个Newsroom,他们还可以为自己的内存创作进行加密签名,以此证明内容实际上是由他们创造的。pushRevision()
函数对现有内容进行更新或修订。参数内容哈希、内容URI、时间戳以及签名,为内容创建新的修订。之后,verifiyRevisionSignature()
函数会调用提议修订,以及最初创建第一个签名修订的内容作者。根据设计,新修订的签署者,只能是创建初始签名内容版本的作者。
verifiyRevisionSignature()
函数会根据DApp生成的内容哈希,以及Newsroom合约的地址,创建一个已签名的消息哈希。然后,调用recover()
函数(来自OpenZeppelin 的ECRecovery库)。随后,调用ecrecover()
函数,并验证作者是否真正签署了消息。已讨论过的两个函数代码是没有问题的,因为只有最初创建内容的作者才能为它创建新的版本,所以实际上它们不存在什么安全问题。
问题在于,合约是不会跟踪内容哈希的,因此,已提交的一个内容哈希及其用户签名,实际是有可能被提交多次的。而恶意的内容作者就可以利用这个漏洞,从其他作者那里获取有效的签名和内容哈希,并在他们不知情的情况下为他们创建新的有效修订。
Civil 已通过跟踪这些内容哈希,并拒绝已是先前修订部分的哈希,来解决这个问题。
在上一次审计去中心化协议0x的过程当中,Diligence发现了这种漏洞类型的一个实例。以下解释,是这次审计报告当中3-2节内容中描述的问题总结。0x协议具有不同签名类型的各种签名验证器,包括Web3以及EIP712。另一个存在的验证器称为SignatureType.Caller
,如果order.makerAddress
等于msg.sender
(order.makerAddress是创建order的用户),则允许order有效。如果设置了SignatureType.Caller
,则没有实际签名验证是由交易合约执行的。现在还不清楚为什么这会导致漏洞,因为已经证实msg.sender
以及order的创建者是相同的,至少从理论上看是这样的。
除了交易合约之外,0x系统还有另一部分称为Forwarder的合约,有了这个合约,用户可以简单地发送以太币,以及他们想要填写的 order,而这个Forwarder合约会在同一笔交易中执行所有的order;
想要用以太币交易其他通证的用户,可以向其他用户发送order,而Forwarder合约将代表他们进行交易。这个交易合约会验证每个order,以确保order签名的有效性,并确保其他用户已实际签署了order。让我们再次查看上面的图,并重新评估以下假设:如果order.makerAddress
等于msg.sender
,则我们不需要在这个交易合约当中进行适当的签名验证,因为发送交易的用户也是order的创建者。如果用户直接向交易合约发送order,则该假设成立。但是,如果我们通过Forwarder合约发送这个order,将order.makerAddress
设置为 Forwarder合约的地址,并使用SignatureType.Caller
签名验证器呢?
在交易执行处理结算个别order的过程中,Forwarder合约会调用这个交易合约。这个交易合约会验证这个order.makerAddress
中的地址,就是msg.sender
,在这种情况下,可以将其设置为Forwarder地址。由于合约在交易双方之间起到了中介作用,所以order.takerAddress
通常被设置为Forwarder地址。因此,恶意用户可以使用Forwarder处理order,其中合约会与其本身进行交易,因为它既是接受者又是制造者。这是因为以下的原因:
transferFrom
((address _from, address _to, uint256 _value) )的ERC20规范,不会阻止用户进行“空传输”。而 _from
和_to
可以是相同的地址;msg.sender
没有发送order。
综上所述,假设消息的发送者也是其创建者,而不去验证其签名,这可能是不安全的,尤其是在通过代理转发交易的情况下。在合约处理消息签名的任何时候,都需要执行正确的签名恢复及验证。0x通过删除了 SignatureType.Caller
签名验证器修复了这个问题。