LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 新闻观点 > 日蚀攻击(Eclipse Attacks):区块链世界的中间人攻击

日蚀攻击(Eclipse Attacks):区块链世界的中间人攻击

2019-07-18 不详 来源:网络
在传统的client-server网路架构中,存在着所谓的中间人攻击:攻击者可以拦截通讯双方的通话并插入新的内容。而在区块链的世界中,也存在着类似的攻击手法,称为日蚀攻击。本文将以波士顿大学团队的一篇论文为基础,对日蚀攻击进行一个基本的介绍。

这篇介绍文预计分成两部分,第一部分(即本文)将会对Ethereum的P2P做一个简单的介绍,如此才能了解日蚀攻击为何能够生效。第二部分将介绍波士顿大学团队论文中关于日蚀攻击的实现原理。

在撰写此篇文章的时候,最新geth版本为1.8.0,而日蚀攻击论文的实验对象为geth 1.6.6 ,本文所描述的各种Ethereum环境,若无特别提及,则视作与日蚀攻击论文一致。

除此之外,Ethereum的重大改版在即,此文中所描述的诸多Ethereum P2P特性都可能面临巨大的改变,请多加注意。

Ethereum的P2P特色

根据日蚀攻击此篇论文的整理,Ethereum的P2P有以下几点特色(A~G):

A. 基于Kademlia设计的P2P协定

Kademlia是一种用于档案共享的结构化P2P协定,其目标为有效率的储存及定位档案。BitTorrent以及eMule等档案共享工具就是基于Kademlia设计而成。

在Kademlia的协定中,每一个档案以及每一个节点都被赋予一个唯一的识别码( ID ),这个ID是以长度为b的位元组所形成(在原始的Kademlia中,b =160)。此ID会用来计算所谓的距离参数( d,distance,个人习惯形容为相异度 ),d值越大表示距离越远(相异度越大),计算方式是对两个ID ( ID 及ID )进行异或运算 ( XOR ),其定义如下:

d = ID ⊕ ID

一个简单范例如下:

ID = 10011...01
ID = 10011...10
ie ID 及ID 的前158个位元皆一致

d = ID ⊕ ID = 10011...01 ⊕ 10011...10 = 00000... 11 = 3

另外,在每一个Kademlia的节点里,包含一种称之为桶(bucket)的资料结构。桶里所储存的是网路中的节点资讯,桶的数量即为识别码的长度( b ),每一个桶里最多会储存k个节点的资讯,因此这种资料结构又被称为k桶( k-bucket ),如下图:

Ethereum沿用了Kademlia的XOR运算及bucket概念,另外在Ethereum的设计中,寻找档案的机制不再需要,只留下更新节点资讯的机制,详细内容可参考下面叙述(E.节点资讯的更新方式) 。

在此篇论文中,作者提到无法理解Ethereum的P2P协定为何要使用Kademlia。

由于Ethereum中的底层资料结构(chaindata等资料),并未分割成不同的小片段去作分散式储存,因此也就无法利用到Kademlia中寻找档案的特性。

个人猜测或许是Ethereum当初在设计开发时,曾经有考虑要将底层资料结构进行分散式储存?此点还待高手协助解惑。

B. ECDSA公钥作为节点ID

Ethereum以ECDSA公钥作为节点ID,其ID长度为512bits。同一台机器(同一个IP)可以运行数个Ethereum节点,亦即一个IP可以产生数个节点ID,这是日蚀攻击能够生效的因素之一。

C. UDP协定与TCP协定负责不同层级的讯息交换

在Ethereum中,P2P资讯的交换是藉由UDP来进行,而区块资讯的交换则是在TCP上进行。

Ethereum UDP所传递的资讯可分为四种:ping,pong,findnode以及neighbor。

· ping and pong: ping讯息用来尝试探知某个节点,若是该节点接受到 ping讯息即会返回 pong讯息。

· findnode and neighbor:findnode讯息会附带一个节点ID(假设为 t ), findnode将会向另一个节点(假设为 x )询问距离 t最近的节点资讯,亦即询问 x的节点列表中距离t最近的节点。x会将这些距离 t最近的节点资讯,以 neighbor讯息返回,最多会返回16笔节点资讯。

