日蚀攻击
日蚀攻击(Eclipse Attack)是BTC中的一种攻击手段,最早在2015年于《Eclipse Attacks on Bitcoin's Peer-to-Peer Network》(比特币的点对点网络中的日蚀攻击)一文中被提出。现在在比特币和以太坊中均存在此类攻击。
概念:
- 攻击者通过攻击手段使得受害者不能从网络中的其它部分接收正确的信息,而只能接收由攻击者操纵的信息,从而控制特定节点对信息的访问。
- 日蚀攻击的手段并不是需要我们所常见的需要全网51%的算力,而是表现是对特定节点或节点集群(目标受害节点以及将要连接目标受害节点的节点)进行网络攻击,让网络出现“分区”,以此来达到双花的目的。
但实际上,比特币的安全算力阈值并非50%而是33%,原因在于通过“自私挖矿”,矿工通过有意识地隐藏发掘出的区块,可以将对网络发起攻击的门槛由50%降低到33%。
攻击方法:
- 日蚀攻击是对区块链的一种网络级攻击,属于p2p网络中的一种攻击。
- 攻击者通过某种方法将目标受害节点连接到自己控制的节点,并使用攻击节点连接器输入节点(全部连满)。而我们知道,在比特币节点的本地中存有新、旧两张节点地址的表格。而区块链节点每次建立的输出连接均是在这两张表中寻找到最新的一个节点。而攻击者要做的就是不断的与该节点进行连接,以便不断刷新表格以达到控制的目的。
- 攻击者使用DDos使目标节点宕机并重启,这样就控制了该节点的输入输出,也就是变相地控制了该节点。
概述
实际上,在比特币系统中,由于网络带宽限制和算力分布限制,比特币系统是限制了单个节点可以接受的信息以及主动链接其他节点的数量上限。而我们知道,在比特币节点的本地中存有新、旧两张节点地址的表格。而区块链节点每次建立的输出连接均是在这两张表中寻找到最新的一个节点。而攻击者要做的就是不断的与该节点进行连接,以便不断刷新表格以达到控制的目的。
如果一个节点所接收信息的117个节点和对外链接的8个节点全部都是由恶意节点操控的话,相当于该节点被恶意者所孤立,其所有接受的信息都受到攻击者控制,这种情况我们便称该节点遭受了“日蚀攻击”,看起来和传统安全领域的中间人攻击类似。
如果恶意者可以控制更多的节点,对更多的正常节点发起日蚀攻击,那么恶意者将可以把比特币网络拆分为两个不同的分区,就像分叉一样。
-
假如恶意者掌握了足够的带宽资源,且自身拥有全网40%的算力,它可以将比特币网络拆分为两个分区,假设两个分区各占有全网30%的算力,恶意者不仅可以通过隔离两个分区之间的信息沟通,并且还能依靠这40%的算力在两个分区中都发起51%攻击(恶意者在每个分区的算力占比都是4/7)。
如果恶意者只有网络带宽资源而没有算力,依然可以实现双花攻击。假设恶意者通过日蚀攻击将网络拆分为两个分区,一个分区占有20%算力而另一个占有80%算力,此时恶意者在先在20%算力分区发起一次交易,同时也在80%分区发起一次转账给自己的交易。
2. 在日蚀攻击的影响下,两个分区无法沟通,故只能依据自身所在分区的算力进行挖矿,由于80%算力分区占有绝对算力优势,其区块高度早晚会超过20%算力分区,当恶意者解除日蚀攻击后,首先发送在20%分区上的交易将会因为最长链原则被废弃,而80%分区上的双花交易则会被接受。恶意者通过日蚀攻击完成了无需算力占比的双花攻击。
当然,日蚀攻击的出现原因在于点对点网络和工作量证明机制,想要从根源上断绝日蚀攻击的风险就必须得从网络结构上下手,这难免会令区块链丧失去中心化等特点。不过,只要通过有效手段提高攻击门槛,并以合理的经济机制激励节点,日蚀攻击对于大多数区块链而言是可以预防的。
平台应用情况
我们知道,比特币设计之初只能生产8个输出的TCP连接,而以太坊需要13个。同时以太坊使用了点对点加密的安全通道,比特币却没有。所以我们一定会下意识的以为以太坊在日蚀攻击中肯定是比比特币平台安全的。但是事实不是这样。
对于比特币来说,攻击者可以控制足够数量的 IP 地址来垄断所有受害节点之间的有效连接。然后攻击者可以征用受害者的挖掘能力,并用它来攻击区块链的一致性算法或用于 “重复支付和私自挖矿”。
对于以太坊,攻击者可以垄断受害节点所有的输入和输出连接,从而将受害节点与网络中其他正常节点隔离开来。然后攻击者日食攻击可以诱骗受害者查看不正确的以太网交易细节,诱骗卖家在交易其实还没有完成的情况下将物品交给给攻击者。
然而以太坊的点对点网络中的节点由其公钥所标识。也就是说,以太坊的版本允许用户在同一个IP下运行无限数量的节点,每个节点都有一个不同的公钥。而比特币相对于的一个IP地址只能对应一个节点。 这也就是为何曾经国外实验室使用了两天PC机就可以模拟日蚀攻击的原因。
如何发起日蚀攻击
由于每种区块链底层的 P2P 网络模型可能不一样,所以就以以太坊为例来做说明。
以太坊Kedemlia网络原理
以太坊底层的 P2P 网络采用的是 kademlia 算法,kademlia 网络是一种结构化的 P2P 网络,网络中的节点按照一定的规则组织在一起。
kademlia 算法中的核心特点是用异或来定义两个节点的距离,这种距离与实际的物理距离没有任何关系。
每个节点的路由表会保存不同距离的节点,这个距离的最小值当然是 0,也就是它自己,这个距离的最大值跟节点 ID 的长度有关系(NodeID)。NodeID 是一段具有特定长度的字符串,每个节点具有唯一的 NodeID,用 NodeID 来作为这个节点在 P2P 网络中的身份信息。比如:
NodeID: DEA25B0AF6CC5EA9DA4961DBC5FFEB97
假设 NodeID 长度为 N bit(对于上面的 NodeID 来说 N 为 32 * 8 = 256,那么这个距离的最大值就是 N -1,即距离范围在 [0, N)。
kademlia 网络中的节点的路由表中会保存每一个距离的节点,数量在 1 个以上,这个值称为为 α 值。也就是说与自己的 NodeID 距离为 1 的会保存 α 个,距离为 2 的会保存 α 个,以此类推,直到距离为 N-1 的节点会保存 α 个。实际情况是距离越大,能找到符合要求的节点的概率也就越大.
这里每一个距离称为一个 Bucket,每一个 Bucket 里保存着 [0, α] 个距离匹配的节点。
通过上述对 kademlia 算法的简要描述可以得出一个结论,知道一个节点的 NodeID,就能够计算出这个节点的路由表中的每一个 Bucket 中应该填入什么样的 NodeID。而这就是日蚀攻击的核心依据。
如何防御日蚀攻击
根据上述的讨论知道,想要避免日蚀攻击由很多种办法:(可能只适用于 kademlia 网络)
-
提高节点进入网络的准入门槛 节点进入 P2P 网络需要一定的门槛,不论是以时间为代价还是以 Stake 或是工作证明为代价,这样就能有效防止大批量伪造节点进入网络,从而从源头上避免日蚀攻击
-
针对同一个 IP 段的节点做连接限制 攻击者很可能利用有限的 IP (1 ~ 2 个)伪造大量节点,发起日蚀攻击,那么对于目标节点来说,看到的节点都是来自于这个 IP,多半可以说明这是恶意节点。那么只需要对来自同一个 IP 段的节点做一定数量的限制,比如 最多 2 个,那么也能显著提高攻击者的攻击成本
-
对节点主动建立连接和被动建立连接的数量做一定的均衡 发起日蚀攻击,需要主动占满目标节点的路由表,也就是主动与目标节点建立连接,即便不是这样,也有方法让目标节点主动建立连接到恶意节点。所以对于进出的连接数,做一定的均衡能有效避免日蚀攻击的发生
-
NodeID 重启之后变化 进行日蚀攻击的前提是需要知道目标节点的 NodeID,并且迫使目标节点重启,然后以事先根据目标节点 NodeID 计算好的伪造节点发起连接,达成日蚀攻击。所以如果节点重启之后 NodeID 变为与原来不再一致,那么攻击者事先计算伪造的节点就变得毫无可用之地了。
-
其他辅助措施
比如节点实时检测,发现恶意节点进行广播,并采取一定的惩罚措施等
DDos攻击
概念:
DDoS(Distributed Denial of Service),是分布式拒绝服务攻击的简称,指攻击者通过控制不同位置的多台机器,并利用这些机器对受害者实施攻击。
想要进行DDos攻击,大概会进行这三个步骤:
-
了解攻击目标:假设攻击目标是一个网站,需要先了解有多少台主机连接到这个网站的站点,以及这个站点的配置信息。
-
找到一些傀儡机,在傀儡机上安装相应的攻击程序,而这个攻击程序是攻击者可以在主控机上控制的。这样小就可以通过主控机和傀儡机去攻击目标网站。
在区块链中,DDoS 攻击的主要目的是大量占用网络中的节点资源,使得这些节点无法提供正常的服务,如果受害的节点过多,很可能会影响整个区块链网络的运行。
攻击者可以向网络节点发送大量的虚假信息,进而让这些虚假信息都访问受害者节点,造成受害者节点压力过大。比如,一些虚假的索引信息,会影响到同步区块的速度等。
有几种不同类型的 DDoS 攻击。一般来说,DDoS 攻击分为三大类:容量耗尽攻击、协议攻击和资源层攻击。
- 容量耗尽攻击通过最初似乎是合法的流量来使网络层不堪重负。这种类型的攻击是最常见的 DDoS 攻击形式。DNS(域名服务器)放大就是一种容量耗尽攻击,它使用开放的 DNS 服务器向目标发送海量 DNS 响应流量。
- 协议攻击通过利用第 3 层和第 4 层协议堆栈中的弱点导致服务中断。SYN 攻击就是此类攻击的一个示例,该攻击会消耗所有可用服务器资源(从而使服务器不可用)。
- 资源(或应用程序)层攻击针对 Web 应用程序数据包并破坏主机之间的数据传输。此类攻击的示例包括 HTTP 协议违规、SQL 注入、跨站脚本和其他第 7 层攻击。
网络攻击者可能对一个网络使用一种或多种类型的攻击。例如,攻击可能从一类攻击开始,然后演变为另一种威胁或与其相结合,进而对系统造成严重破坏。
此外,每个类别中都包含多种网络攻击。随着网络犯罪分子的技术越来越复杂,新网络威胁的数量正在增加,并且预计还会攀升。
如果怀疑网络受到攻击,那么必须快速采取行动,因为除了导致故障外,DDoS 攻击还会使组织容易受到其他黑客、恶意软件或网络威胁的攻击。
以太坊智能合约中的DDos攻击
利用非预期的回滚的 DoS(DoS with (Unexpected) revert)
- 依赖外部调用状态
- 权限操作
依赖外部调用状态
在以太坊智能合约中,DoS 漏洞可以简单理解为“不可恢复的恶意操纵或不受控制的无限资源消耗”,即对以太坊合约进行 DoS 攻击,可能导致大量消耗 Ether 和 Gas,甚至导致异常的合约逻辑。
contract Auction {
address currentLeader;
uint highestBid;
function bid() payable {
require(msg.value > highestBid);
require(currentLeader.send(highestBid)); // Refund the old leader, if it fails then revert
currentLeader = msg.sender;
highestBid = msg.value;
}
}
这个合约存在一个潜在的漏洞,即在竞拍结束后,原来的领先者可以通过发送一个比当前最高出价低的出价来触发退款操作,从而获取之前出价的以太币。 具体来说,假设当前领先者为地址 A,最高出价为 x 个以太币。此时,另一个地址 B 发送了一个比 x 小的出价 y(y < x),由于 y 不大于当前最高出价,因此该出价会被拒绝。然而,在这个过程中,地址 A 的 send 操作会被触发,并将之前的最高出价 x 退回给地址 A。因此,地址 A 可以通过不断发送比当前最高出价低的出价来获取之前所有人所支付的以太币。
address[] private refundAddresses;
mapping (address => uint) public refunds;
// bad
function refundAll() public {
for(uint x; x < refundAddresses.length; x++) { // arbitrary length iteration based on how many addresses participated
require(refundAddresses[x].send(refunds[refundAddresses[x]])) // doubly bad, now a single failure on send will hold up all funds
}
}
这个合约存在两个潜在的漏洞。
首先,refundAll 函数中的循环是基于参与竞拍的地址数量进行的,这意味着如果有大量地址参与了竞拍,循环次数可能会非常大,从而导致交易失败。因此,在实际应用中,应该避免使用基于数组长度的循环方式。
其次,在 refundAll 函数中,对于每个参与竞拍的地址,都会调用 send 函数将其所支付的以太币退回。然而,在实际应用中,如果有一个地址的退款操作失败了(例如由于余额不足或者其他原因),那么整个函数就会停止执行,并且之前已经成功退款的地址也无法收到他们所支付的以太币。
因此,在实际应用中,应该避免在循环中使用 send 函数,并且应该使用更加安全可靠的方式来处理退款操作。 为了解决这些问题,可以考虑将所有需要退款的地址和对应金额存储在一个数组或映射表中,并在需要时逐一处理每个地址。同时,在处理每个地址时,可以使用 transfer 函数来进行转账操作,并且在转账失败时使用异常机制进行处理。例如:
address[] private refundAddresses;
mapping (address => uint) public refunds;
function refundAll() public {
for(uint x; x < refundAddresses.length; x++) {
address payable recipient = payable(refundAddresses[x]);
uint amount = refunds[recipient];
refunds[recipient] = 0;
if(amount > 0) {
bool success = recipient.send(amount);
if(!success) {
refunds[recipient] = amount;
}
}
}
}
权限操作
在智能合约中,有一些特权的地址是很常见的,比如 owner 地址,它负责管理合约的参数调整、紧急关停等敏感操作。如果 owner 地址丢失或无法正常工作,则会导致整个合约无法运行,从而导致非主观的 DoS 攻击。
bool public isFinalized = false;
address public owner; // Contract owner
function finalize() public {
require(msg.sender == owner);
isFinalized == true;
}
// ... Some extra ICO features
// Rewrite the transfer function to check isFinalized
function transfer(address _to, uint _value) returns (bool) {
require(isFinalized);
super.transfer(_to,_value)
}
这段合约代码及其的依赖 owner 地址。在 ICO 结束后,如果 owner 地址的私钥丢失,则无法调用 Finalize() 函数启动 transfer 功能,用户也就无法进行 token 转移,合同也就无法如期的正常工作了。
当然还有其他漏洞,这里就看权限控制方面的。
利用区块 Gas Limit 的 DoS(DoS with Block Gas Limit)
每个区块都有可以消耗的 gas 上限(Gas Limit),这是Block Gas Limit。如果消耗的 gas 超过此限制,交易将失败。这导致了几个可能的拒绝服务攻击面:
-
不受控制的操作在合约层进行的 Gas Limit DoS(Gas Limit DoS on a Contract via Unbounded Operations)
-
通过区块填充在网络层进行的Gas Limit DoS(Gas Limit DoS on the Network via Block Stuffing)
不受控制的操作在合约层进行的 Gas Limit DoS
攻击者可能会添加大量的地址到 refundAddresses 中,每个地址只支付很少的以太(比如添加地址的条件是转入的 Ether 大于0,那么最低可以每个地址只花 1 Wei 的成本)。那么最终,进行退款时由于需要给大量地址进行退款,因此它的 gas 成本很可能超过了Block Gas Limit,从而阻止交易的执行。
Block Stuffing 攻击是指攻击者在自己的交易中包含大量的无效数据(通常是随机的或者与以太坊网络无关的数据),以便增加区块的大小和复杂度,从而导致整个以太坊网络的拥堵和延迟。但block stuffing是在交易层进行的攻击,而不是合约层,合约层的攻击可以参考Reentrancy。
这种攻击的主要目的是通过使网络拥塞来阻止其他用户进行交易,从而影响以太坊网络的正常运行。攻击者通常会选择在高峰交易时段进行攻击,以使网络更容易受到影响。
如何检测和响应 DDoS 攻击
虽然没有一种方法可以检测 DDoS 攻击,但有一些迹象可能提示网络正受到攻击:
- 看到网络流量激增但来源不明,这些流量实际来自同一个 IP 地址或范围。
- 网络性能缓慢或异常。
- 网站、在线商店或其他服务完全离线。
流量攻击识别主要有以下2种方法:
- Ping测试:若发现Ping超时或丢包严重,则可能遭受攻击,若发现相同交换机上的服务器也无法访问,基本可以确定为流量攻击。测试前提是受害主机到服务器间的ICMP协议没有被路由器和防火墙等设备屏蔽;("ping"是一种网络工具,用于测试主机之间的网络连接和延迟。它通过发送一个特定类型的网络数据包,称为"Internet Control Message Protocol (ICMP)"数据包,来测试目标主机是否能够接收和响应数据包。)
2) Telnet测试:其显著特征是远程终端连接服务器失败,相对流量攻击,资源耗尽攻击易判断,若网站访问突然非常缓慢或无法访问,但可Ping通,则很可能遭受攻击,若在服务器上用Netstat-na命令观察到大量 SYN_RECEIVED、 TIME_WAIT, FIN_ WAIT_1等状态,而EASTBLISHED很少,可判定为资源耗尽攻击,特征是受害主机Ping不通或丢包严重而Ping相同交换机上的服务器正常,则原因是攻击导致系统内核或应用程序CPU利用率达100%无法回应Ping命令,但因仍有带宽,可ping通相同交换机上主机。
应对方法
pull payment system
外部调用可能会意外或故意的失败,因此,为了最大限度地减少此类故障造成的损害,通常最好将每个外部调用隔离到它自己的交易中,该交易可以由调用的接收者启动。尤其是在支付相关的场景,最好让用户自己提取资金而不是自动将资金支付给他们(这也减少了gas limit 出现问题的可能性)。避免在单个交易中合并多个转账操作。
示例:
// bad
contract auction {
address highestBidder;
uint highestBid;
function bid() payable {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
(bool success, ) = highestBidder.call.value(highestBid)("");
require(success); // if this call consistently fails, no one else can bid
}
highestBidder = msg.sender;
highestBid = msg.value;
}
}
// good
contract auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
function bid() payable external {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
refunds[highestBidder] += highestBid; // record the refund that this user can claim
}
highestBidder = msg.sender;
highestBid = msg.value;
}
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)("");
require(success);
}
}
避免单点故障
对于权限操作引起的 DoS 攻击来说,最主要的就是它出现了单点故障,从而导致整个系统无法正常工作。一般来说解决思路有两个:
- 特权地址不要单纯使用外部拥有地址(EOA),而使用多签钱包地址或 DAO 地址来代替。
- 预留备用方案,来避免单点故障。