比特币DoS漏洞为何如此严重?
上周,bitcoin core 0.16.3 版本客户端的突然发布,以及开发者敦促大家尽快升级一事,令比特币世界的人们感到了惊讶。表面上的原因,在于0.14-0.16.2版本客户端中存在一个拒绝服务 (DoS) 向量需要被修补。到后来,我们才发现,在0.15-0.16.2版本core客户端中的另一个漏洞,可能会引起比特币的超发问题。
在这篇文章中,作者试图说明:到底发生了什么?潜在的危险是什么?以及如果有人利用这个漏洞,还将会发生什么?
双重支付的两种方式
在我们接触实际的漏洞之前,我们需要解释一些东西。我们首先需要定义一下双重支付,因为这个漏洞就可以用于双重支付。
所谓双重支付的情况,就比如说爱丽丝(Alice)向鲍勃(Bob)支付了一笔币,然后她又把相同的币再一次支付给了查利(Charlie),爱丽丝基本上试图进行两次支付,其中的一笔她知道会被拒回。当然,当我们考虑支付时,爱丽丝的某些账户通过写这两次支付被透支了。这很接近比特币的工作原理,但并不是十分准确。
比特币并不是基于帐户模型的,而是基于未花费交易输出(UTXO)。一笔交易的输出基本包含了一个地址以及数量。一旦输出被使用了,它就无法再次被花费。试想一下一个UTXO(作为一笔发送给你的币),它可以是任意数量的,比如说0.413 BTC。
比特币的双重支付意味着一笔币(UTXO)被花费了两次。通常,这意味着爱丽丝将她的0.413 BTC发送给了鲍勃,然后她又把同一笔比特币又发送给了查利。
比特币的解决方法是,其中一笔交易会纳入一个区块,由此来决定实际谁得到了报酬。如果两笔交易不知何故都传递到了多个区块,那么后面发生的区块,就会被软件给拒绝掉。如果两笔交易都在同一个区块当中,那么这个区块也会遭到软件的拒绝。
基本上,比特币软件会检测到双重支付行为,如果有双重支付行为的发生,则应该拒绝掉相应的区块。
然而,在两笔不同的交易中发送同一个UTXO,并不是唯一的双花方法。实际还存在着同一UTXO在同一交易进行双重支付的病态情况。在这种情况下,爱丽丝向鲍勃发送同一笔币两次。所以,爱丽丝实际支付的是0.413 BTC,但鲍勃收到的却是0.826 BTC。这显然不是一个有效的交易,因为只有一笔价值0.413 BTC的UTXO 是被发送的。这就相当于,爱丽丝用同一10美元向鲍勃发送了两次,而鲍勃收到的则是20美元。
定义漏洞
因此,总结一下我们所定义的两种类型的双重支付尝试:
使用两笔或更多的交易,来花费相同的UTXO;
使用一笔交易花费同一UTXO多次;
结果表明,Bitcoin Core 软件正确地处理了第一个问题,而第二个问题,正是我们要关心的。任何人都可以像这样构造出一笔双花交易,但要让节点接受这种交易,又是另一回事了。
目前有两种方法可以让交易被纳入一个区块当中: A. 支付足够的费用,将交易广播到网络上,那么矿工会负责把交易纳入区块当中;
B. 作为一名矿工,把交易纳入一个区块;
(A) 除了创建交易,并将其广播到网络上的节点之外,你不需要做太多的工作。 (B) 需要你找到足够的工作量证明。这也是这次漏洞的关键。
(A) 不是一个可能的攻击向量,因为这些交易会立即被标记为无效的,网络上的节点会拒绝它们。没有矿工们的合作,这种交易就无法进入矿工们的记忆库,因为它们不会得到传播。
(B)是漏洞显现的唯一情况。换句话说,想要利用这个漏洞,你就需要工作量证明,或者说足够的矿机设备和电力。
为了明确起见,双花交易有4种情况需要处理:
1A?— 多笔 mempool交易花费了同一UTXO ;
1B?— 多笔区块交易花费了同一UTXO ;
2A?— 单笔mempool交易花费了同一UTXO多次;
2B?— 单笔区块交易花费了同一UTXO多次;
该漏洞有两种表现形式。在0.14.x版本客户端中,存在着一个拒绝服务(DoS)的漏洞,而在0.15.x - 0.16.2版本的客户端,则存在一个超发漏洞。接下来,我们会分别分析它们。
拒绝服务攻击
故事始于2009年的Bitcoin 0.1版本客户端,这一版本的代码通过拒绝案例1B和案例2B(检查区块没有双重支付)来强制达成共识。
你可以看到“检查冲突”的注释,其代码负责检查每个输入没有被花费。“将输出标记为已使用”注释下面的代码,标记了UTXO的使用。如果任何UTXO的花费超过一次,则会导致错误。
在2011年,PR 443被合并到了比特币代码库。这一改变是为了处理通过mempool (上面的情况2A)传输单笔交易双重支付的情况。这个合并请求注释的目的非常明确:
“而且,没有具有重复输入的交易会被纳入区块当中。..。..几个星期前,有人尝试过了,但这些交易并没有被纳入区块。我假设某个地方存在了一个检查关,它会阻止这些重复交易进入区块,虽然我没有对这个问题进行任何挖掘。这实际上是为了防止这种明显无效的交易得到中继。”
实际的代码更改,或多或少与上面的ConnectInputs 中的“检查冲突”注释下的代码执行了相同的操作,但位于的是不同的位置。代码更改是在CheckTransaction 中运行的,其负责了所有上述的4种情况(1A, 1B, 2A, 2B)。因此,我们在区块双重支付共识代码中有了一些冗余,正如案例1B和2B都被检查了两次,其中一次检查是在CheckTransaction,另一次则发生在ConnectInputs。
到了2013年,PR 2224被纳入了比特币软件。这一改变的目的是区分共识错误(例如双重支付)和系统错误(例如磁盘空间耗尽)之间的差别,正如PR注释中所表明的那样:
“它引入了CValidationState,它会存储关于区块的元数据,或者正在执行的交易验证数据。它被用于区分验证错误(例如,未能满足网络规则)和运行时错误(比如磁盘空间的不足),从前这些可能会产生混淆,因磁盘空间用完会导致区块被标记为无效。此外,CValidationState还承担了跟踪 DoS级别的角色(因此它不需要存储于交易或区块当中。..)”
实际的相关代码更改如下:
在那个时候,ConnectInputs已经被模块化成多个方法,并且这个函数成为了检查双重支付的函数。这里的关键改变是,曾经的error被改为了 assert
assert在C++中是做什么的?它会完全中止程序。程序员为什么要在这里停止程序?这就是Pull请求的目的所在。下面就是那个时候的代码片段:
它会像以前一样处理案例1B和2B。函数名则从ConnectInputs更改为ConnectBlock,但检查案例1B和2B的冗余性仍然在PR 443中保留。正如我们已经看到的,UpdateCoins做了第二次双重支付检查。其中CheckBlock通过调用CheckTransaction进行了第一次双重支付检查:
由于这是第二次检查相同的内容,所以要让UpdateCoins的双花检查失败的唯一方法,就是存在某种UTXO数据库或交易存储损坏。事实上,这似乎是改成assert的原因。因为CheckBlock通过CheckTransaction在UpdateCoins之前已经进行了检查,我们已知道某笔交易并不是双花交易。因此,PR 2224正确地推测到,UpdateCoins中的这个状态必然是一个系统错误,而不是一个共识错误。在这种情况下,为了防止进一步的数据损坏,正确的做法就是停止程序。
到了2017年,PR 9049作为Bitcoin 0.14的一部分被引入比特币网络。随着隔离见证(Segwit)的纳入,它是加快区块验证时间的诸多更改的其中之一,其代码更改实际是非常少的:
你可以看到布尔函数 fCheckDuplicateInputs被添加了进去,用于加快区块检查。我们将在下面看到,这是一个被认为是冗余的检查。不幸的是, UpdateCoins中的代码在PR 2224中被更改为系统损坏检查,而不是共识检查。到了0.14.0版本客户端,其代码进行了更多的模块化更改,而assert也发生了一些改变:
曾经是作为一个冗余检查,现在却成了负责区块单笔交易双重支付检查(案例2B),并负责停止程序。从技术上来说,它仍然是强制执行共识规则。只是在中止程序问题上,它表现地非常糟糕。
PR 9049是如何获得通过的? Greg Maxwell 给了我IRC上的聊天记录。
长话短说,开发者们在讨论PR 9049时,倾向于认为区块级单笔交易双重支付(案例2B)会在PR 443处遭到检查,而没有考虑PR 2224。这使得开发者们并没有密切关注PR 9049;
总而言之:
1、在2011年引入用于防止双重支付交易中继(案例2A)的 PR 443,实际产生了一个副作用,即对区块的双重支付共识规则检查创造了冗余校验(案例1B和 2B)。
2、PR 2224是在2013年引入的,作为一种副作用,将(1)中用于区块验证的代码,从冗余升级到了共识层;
3、PR 9049是在2017年被引入的,并且它跳过了(1)中用于单个区块单笔交易双重支付(案例1B)检查的代码。开发人员错误地认为代码是多余的,因为他们没有考虑到(2)。事实上,这种改变跳过了共识的关键部分。
公平地讲,这些事的汇合导致了这次漏洞。
DoS漏洞的严重性
这意味着 0.14.x 版本的Core软件可能会因为一个奇怪的区块而崩溃。而要让软件崩溃,攻击者需要做的事是:
1.创建一笔花费两次同一UTXO的交易;
2.通过足够的工作量证明,将(1)中的交易纳入一个比特币区块;
3.将这个区块广播到0.14.x版本软件的节点;
(1) 和 (3) 的成本并不高,而步骤(2)的最小成本为12.5 BTC。
如果你认为从博弈论的角度来看,分裂网络并不是那么好,那么利用这个漏洞的动机就相当低了。充其量,作为攻击者,你花费了12.5 BTC将部分全节点给搞崩溃。由于不可能从分裂网络中获利,攻击者无法轻易地补偿自己的攻击成本。
如果这是唯一的漏洞,那么攻击者可能给很多人带来一些不便,但这不会是持续的,因为这些被攻击的节点可以简单地重启,并连接到其它诚实节点。一旦有一个较长的链,那么恶意区块攻击就会完全失去它的威胁。除非攻击者以每区块12.5 BTC的代价继续创建区块,并将其传播给 0.14.x版本软件的节点,否则攻击就是不可持续的。
换句话说,虽然这个漏洞的确存在着,但对 DoS攻击的经济刺激却是相当低的。
超发漏洞
从0.15.0版本软件开始,core软件引入了一个新的特性,以便更快地查找和存储UTXO,而这恰恰又引入了另一个漏洞。当一笔具有双重支付单个交易的区块纳入区块链时,软件会将其视为有效,而不会出现崩溃的现象。
这就意味着一笔病理性交易(相同UTXO在同一交易中被使用多次,即案例2B),0.14版本的节点会因此而崩溃,而使用0.15版本软件的节点却会认为交易是有效的,这基本上是凭空在创建比特币。
谈谈它是如何发生的。在0.15中出现的PR 10195 ,引入了很多内容,但它的主要要点在于改变了 UTXO的存储方式,使得它们更有效地进行查找。因此,它出现了很多变化,包括对早期UpdateCoins函数的更改:
注意,assert(false) 周围的代码是如何被完全取出的。注意这一点,0.15.0中的PR 10537也更改了代码。
assert失败的条件现在取决于inputs.SpendCoin,它看起来是这样子的:
本质上,SpendCoin返回“ false”值的唯一方法,就是让币不存在于UTXO集中。但正如你所看到的,这需要币是FRESH的,而不是DIRTY的。这些不是常见的术语,但值得庆幸的是,core开发者Andrew Chow给出了解释:
“现在的问题是,什么时候UTXO会被标记为FRESH?当它们被添加到UTXO数据库时,它们就会被标记为FRESH。但是,UTXO数据库仍然只存在于存储当中的(作为缓存)。当它被保存到磁盘时,存储中的条目将不再被标记为FRESH……”
标记为FRESH的币,是进入交易存储池(memory pool)中的币。而攻击者可以通过 UpdateCoins函数中的assert语句来破坏节点。更糟的是,如果币是属于DIRTY的(基本上从磁盘上读取的),那么这就会导致比特币的超发。
因此,攻击者可以欺骗那些运行 0.15.0- 0.16.2版本软件的矿工接受一个奇怪的、无效的区块,从而导致比特币的供应超发。
超发漏洞的严重性
这种攻击的经济诱因似乎明显高于DoS攻击,因为攻击者可能会凭空制造出比特币。但你仍然需要有挖矿设备来执行攻击,但考虑到潜在的经济诱因,这可能是值得的,或者看起来是这样的。
下面是使用这种漏洞的一种简单攻击方式:
1.创造一笔带有双重支付交易的区块,其会向自己支付两次,比方说 50 BTC →100 BTC;
2.将该区块广播给0.15/0.16版本客户端的所有矿工;
下面是会发生的一些事:
1.0.14.x 版本节点会崩溃;
2.较旧版本的节点及其它替代客户端会拒绝这个区块;
3.很多区块链浏览器是运行在自定义软件上的,而不是基于core,因此,至少有一些浏览器会拒绝该区块,并且不会显示来自该区块的任何交易。
4.取决于矿工们运行的软件,我们可能会迎来链分裂;
有可能,所有的矿工都是运行的Bitcoin Core 0.15+版本软件,在这种情况下,不受攻击的客户端可能会停滞不前。也有可能矿工会运行其它东西,在这种情况下,当他们发现一个区块时,链就会发生分叉。
由于这些违规行为,网络上的人们很快就会追踪到这一点,可能已提醒一些开发人员,并且core开发者已经修复了它。如果存在分叉,那么在那个时候,关于哪条链是正确的共识链,将开始得到讨论,而出现意外超发的链,可能会遭到抛弃。如果真的发生了,那么社区可能会自愿进行一次回滚,以惩罚攻击者。
所以对于攻击者来说,这不会带来50 BTC的收入,更可能的是失去12.5 BTC。如果攻击者加倍花费,比如说200 BTC,那么超发漏洞将持续存在的可能性会更小,因为攻击会更明显。
因此,从攻击者的角度来看,这并不是一种好的获利方式。
攻击者可以获利的另一种方式,就是事先做空比特币,然后再执行攻击。这也是具有风险的,因为攻击不能保证比特币价格会下跌,特别是当危机得到迅速和果断处理时。此外,考虑到大多数交易所提供的杠杠交易,都需要AML/KYC,这可能导致攻击者很快暴露。
攻击者不仅面临着巨大的资金风险,而且还会有身体危险。从经济角度来看,这并不是一个容易获利的漏洞。
当然,有一些别有用心的人,可能会用这种漏洞来吓唬那些比特币持有者。投资回报率将变得更抽象,因此从理论上来讲,这可能会达到这些别有用心者的目的。
结论
毫无疑问,这是一个相当严重的漏洞。尽管我和Awemany之间有着分歧,但我很感激他选择公开地和我辩论。也就是说,考虑到经济博弈理论,我不认为这个漏洞会像他所描述的那样严重。
即使这个漏洞在被发现之前被坏人所利用,攻击者可能也不会选择利用它,因为从经济学上来讲,它是没有意义的。可以肯定的是,这一技术漏洞应该被修复,并且开发者应该做得更好,但真正能够利用这种漏洞的对象其实是非常少的,基本上,只有那些想要摧毁比特币的组织才会这么干。
Bitcoin Core开发者的教训有很多:
1、任何共识变化(即使是微小的变化,例如9049),也需要更多的人进行审查; 2、需要对病理交易进行更多的检查; 3、代码库中哪些检查是冗余的,哪些检查是不冗余的,以及实际代码将要做些什么,需要变得更加清晰; 4、过去存在漏洞,将来也会存在漏洞。现在重要的是,学习并反思这一教训;
币搜:比特币领域的搜索引擎www.btcsearch.com