为了防御重放攻击(replay attacks),UDP传递的这些资讯,都会加上时间戳记(timestamp),当节点发现某一个UDP讯息其时间比本机时间还老旧(比本机时间早20秒) ,就会丢弃该讯息。

Ethereum的TCP连线可分为两种,一种是由本机节点发出请求给其他节点的outgoing连线,另一种是其他节点发起请求给本机节点的incoming连线。一个Ethereum节点同一时间最多可允许25个TCP连线存在( maxpeers ),而在geth 1.8.0版之前,这25个TCP连线可以全部都是incoming连线,这也是日蚀攻击能够生效的其中一个因素。

D. 储存P2P节点资讯的资料结构

Ethereum有两种储存节点资讯的资料结构,一种用于长期储存,日蚀攻击论文的作者称之为db。db会将节点资讯持久化的储存于硬碟内,因此每次节点重启时,db的资料仍会保存。db内储存了数笔节点的资讯,每一笔资讯包含以下的栏位:

· nodeIP:节点IP
· tcpPort:TCP Port number
· udpPort:UDP Port number
· latestPing:最近一次尝试接触该节点的时间(亦即ping讯息送出的时间)
· latestPong:最近一次该节点回应的时间(亦即收到pong讯息的时间)
· failedTimes:以findnode尝试询问该节点却失败的次数

以上栏位名称并非Ethereum geth原始码中所定义的名称,只是为了方便本文后续说明而定义的名称。

Ethereum本机节点会定期检查db内的每笔节点资讯,若是某节点的latestPong与当下时间间隔已经超过一天,则将该节点从db中移除。

另一种结构是短期储存,称之为table,每次重启节点时,table都会是空的。table含有256个bucket,每个bucket又包含16笔其他节点的资料,每笔资料的栏位如下:

· nodeID:节点ID
· nodeIP:节点IP
· tcpPort:TCP Port number
· udpPort:UDP Port number

bucket内的每笔节点资料都会按照加入顺序而排序,当一个bukcet已经额满却还有新节点资料要加入的时候,本机节点会传送ping讯息给bukcet中最早加入的节点,若是最早加入的节点没有回应pong讯息,则移除最早加入的节点,并将新节点加入,反之则丢弃新节点。

为了决定某一个节点资讯要放到哪一个bucket之中,需配合logdist函式来进行计算,过程如下,其中ID 及ID 是logdist函式的输入参数:

H = SHA3(ID ) ,where ID is the ID of local node
H = SHA3(ID ) ,where ID is the ID of other node
r = similarity(H ,H ) ,where 0 ≤ r ≤ 255
i.e. r is number of most significant bits that H and H are the same

经过上面的计算得到r值后,就可算出节点会被储存在编号为bucket_num (等于256﹣ r )的bucket之内。由于logdist函式的特性,节点在不同bucket_num的分配会呈现高度偏移(highly-skewed)的状况,即任一节点有较高机率被分配到bucket_num较大的bucket中,其机率分布如下式:

P = 1 / ( 2^( r +1) ) ,where 0 ≤ r ≤ 255 , bucket_num = 256﹣ r

根据上式,机率分布图如下:

理论上table结构最大可容纳4096笔节点资料(一个bucket最多容纳16笔,共有256个bucket ) ,但是由于这种高度偏移状况,实际上table大部分的时间只会储存少量的节点资料(大约150~200笔,参考下图,出自日蚀攻击论文)。

E. 节点资讯的更新方式

Ethereum综合了下述的预设节点资讯以及运算模式,以新增或更新其节点资讯纪录( table及db ) :

1.预设节点( Bootstrap nodes)
Ethereum内建(hardcoded)了六笔预设节点资讯。当一个Ethereum节点初始化并第一次启动时,db没有储存任何资料,此时就会用到预设节点资讯。

2.联结更新运算( Bonding)
联结更新运算是本机节点尝试新增或更新某一个远端节点资讯的过程( db以及table ),其具体过程如下:

