LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 区块链资产 > tBTC安全漏洞完整披露去中心化锚定币为何步履艰难?

tBTC安全漏洞完整披露去中心化锚定币为何步履艰难?

2020-05-21 巴比特资讯 来源:区块链网络

写在前面:从备受瞩目的主网上线,到突发的紧急暂停,tBTC只用了大约2天的时间,那它到底发生了什么?由Keep团队撰写的事件回顾报告,详细指出了漏洞所在以及相关的发现经过,同时,这也暴露出了tBTC系统复杂设计所带来的挑战。

(图片来自:tuchong.com)

以下是报告译文:

2020年5月18日上午(UTC时间),在以太坊和比特币主网进行了大约48小时的测试之后,Keep团队决定触发TBTCSystem合约允许的10天紧急存款暂停,该团队发现,当某些类型的比特币地址用于赎回时,存款合约的赎回流中出现了一个重大问题,它会使开放存款的签名者保证金面临清算风险,因此决定触发这一暂停。

实际上,支持开放存款的保证金(以及部分未偿TBTC供应)完全属于一个单独的运营商,该运营商很早就加入了系统,在测试过程中,其与Keep团队经常保持沟通。在暂停后不久,团队提出用1.005 BTC 兑1 TBTC的比率交换TBTC,以恢复TBTC的供应,从而使该地址的供应恢复了99.83%。团队会触发对开放存款的有控制赎回,以释放支持这些存款的剩余保证金。Keep团队还将协调清除系统中任何剩余的未使用ETH,尽管它们不存在风险。

tBTC系统的大致设计

首先,先为大家简单介绍一下tBTC系统的设计(译者吐槽:虽然是简介,但实际并不简单)。在tBTC系统中,有权访问BTC的人(即存款人)可以在以太坊区块链上打开一个tBTC存款合约。这个存款是一个智能合约,它与网络中的3个签名者进行交互,这些签名者共同生成和控制一个比特币钱包(没有单一的签名者可以访问该钱包)。在打开一个存款合约时,存款人从几个可用的lot大小中选择一个(译者注:最初的设计为1BTC,目前有了多种选择),一旦存款生成了该钱包地址,将相应数量的BTC发送到该比特币地址。然后,存款人向存款智能合约提交证明,证明BTC转账已经发生了,并且能够铸造出等量的TBTC(以太坊ERC-20代币)。这允许BTC持有者进入tBTC系统,然后使用其TBTC代币余额与支持它的智能合约交互。

为了向以太坊区块链正确证明比特币网络上的交易,tBTC系统采用了一种中继(relay),将有关比特币区块的足够数据传送到以太坊智能合约,以确认比特币交易已累积了一定数量的确认。这是用来确保交易(a)存在于比特币链上,并且(b)被充分确认有合理的确定性,即比特币区块链的分叉将无法移除它。

为确保控制钱包的签名者,不能以未经授权的方式非法转让他们共同持有的BTC,他们必须要提供相当于存款BTC 150%价值的ETH保证金。这些保证金由存款智能合约持有,直至存款被赎回。

当TBTC持有者有意换回BTC时,他们可以通过一个称为赎回的过程来完成。赎回操作允许以太坊用户(赎回者)支付 lot大小的资金加上少量的签名费,然后其指定一个比特币地址,并授权3个签名者共同生成一个签名来完成比特币交易,将BTC从存款地址转移至指定地址。这允许TBTC持有人退出tBTC系统,回到比特币的区块链,同时将签名者的保证金返回到各自的可用资金池以支持新存款。然后,签名者瓜分掉由赎回人支付的签名费用。

事件时间线

Keep团队认为这次事件起源于部署tBTC时,该过程是在2020年3月15日15:52 (?UTC时间)完成的,当时创建了tBTC系统的抽签池。不过,该抽签池的部署本身并没有使任何资金处于风险之中,它是用于随机选择有足够保证金支持的签名者。签名者必须通过将ETH放入保证金合约来选择使用这个抽签池。然后,该合约需要签名者的额外授权,以便能够使用他们注入到保证金合约中的资金。最后,签名者必须在抽签池中进行注册。

在接下来的3天左右的时间里,有几位签名者提供了保证金,并授权了抽签池,但其中只有3位签名者在池中进行了注册,以在存款开放期间使用,而这3位签名者实际都由同一个人控制,在Keep团队测试存款和赎回时,他们在做好准备的情况下伸出了援助之手,帮助Keep进行了测试。

