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),謝謝。