step1-a:检查该远端节点是否已存在于db中
step1-b:检查failedTimes是否为0
step1-c:检查latestPong是否小于24小时
step2:若是第一个步骤的三个条件都成立,则将该节点存入table中对应的bucket,具体存入过程请参考上述D.储存P2P节点资讯的资料结构。 若是有任一条件不成立,则进到step3。

step3:尝试传送ping讯息给该节点,若收到该节点的pong讯息,则于db加入或更新该节 点资讯,并且将该节点存入table中对应的bucket。

3. ping讯息触发运算( Unsolicited pings )

当本机节点收到一个远端节点传送过来的ping讯息时,除了回应pong讯息给该远端节点,本机节点也会针对该远端节点执行上述的联结更新运算( Bonding )。

4.寻访运算( Lookup )

寻访运算即针对某一个目标节点找寻其最近节点,并且纪录这些最近节点,步骤如下:

step1:给定一个目标节点,称之为t。
step2:从本机节点table中的所有节点中选出16个最接近t (根据logdist函式)的节点,
这16个节点组成"待访节点列表"。
step3:本机分别去访问( findnode )"待访节点列表"的每个节点,每一个被访问的节点会再
各自返回多个(最多16个)更接近t的节点( neighbor )。
step4: step3完成后,本机可得到数个新节点(最多256个),接着对这数个新节点进行联结
更新运算( bonding )。
step5:从step2所选出的16个节点加上step4完成联结更新运算的新节点(最多16+256
个)中再度组成新的待访节点列表,并取代step2原本的待访节点列表。
step6:本机不断重复step2 ~ step5,直到节点资讯不再变化,表示寻访机制完成(当
step5新的待访节点列表与step2原本的待访节点列表相同时,视作不再变化),此
时会将这组最新的待访节点列表放到一个lookup_buffer的资料结构中。

F. 种子运算(Seeding)

种子运算皆由本机自主触发,是以近期之内保持在线上的节点资讯作为更新table资讯的依据,具体行为是将预设节点以及db之中的节点资讯复制到table中,有三种情况会触发种子运算:

· 当Ethereum节点启动时
· 每一小时定期触发
· 寻访运算( Lookup )启动时

种子运算的具体步骤如下:

step1:本机节点检查是否table为空;若为空,才继续执行后续步骤。
step2:本机节点对六笔预设节点执行联结更新运算。
step3:从本机db中,取出latestPong小于或等于120小时的一组节点,若这一组节点的数
量少于30个,则全部保留;若数量多于30个,则随机留下其中30个.接着本机节点针
对这些节点去执行联结更新运算。
step4:本机节点以自己为目标执行寻访运算。

G. Outgoing TCP连线的建立

Ethereum节点会持续建立Outgoing TCP连线,Outgoing TCP连线数最多可达13个(0.5×(1+ maxpeers ))。Ethereum节点会准备两种执行绪:

第一种执行绪是discover_task,此执行绪之任务是以一个随机节点为目标去进行寻访运算以持续更新节点资讯。

第二种执行绪是dial_task,此执行绪之任务用于对某个目标节点建立TCP连线,尝试建立连线前,首先会检查几个条件:

· 该节点不在黑名单内
· 与该节点的TCP连线尚未建立
· 是否正在对该节点进行拨号( dial );若否,则成立
· 是否最近曾对该节点进行拨号;若否,则成立

整体而言,Ethereum会由lookup_buffer (经由寻访运算得出)及table取得节点资讯,再尝试去和这些节点建立TCP连线。

综合上述,可以整理出Ethereum更新节点资讯以及连结节点的过程,如下图:

以上是关于Ethereum P2P的简介,而日蚀攻击便是针对Ethereum建立节点资讯和连结节点的过程进行攻击。


日蚀攻击手法

接下来的叙述中,只要受害目标的所有TCP连线都被攻击者占据,我们都定义为完成日蚀攻击。

A. Monopolizing Connections — 主动占据受害目标(victim)的连线

此攻击的概念就是攻击者主动去占据受害目标的所有TCP连线。这种攻击能够成功的原因,是基于Ethereum节点的几个特性:

· 一个节点的所有TCP连线可以全部是Incoming连线,也就是由其他远端节点发起请求所形成的连线。
· 当一个节点重启时,Outgoing及Incoming的连线数皆为0。
· 当一个节点重启时,需要一段颇长的时间才会建立起Outgoing连线。