存款可通过https://dapp.tbtc.network/上的alpha版dApp看到,目前限制为0.001 BTC存款大小。三位签名者提供的ETH保证金也对可铸造的TBTC数量设定了一个上限,因为每一笔TBTC存款,需要以1.5倍价值的ETH进行担保。在调查潜在问题时,该dApp在2020年5月15日晚上短暂撤下,但在了解到问题后,团队便在5月16日重新启用。此外,社区中的几位成员设置了dApp的本地版本,并使用它们来开设大于0.001 BTC lot大小的存款。

5月18日2:29(UTC时间),控制这3个签名者的运营商试图赎回他们已开设的存款,但发现无法完成这个兑换过程。

他们联系了团队的一个成员,其转达了另一位团队成员注意到的一个问题,指出以太坊区块链上的高gas价格导致中继对比特币状态的更新落后了几个区块。Keep团队告知该运营者很可能是其所看到的问题,而该中继开始使用更高的gas价格,并在UTC时间 3:07赶上了进度。

在UTC时间3:13,该运营商表示他们仍然无法完成赎回操作,然后Keep团队使用了本地版本的dApp来调查问题,此时观察到存款合约中存在的一个漏洞——“Tx sends value to wrong pubkeyhash。” 该漏洞表明,dApp构建的用于向以太坊区块链显示赎回已成功完成的证据,是不正确的。特别是,该证据未能成功证明交易将存款的BTC发送到正确的赎回者地址。

经过30分钟的调查,Keep团队怀疑该问题不一定是出在dApp的客户端证明中,而是在以太坊区块链上用于赎回的特定比特币地址及其可证明性。在核实这一怀疑之前,Matt Luongo通知了涉及到的3位当事人的其中两位,让他们准备就绪,以防万一。之后,Keep团队调查了智能合约中存在的问题,并确定了潜在的保证金威胁问题。由于签名者的保证金在没有赎回证明的情况下,只能在6小时后才能扣押,因此Keep团队决定继续调查并确认合约问题,然后再采取进一步的行动。

在UTC时间4:43,Matt通知了James Prestwich,让其对该发现进行证实,而后者撰写了tBTC 合约的大部分内容,其具有丰富的比特币开发经验,并且他还是相关bitcoin-spv及中继库的作者。James在UTC时间 5:02证实了这一发现,此后,Keep团队立即开始将托管的dApp URL重定向到tBTC主页,以防止新的存款被打开。

在UTC时间5:18,Keep团队在确认问题的存在,并意识到合约无法简单通过外部修复之后,决定触发tBTC系统合约中可用的一次性10天紧急暂停。此功能可暂停为期10天的新存款,但不会影响任何已打开的存款。为了安全起见,触发任何tBTC系统更新的过程,需要钱包团队3名技术成员中的2名,通过手动的方式创建以太坊交易,然后使用气隙系统签署交易信息,最后将交易和签名提交到以太坊区块链。而该过程是在UTC时间5:45完成的。

在第二天早晨(东部时间),Keep团队意识到,尽管托管dApp的登录页面已重定向至tBTC主页,但托管dApp上的其他页面(例如特定的存款页面),却没有这个操作。与其冒着任何现有存款无意中触发赎回错误的风险,还不如将托管dApp的其余页面重定向到UTC时间14:11的tBTC主页。

技术问题描述

问题本身的根源,在于证明赎回交易事实上已在比特币区块链上进行的过程。在正常情况下,为比特币交易提供有效签名的签名者,可能会立即释放保证金,让赎回者负责在比特币区块链上广播该交易。然而,如果tBTC系统在这一阶段解除了签名者的经济义务,则签名者将有机会进行作恶。

因此,tBTC系统仅在签名者出示有效签名并证明交易在比特币区块链上被接受后,才会释放签名者保证金。

证明在比特币区块链上的赎回交易已得到充分确认的证据,适用于一些健全性检查。其中之一是验证比特币交易是否将签名者共同控制的资金,发送到请求的赎回地址。这些检查由redemptionTransactionChecks?函数执行的:

function?redemptionTransactionChecks( ????DepositUtils.Deposit?storage?_d, ????bytes?memory?_txInputVector, ????bytes?memory?_txOutputVector )?public?view?returns?(uint256)?{ ????require( ????????_txInputVector.validateVin(), ????????"invalid?input?vector?provided" ????); ????require( ????????_txOutputVector.validateVout(), ????????"invalid?output?vector?provided" ????); ????bytes?memory?_input?= ????????_txInputVector.slice(1,?_txInputVector.length-1); ????bytes?memory?_output?= ????????_txOutputVector.slice(1,?_txOutputVector.length-1); ????require( ????????keccak256(_input.extractOutpoint())?== ????????????keccak256(_d.utxoOutpoint), ????????"Tx?spends?the?wrong?UTXO" ????); ????require( ????????keccak256(_output.slice(8,?3).concat(_output.extractHash()))?== ????????????keccak256(abi.encodePacked(_d.redeemerOutputScript)), ????????"Tx?sends?value?to?wrong?pubkeyhash" ????); ????return?(uint256(_output.extractValue())); }

而观察到的错误是在最后一次检查中,“Tx sends value to wrong pubkeyhash” 。

require( ????keccak256(_output.slice(8,?3).concat(_output.extractHash()))?== ????????keccak256(abi.encodePacked(_d.redeemerOutputScript)), ????"Tx?sends?value?to?wrong?pubkeyhash" );

比特币有几种类型的输出脚本。最常见的类型有:pay to pubkeyhash (p2pkh)、pay to scripthash (p2sh)、pay to witness pubkeyhash (p2wpkh)以及pay to witness scripthash (p2wsh),我们将这些称为标准输出类型。地址代表一个20或32字节的哈希、一个校验以及有关输出脚本类型的信息。 类型信息用于将哈希插入标准模板,这将创建相应的输出脚本。

输出脚本的长度不同。所有输出脚本都以1字节作为前缀,例如,标准的p2pkh输出脚本的长度为25字节,或26字节(计算其长度前缀)。输出的值表示为8字节的小端(little-endian)整数,在输出脚本之前立即序列化。因此,标准输出的长度在(8 +1 + 22 =)31到(8 +1 + 34 =)43个字节之间。

BTCUtils.extractHash()从标准输出中提取哈希。它通过检查输出脚本的前缀和后缀来确定哈希的位置。如果输出脚本是非标准的,则返回空的bytearray(字节数组)。

我们已经可以看到一些模式。所有旧类型脚本都有后缀,而所有见证类型(witness)都没有。除p2pkh以外的所有类型脚本都将在输出的第十个字节开始哈希,该字节位于索引:

(_output.slice(8,?3).concat(_output.extractHash()))

该表达式占用字节8、9和10,并连接哈希。对于见证(witness)类型,字节8是长度前缀,而字节9和10是模板前缀,因此很容易看出,将它们连接到哈希将产生(长度前缀)输出脚本。但是,对于p2sh地址,此表达式不会附加模板后缀。对于p2pkh地址,它将仅提取前缀的2个字节,并且(同样)不附加后缀。这意味着表达式会修改旧输出脚本,并且永远不会输出有效的旧输出脚本。

bytes?memory?_modifiedLegacyOutputScript?= ????(_output.slice(8,?3).concat(_output.extractHash())); require( ????keccak256(_modifiedLegacyOutputScript)?== ????????keccak256(abi.encodePacked(_d.redeemerOutputScript)) );

此代码等效于已部署的代码。在意外修改旧版脚本之后,它会将它们与未修改的旧版输出脚本进行比较。当?_d.redeemerOutputScript是旧版脚本时,此等式将始终失败,并且交易将始终被还原。

这个错误既不会损害赎回者,也不会损害存款者的利益,也就是说,用户的资金是安全的。实际上,由于此代码验证了赎回证明,因此它只在赎回者收到BTC后运行。

但是,由于系统无法验证赎回是否成功,签名者保证金可以像赎回失败一样被扣押。特别是,如果在签名者提供该笔交易的签名后6小时,尚未证明存款合约发生了赎回交易,则赎回人可通知合约赎回证明已超时。

而赎回证明超时通知,会被视为签名者中止的信号,这意味着签名者未满足系统要求,但系统不将其视为恶意。在赎回期间,这意味着系统无法验证赎回人是否收到了他们的资金。在这种情况下,系统会没收签名者的保证金,并将全部保证金作为补偿发送给赎回者。采取这种方法是为了防止以下情况:签名者在请求的赎回交易上生成签名,但又合谋在另一笔交易上生成签名,然后在确认正确的交易之前竞相确认其交易。