根据论文的实验数据显示,在一台2 vCPU及2GB RAM的云端服务器上启动Ethereum节点,要8秒钟之后种子运算才会开始进行,也就是8秒钟之后才会开始尝试进行Outgoing TCP连线。

攻击细节如下:首先,攻击者会产生N个节点ID,N的数量远大于一个Ethereum节点所能允许的最大TCP连线数( maxpeers,预设25个)。接下来,等到受害节点重启之后,立即以这N个节点对受害节点进行Incoming连线。当受害者的全部TCP连线都被攻击者的Incoming连线占据时,即完成日蚀攻击。

论文作者尝试了两个实验,在第一个实验中,攻击者在两台主机上建立了1000个攻击节点,接着重启受害者节点再去攻击。这个实验重复了50次,每次都会将受害者回复到攻击前的状态。最后发现50次攻击里有49次可以完成日蚀攻击。在失败的那一次中,受害者节点成功建立了一条Outgoing连线。

在第二个实验中,论文作者测试了网路延迟(latency)的影响。作者这次将受害者节点安排在距离攻击者节点较远的地方(受害者在新加坡,攻击者则在纽约),接着一样建立1000个攻击节点,最后发现53次的攻击里只有43次会完成日蚀攻击。

根据实验结果,作者认为Ethereum节点的Incoming连线没有限制数量是关键弱点所在,因此建议应该限制Incoming连线数,以保证Ethereum节点能够进行Outgoing连线。

这一个建议已经被Ethereum社群所采纳,在geth 1.8.0版本中, Incoming连线的数量变成是可以限制的,预设上限是maxpeers /3。

B. Table Poisoning — 窜改受害者的节点资讯列表

在第一种攻击中,作者在最后提出了防御补强的建议,假设受害者采纳了这个建议,使得本机能够限制Incoming连线数,那么攻击者是否仍有机会完成日蚀攻击?

在这种前提下,作者设想了另一种可能的攻击方式— Table Poisoning。在Table Poisoning这种攻击手法中,除了主动占据受害者的Incoming连线,攻击者还必须设法侵占受害者的节点资讯列表,使其列表中的大多数节点都是属于攻击者所控制的节点,那么当受害者尝试进行Outgoing连线时,仍然会连接到攻击者的节点,借此达成占据其所有TCP连线的意图,此攻击经由以下几个步骤完成。

首先第一步,由于攻击者知道受害者的节点ID,攻击者便可计算出受害者table的不同bucket中,可能会储存哪些节点资讯( logdist函式)。由于Ethereum 节点储存方式的特性,攻击者并不需要占满受害者的所有bucket,攻击者只要尝试占满受害者的最后n个bucket ( bucket_num从256~256﹣ n ),即可以极高的机率占据其TCP连线。所以攻击者的第一步工作,便是去算出多个匹配bucket_num的攻击用节点ID (以下简称IDattack )。

可预期的是当n越大,需要越多时间去算出能够匹配bucket_num的IDattack。

在本论文中,作者选择n=17作为其bucket攻击数量,总共需要计算出272个IDattack,作者使用一台MacBook Pro进行计算(CPU:i5 2.9 GH;RAM:16 GB),总共需要15分钟去算出272个IDattack。

接下来第二步,在计算出IDattack之后,便是设法让这些IDattack被插入到受害者的db之中。攻击者会藉由这些IDattack发出ping讯息给受害者,受害者收到ping讯息之后,除了回应pong讯息,还会对这些IDattack进行Bonding,如此便可让这些IDattack被插入到受害者的db之中。

在这个过程中,攻击者每24小时便会由这些IDattack发出ping讯息给受害者,并且攻击者也要确实回应受害者的ping讯息(返回pong )以及findnode讯息(攻击者会返回空的neighbor讯息),如此这些IDattack便具备了快速占据受害者table的条件(详细内容请参考Bonding运算)。

第三步是此攻击的最后步骤,设法让受害者节点重新启动。Ethereum节点重新启动时,其table是空的,攻击者在这时会不断的藉由已经产生的IDattack发出ping讯息给受害者,由于在第二步中,我们使这些IDattack具备了快速占据受害者table的条件,此时便可很快的占据受害者table。

Ethereum节点在启动时,UDP监听程序会先运作(此时就可以连接其他节点的Incoming连线,并接受其他节点传来的ping讯息进行Bonding运算),然后才进行种子运算程序,两个程序之间大约间隔一秒。

藉由种子运算的特性,我们可以得知,在table已存有节点资讯的情况下,种子运算并不会发生作用,也就是db中没有任何节点资讯会转移到table中(妨碍种子运算),如此受害者的Outgoing连线将全部连接到攻击者的节点,到这个时候,攻击者只要再配合Monopolizing Connections攻击的方法想办法占据剩余的Incoming连线,就有机会完成日蚀攻击。

table的更新可能是外部触发(Unsolicited pings ,远端节点发出ping给本机)或是自主触发(本机的种子运算)。Table Poisoning的作用是尝试让自主触发失效。

IDattack只是具备快速占据受害者table的条件,并无法完全保证table只储存IDattack。若是有其他诚实节点尝试发出ping讯息给受害者,table中就有可能储存了诚实节点。

针对这种攻击,作者也执行了两种实验。第一个实验,攻击者先准备了一个已经运行33天的受害者(地点在纽约),此受害者的db中储存了25580笔节点资讯,在那个时间点,这已经可以视为Ethereum全网的全部节点数量。接著作者依上述步骤开始进行攻击,第一步和第二步各执行一次,第三步则重复执行51次,每次重复之前都会将受害者主机重新启动。

攻击者准备了两台攻击用主机,一台主机(位于波士顿)以272个IDattack传送ping讯息给受害者,另一台主机(位于纽约)则依照Monopolizing Connections攻击以1000个攻击节点对受害者进行Incoming连线。在这51次的实验里,有49次受害者的Outgoing连线皆被攻击者占据(其中只有19次成功地妨碍种子运算),然而这51次中却只有34次完成日蚀攻击。

为何此次攻击的成功率明显较为低落(比起单纯Monopolizing Connections攻击)?

作者解释到,由于此实验仍是使用旧版geth来进行(在他们实验的时候,尚未发行geth 1.8.0,也就是尚未发行针对第一种攻击进行修正的版本),为了模拟出限制Incoming连线数的效果(也就是25个TCP连线中,一部分必须是属于Outgoing),攻击者一开始不会积极抢占受害者的Incoming连线,直到确认受害者建立了Outgoing连线,才会尝试抢占受害者的Incoming连线,而在这段等待确认Outgoing的时间,受害者可能已经连接了其他诚实节点的Incoming连线,导致日蚀攻击无法完成。

而在第二个实验中,作者测试了网路延迟(latency)的影响。攻击者准备了一个已经运行1小时的受害者(地点在新加玻),此受害者的db中储存了7000笔节点资讯。两台攻击用主机则与第一个实验相同,不再赘述。第二个实验重复了50次,最后有44次完成了日蚀攻击,6次的失败中,有1次是因为受害者建立了Outgoing连线,另外5次则是其他诚实节点建立了Incoming连线。

在实际应用上,Table Poisoning仍有缺陷。为了匹配某个受害者的节点ID去产生攻击用节点ID,攻击者需要耗费可观的运算资源,如果攻击者想攻击多个不同的受害者,就必须产生大量的攻击用节点ID,资源的消耗也会急遽增加。

针对这种资源消耗的缺陷,作者设想了另一种产生攻击用节点ID的方式,称之为建立寻访表( lookup table )。

针对Table Poisoning的攻击手法,作者建议了几种补强方式,大致上可区分为两种策略—改良节点ID的管理方式以及改良种子运算:

1.改良节点ID的管理方式

此策略的目的在于让攻击者无法轻易的算出IDattack,以及让攻击者在同一台机器上(单一个IP)无法操纵多个不同节点ID。

作者建议,应该严格要求同一台机器(同一个IP)只能同时与一个节点ID相关,也就是在table及db的储存结构,以及nieghbor讯息的结构中应该去设定这个限制。此外,为了让攻击者无法轻易的算出IDattack,应该要改变logdist函式的输入参数,而寻访运算也应该改变其运算目标。