在正常情况下,产生不良交易的签名者,可能会遭受惩罚,但这次发生的情况,显然并不是正常的。

tBTC的代码是如何过审的?

tBTC的初始设计将赎回限制为p2wpkh地址,并在赎回过程中强制执行此限制。但在2月初,Keep团队的工程主管Antonio提出了一项更改,放宽了赎回交易允许的输出脚本(不仅仅是p2wpkh)。在赎回期间,这是为了允许存款接受任意比特币输出脚本,让赎回者能够灵活地接受他们喜欢使用的任何钱包。而有问题的代码在更改之后被保留了下来。

上面的commit消息指出:“结果通过了所有当前测试,但repo中尚未对非p2wpkh输出脚本进行测试。”

这一点在接下来的几个月内没有改变。

其他观察

上面的问题不是赎回代码中存在的唯一问题,实际上,即使证明代码正确无误,由于有关OP_VERIF和OP_VERNOTIF的共识规则,恶意的赎回者仍可能会指定一个产生无效比特币交易的输出脚本。这将迫使交易永远不会包含在比特币区块中。在这种情况下,能够确认交易的输出脚本是无关紧要的,赎回方将能够保证收到保证金(同时将BTC留给签名者)。也就是说,除了赎回后的错误验证外,在请求赎回时还缺少验证。因此,将来的版本必须只支持标准地址类型。

值得注意的是,如果赎回者指定了将导致无效比特币交易的输出脚本,他们就能够拿到签名者的保证金,而签名者则只能控制BTC存款,但由于签名者保证金150%的超额抵押,这意味着恶意赎回者仍将从这种情况中获益。

这个错误的验证代码也存在于无效的代码路径中,其在错误修正PR中已被删除。

总结错误

最终,总结下这次事件当中的几个问题:

首先,Keep团队未能在新commit提出之后进行更多的测试。因此,团队错过了在开发过程中抓住这个问题的机会。

在基于dApp的手动质量检查过程中,Keep团队没有验证UI中的成功兑换是否导致了链上的关闭存款。结果导致团队错过了在手动质量检查过程中发现问题的机会。

Keep团队没有在赎回的入口点充分考虑输入验证。这是系统中相对较少的完全由用户控制的数据片段之一,因此应该是输入验证的首要考虑因素。

Keep团队没有花费足够的时间为单元测试生成比特币测试向量。

Keep团队已经做了什么?

James Prestwich已在GitHub上发布了一个PR,并提供了建议的修复程序。 在合并该修复程序之前,Keep团队会在接下来的几天进行测试。

Keep团队已调整了计划中的Trail of Bits审计范围;

Keep团队已将发现的问题及修复方案与其之前的审计方ConsenSys Diligence和现在的审计方Trail of Bits进行了沟通,以确认问题,并让他们提供进一步的反馈。

通过提供1.005 BTC-1 TBTC的比率来兑换未赎回的TBTC,Keep团队恢复了99.83%的TBTC供应,他们将使用有抵押的TBTC赎回未结存款,并释放抵押的ETH。

接下来要做什么?

除了要进行技术和流程改进之外,在未来的几天,KEEP团队还将宣布如何重部署tBTC系统。

——————————————————————————————————————————————————————————————————————

以上就是Keep团队对这次漏洞事件的完整解释,对此,比特币代码维护者Pieter Wuille也和相关人员进行了讨论,并确认了问题所在。

此外,也有人建议Keep团队与经验丰富的比特币开发者接触,让他们对比特币的逻辑部分进行双重甚至三重审核,而仅仅是以太坊合约部分的审核是不够的。

而这一建议,也得到了Keep创始人的肯定。

下面简单谈谈个人的一些看法:

为了追求最大化的去中心化,tBTC系统的设计毫无疑问是非常复杂的,这导致潜在的安全问题会有很多,这次的事件并不让人意外;

短期内,愿意去尝试tBTC的用户其实非常少,其正式在以太坊主网上线后,只有3名签名者完成了注册步骤,这与高门槛及协议处于初期阶段有关;

系统确实很去中心化,关于问题的报告也写得非常详细;

短期内,一些较中心化的比特币锚定币(例如WBTC)会发展得较快,tBTC需要时间和更多的努力来证明自己;

—-

编译者/作者:巴比特资讯

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

LOADING...
LOADING...