2. 改良种子运算

作者建议,即使table不是空的,仍应执行种子运算。此外,当一个节点在重新启动之后,在被动的执行联结更新运算之前(由Unsolicited pings所触发),应优先执行寻访运算。如此以来,就可以尽量避免被攻击用节点ID占据。

在geth 1.8.0版,作者所建议的改良种子运算已经实作。

C. Manipulating Time — 时间操作攻击

这个攻击利用了Ethereum防御重放攻击的机制—若是某个UDP讯息的时间戳记比节点的本机时间还要早20秒以上,该讯息将被丢弃。

攻击者借着操弄NTP等方式,使得受害者的本机时间总是比外界真实时间还要迟缓20秒以上(也就是比其他诚实节点迟缓20秒)。如此一来,受害者将不再理会其他诚实节点传送过来的pong及neighbor讯息,而经过数日之后,由于过期检查机制,受害者的db将会清除掉所有的诚实节点。

另外,由于neighbor讯息都被丢弃,也会让寻访机制失去效用,导致table内的诚实节点都被清除,最后就会使得受害者节点不再储存任何诚实节点的资讯。而相对的,因为受害者也不再理会其他诚实节点传送过来的ping及findnode讯息,在一段时间之后,其他诚实节点也就不会再储存受害者节点的资讯。

依据以上的叙述,作者设计了一个实验。作者准备了一台已经运行34天的受害者节点(在纽约),接着便开始对受害者模拟NTP窜改时间攻击(具体做法是直接改变受害者节点的本机时间),实验进行6次,每一次受害者节点的本机时间都比真实时间延迟许多(6个不同的延迟时间:25秒,70秒,5分钟,7分钟,9分钟,13分钟),整个实验历时数日(从8月17日至9月4日)。实验结果显示,到了攻击第3天,受害者db中纪录的节点数量显著的减少,而且之后便趋于平缓几乎不再增加(从一开始的数万个变成10几个),而table中的数量也有一样的变化趋势。

虽然db及table中的节点数量大幅减少并且几乎不再增加,但是与受害者连接的节点数量却呈现大幅波动,有时可以达到预设上限25个,有时又不到10个,进一步分析之后发现,这些与受害者连接的节点,极大部分不属于geth版本,而是诸如parity,Ethereum(J)等其他Ethereum的实作版本,在实验的最后11天里,作者统计了与受害者连结的节点数量与类别,其中属于geth版本仅有130个,而非geth版本的则高达64374个。

Manipulating Time攻击能够弱化受害者节点,使得受害者几乎无视其他节点,也让其他节点几乎遗忘了受害者节点,这样一来,就能够以更少的资源消耗来执行Table Poisoning攻击。

而针对Manipulating Time攻击,作者建议应该改良防御重放攻击的方式,也就是每一个UDP消息不再以时间戳记(timestamp)进行标记,而是以随机的nonce进行标记,当送出ping或findnode讯息时附加一个nonce,而之后送回的pong或neighbor也应该附加相对应的nonce才视作合法讯息。

作者自己也坦承,这样的改良只能保证pong或neighbor不会重放,却无法保证ping或findnode本身重放.

以上便是波士顿大学团队研究所提出的几种日蚀攻击手法,在论文中,作者还有提出一些更深入的分析及实验,例如,怎么样去放大攻击的规模,怎么样做出最有效率(最省成本)的攻击。

后记

由于Ethereum重大改版在即,此论文中关于Ethereum的P2P基础都面临资讯过期的可能,再加上还没去实作论文中的实验,曾经一度中断了此文的撰写。

在这边必须感谢台北以太坊社群( Taipei Ethereum Meetup )的雅信以及陈品,在他们的鼓励(督促?)之下,最终还是把这篇文章完成了。

最后,谢谢读者们花时间阅读本文,波士顿大学团队的这篇日蚀攻击论文,其内容既深且广,若是我的文章中有描述不完整或谬误之处,还请大家不吝指教(请来信losesong @hotmail.com),谢谢。

—-

编译者/作者:不详

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

LOADING...
LOADING...